/** * MCP tools for rTrips (travel planning). * * Tools: rtrips_list_trips, rtrips_get_trip, rtrips_list_itinerary, rtrips_list_expenses */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { SyncServer } from "../local-first/sync-server"; import type { TripDoc } from "../../modules/rtrips/schemas"; import { resolveAccess, accessDeniedResponse } from "./_auth"; const TRIP_PREFIX = ":trips:trips:"; function findTripDocIds(syncServer: SyncServer, space: string): string[] { const prefix = `${space}${TRIP_PREFIX}`; return syncServer.getDocIds().filter(id => id.startsWith(prefix)); } export function registerTripsTools(server: McpServer, syncServer: SyncServer) { server.tool( "rtrips_list_trips", "List all trips in a space with budget summaries", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), }, async ({ space, token }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const docIds = findTripDocIds(syncServer, space); const trips = []; for (const docId of docIds) { const doc = syncServer.getDoc(docId); if (!doc?.trip) continue; const t = doc.trip; trips.push({ id: t.id, title: t.title, slug: t.slug, status: t.status, startDate: t.startDate, endDate: t.endDate, budgetTotal: t.budgetTotal, budgetCurrency: t.budgetCurrency, destinationCount: Object.keys(doc.destinations || {}).length, bookingCount: Object.keys(doc.bookings || {}).length, expenseCount: Object.keys(doc.expenses || {}).length, createdAt: t.createdAt, }); } return { content: [{ type: "text", text: JSON.stringify(trips, null, 2) }] }; }, ); server.tool( "rtrips_get_trip", "Get full trip details with destinations and bookings", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), trip_id: z.string().describe("Trip ID"), }, async ({ space, token, trip_id }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const docId = `${space}${TRIP_PREFIX}${trip_id}`; const doc = syncServer.getDoc(docId); if (!doc?.trip) { return { content: [{ type: "text", text: JSON.stringify({ error: "Trip not found" }) }] }; } return { content: [{ type: "text", text: JSON.stringify({ trip: doc.trip, destinations: Object.values(doc.destinations || {}), bookings: Object.values(doc.bookings || {}), packingItemCount: Object.keys(doc.packingItems || {}).length, expenseTotal: Object.values(doc.expenses || {}).reduce((sum, e) => sum + (e.amount || 0), 0), }, null, 2), }], }; }, ); server.tool( "rtrips_list_itinerary", "List itinerary items for a trip, sorted by date", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), trip_id: z.string().describe("Trip ID"), date: z.string().optional().describe("Filter by date (YYYY-MM-DD)"), }, async ({ space, token, trip_id, date }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const docId = `${space}${TRIP_PREFIX}${trip_id}`; const doc = syncServer.getDoc(docId); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "Trip not found" }) }] }; } let items = Object.values(doc.itinerary || {}); if (date) items = items.filter(i => i.date === date); items.sort((a, b) => (a.date || "").localeCompare(b.date || "") || a.sortOrder - b.sortOrder); return { content: [{ type: "text", text: JSON.stringify(items, null, 2) }] }; }, ); server.tool( "rtrips_list_expenses", "List trip expenses with totals by category", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), trip_id: z.string().describe("Trip ID"), }, async ({ space, token, trip_id }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const docId = `${space}${TRIP_PREFIX}${trip_id}`; const doc = syncServer.getDoc(docId); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "Trip not found" }) }] }; } const expenses = Object.values(doc.expenses || {}); const total = expenses.reduce((sum, e) => sum + (e.amount || 0), 0); const byCategory: Record = {}; for (const e of expenses) { const cat = e.category || "uncategorized"; byCategory[cat] = (byCategory[cat] || 0) + (e.amount || 0); } return { content: [{ type: "text", text: JSON.stringify({ expenses, total, currency: doc.trip?.budgetCurrency || null, byCategory, }, null, 2), }], }; }, ); }