From a012dffa223053cc5eb9f1742987beec1799b72d Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 6 Apr 2026 10:43:31 +0200 Subject: [PATCH] MCP: add tool annotations, prompts, mimeType, and capabilities - Add tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) to all 40+ tools - Register 3 MCP prompts: trip-summary, packing-list, budget-overview - Add explicit mimeType: application/json to all resource registrations - Announce capabilities with listChanged on resources, tools, prompts - Update server name to 'TREK MCP' in MCP initialization --- server/src/mcp/index.ts | 10 ++- server/src/mcp/resources.ts | 28 +++--- server/src/mcp/tools.ts | 174 ++++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 15 deletions(-) diff --git a/server/src/mcp/index.ts b/server/src/mcp/index.ts index 86714a05..d647dd92 100644 --- a/server/src/mcp/index.ts +++ b/server/src/mcp/index.ts @@ -128,7 +128,15 @@ export async function mcpHandler(req: Request, res: Response): Promise { } // Create a new per-user MCP server and session - const server = new McpServer({ name: 'trek', version: '1.0.0' }); + const server = new McpServer({ + name: 'TREK MCP', + version: '1.0.0', + capabilities: { + resources: { listChanged: true }, + tools: { listChanged: true }, + prompts: { listChanged: true }, + }, + }); registerResources(server, user.id); registerTools(server, user.id); diff --git a/server/src/mcp/resources.ts b/server/src/mcp/resources.ts index b3397839..84582fc3 100644 --- a/server/src/mcp/resources.ts +++ b/server/src/mcp/resources.ts @@ -41,7 +41,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'trips', 'trek://trips', - { description: 'All trips the user owns or is a member of' }, + { description: 'All trips the user owns or is a member of', mimeType: 'application/json' }, async (uri) => { const trips = listTrips(userId, 0); return jsonContent(uri.href, trips); @@ -52,7 +52,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'trip', new ResourceTemplate('trek://trips/{tripId}', { list: undefined }), - { description: 'A single trip with metadata and member count' }, + { description: 'A single trip with metadata and member count', mimeType: 'application/json' }, async (uri, { tripId }) => { const id = parseId(tripId); if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href); @@ -65,7 +65,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'trip-days', new ResourceTemplate('trek://trips/{tripId}/days', { list: undefined }), - { description: 'Days of a trip with their assigned places' }, + { description: 'Days of a trip with their assigned places', mimeType: 'application/json' }, async (uri, { tripId }) => { const id = parseId(tripId); if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href); @@ -79,7 +79,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'trip-places', new ResourceTemplate('trek://trips/{tripId}/places', { list: undefined }), - { description: 'All places/POIs saved in a trip' }, + { description: 'All places/POIs saved in a trip', mimeType: 'application/json' }, async (uri, { tripId }) => { const id = parseId(tripId); if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href); @@ -92,7 +92,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'trip-budget', new ResourceTemplate('trek://trips/{tripId}/budget', { list: undefined }), - { description: 'Budget and expense items for a trip' }, + { description: 'Budget and expense items for a trip', mimeType: 'application/json' }, async (uri, { tripId }) => { const id = parseId(tripId); if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href); @@ -105,7 +105,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'trip-packing', new ResourceTemplate('trek://trips/{tripId}/packing', { list: undefined }), - { description: 'Packing checklist for a trip' }, + { description: 'Packing checklist for a trip', mimeType: 'application/json' }, async (uri, { tripId }) => { const id = parseId(tripId); if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href); @@ -118,7 +118,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'trip-reservations', new ResourceTemplate('trek://trips/{tripId}/reservations', { list: undefined }), - { description: 'Reservations (flights, hotels, restaurants) for a trip' }, + { description: 'Reservations (flights, hotels, restaurants) for a trip', mimeType: 'application/json' }, async (uri, { tripId }) => { const id = parseId(tripId); if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href); @@ -131,7 +131,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'day-notes', new ResourceTemplate('trek://trips/{tripId}/days/{dayId}/notes', { list: undefined }), - { description: 'Notes for a specific day in a trip' }, + { description: 'Notes for a specific day in a trip', mimeType: 'application/json' }, async (uri, { tripId, dayId }) => { const tId = parseId(tripId); const dId = parseId(dayId); @@ -145,7 +145,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'trip-accommodations', new ResourceTemplate('trek://trips/{tripId}/accommodations', { list: undefined }), - { description: 'Accommodations (hotels, rentals) for a trip with check-in/out details' }, + { description: 'Accommodations (hotels, rentals) for a trip with check-in/out details', mimeType: 'application/json' }, async (uri, { tripId }) => { const id = parseId(tripId); if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href); @@ -158,7 +158,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'trip-members', new ResourceTemplate('trek://trips/{tripId}/members', { list: undefined }), - { description: 'Owner and collaborators of a trip' }, + { description: 'Owner and collaborators of a trip', mimeType: 'application/json' }, async (uri, { tripId }) => { const id = parseId(tripId); if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href); @@ -173,7 +173,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'trip-collab-notes', new ResourceTemplate('trek://trips/{tripId}/collab-notes', { list: undefined }), - { description: 'Shared collaborative notes for a trip' }, + { description: 'Shared collaborative notes for a trip', mimeType: 'application/json' }, async (uri, { tripId }) => { const id = parseId(tripId); if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href); @@ -186,7 +186,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'categories', 'trek://categories', - { description: 'All available place categories (id, name, color, icon) for use when creating places' }, + { description: 'All available place categories (id, name, color, icon) for use when creating places', mimeType: 'application/json' }, async (uri) => { const categories = listCategories(); return jsonContent(uri.href, categories); @@ -197,7 +197,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'bucket-list', 'trek://bucket-list', - { description: 'Your personal travel bucket list' }, + { description: 'Your personal travel bucket list', mimeType: 'application/json' }, async (uri) => { const items = listBucketList(userId); return jsonContent(uri.href, items); @@ -208,7 +208,7 @@ export function registerResources(server: McpServer, userId: number): void { server.registerResource( 'visited-countries', 'trek://visited-countries', - { description: 'Countries you have marked as visited in Atlas' }, + { description: 'Countries you have marked as visited in Atlas', mimeType: 'application/json' }, async (uri) => { const countries = listVisitedCountries(userId); return jsonContent(uri.href, countries); diff --git a/server/src/mcp/tools.ts b/server/src/mcp/tools.ts index b2dbd8aa..547fbcf4 100644 --- a/server/src/mcp/tools.ts +++ b/server/src/mcp/tools.ts @@ -26,6 +26,34 @@ import { searchPlaces } from '../services/mapsService'; const MAX_MCP_TRIP_DAYS = 90; +const TOOL_ANNOTATIONS_READONLY = { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, +} as const; + +const TOOL_ANNOTATIONS_WRITE = { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, +} as const; + +const TOOL_ANNOTATIONS_DELETE = { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: false, +} as const; + +const TOOL_ANNOTATIONS_NON_IDEMPOTENT = { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, +} as const; + function demoDenied() { return { content: [{ type: 'text' as const, text: 'Write operations are disabled in demo mode.' }], isError: true }; } @@ -52,6 +80,7 @@ export function registerTools(server: McpServer, userId: number): void { end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe('End date (YYYY-MM-DD)'), currency: z.string().length(3).optional().describe('Currency code (e.g. EUR, USD)'), }, + annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, }, async ({ title, description, start_date, end_date, currency }) => { if (isDemoUser(userId)) return demoDenied(); @@ -85,6 +114,7 @@ export function registerTools(server: McpServer, userId: number): void { end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), currency: z.string().length(3).optional(), }, + annotations: TOOL_ANNOTATIONS_WRITE, }, async ({ tripId, title, description, start_date, end_date, currency }) => { if (isDemoUser(userId)) return demoDenied(); @@ -112,6 +142,7 @@ export function registerTools(server: McpServer, userId: number): void { inputSchema: { tripId: z.number().int().positive(), }, + annotations: TOOL_ANNOTATIONS_DELETE, }, async ({ tripId }) => { if (isDemoUser(userId)) return demoDenied(); @@ -128,6 +159,7 @@ export function registerTools(server: McpServer, userId: number): void { inputSchema: { include_archived: z.boolean().optional().describe('Include archived trips (default false)'), }, + annotations: TOOL_ANNOTATIONS_READONLY, }, async ({ include_archived }) => { const trips = listTrips(userId, include_archived ? null : 0); @@ -155,6 +187,7 @@ export function registerTools(server: McpServer, userId: number): void { website: z.string().max(500).optional(), phone: z.string().max(50).optional(), }, + annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, }, async ({ tripId, name, description, lat, lng, address, category_id, google_place_id, osm_id, notes, website, phone }) => { if (isDemoUser(userId)) return demoDenied(); @@ -181,6 +214,7 @@ export function registerTools(server: McpServer, userId: number): void { website: z.string().max(500).optional(), phone: z.string().max(50).optional(), }, + annotations: TOOL_ANNOTATIONS_WRITE, }, async ({ tripId, placeId, name, description, lat, lng, address, notes, website, phone }) => { if (isDemoUser(userId)) return demoDenied(); @@ -200,6 +234,7 @@ export function registerTools(server: McpServer, userId: number): void { tripId: z.number().int().positive(), placeId: z.number().int().positive(), }, + annotations: TOOL_ANNOTATIONS_DELETE, }, async ({ tripId, placeId }) => { if (isDemoUser(userId)) return demoDenied(); @@ -218,6 +253,7 @@ export function registerTools(server: McpServer, userId: number): void { { description: 'List all available place categories with their id, name, icon and color. Use category_id when creating or updating places.', inputSchema: {}, + annotations: TOOL_ANNOTATIONS_READONLY, }, async () => { const categories = listCategories(); @@ -234,6 +270,7 @@ export function registerTools(server: McpServer, userId: number): void { inputSchema: { query: z.string().min(1).max(500).describe('Place name or address to search for'), }, + annotations: TOOL_ANNOTATIONS_READONLY, }, async ({ query }) => { try { @@ -257,6 +294,7 @@ export function registerTools(server: McpServer, userId: number): void { placeId: z.number().int().positive(), notes: z.string().max(500).optional(), }, + annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, }, async ({ tripId, dayId, placeId, notes }) => { if (isDemoUser(userId)) return demoDenied(); @@ -278,6 +316,7 @@ export function registerTools(server: McpServer, userId: number): void { dayId: z.number().int().positive(), assignmentId: z.number().int().positive(), }, + annotations: TOOL_ANNOTATIONS_DELETE, }, async ({ tripId, dayId, assignmentId }) => { if (isDemoUser(userId)) return demoDenied(); @@ -303,6 +342,7 @@ export function registerTools(server: McpServer, userId: number): void { total_price: z.number().nonnegative(), note: z.string().max(500).optional(), }, + annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, }, async ({ tripId, name, category, total_price, note }) => { if (isDemoUser(userId)) return demoDenied(); @@ -321,6 +361,7 @@ export function registerTools(server: McpServer, userId: number): void { tripId: z.number().int().positive(), itemId: z.number().int().positive(), }, + annotations: TOOL_ANNOTATIONS_DELETE, }, async ({ tripId, itemId }) => { if (isDemoUser(userId)) return demoDenied(); @@ -343,6 +384,7 @@ export function registerTools(server: McpServer, userId: number): void { name: z.string().min(1).max(200), category: z.string().max(100).optional().describe('Packing category (e.g. Clothes, Electronics)'), }, + annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, }, async ({ tripId, name, category }) => { if (isDemoUser(userId)) return demoDenied(); @@ -362,6 +404,7 @@ export function registerTools(server: McpServer, userId: number): void { itemId: z.number().int().positive(), checked: z.boolean(), }, + annotations: TOOL_ANNOTATIONS_WRITE, }, async ({ tripId, itemId, checked }) => { if (isDemoUser(userId)) return demoDenied(); @@ -381,6 +424,7 @@ export function registerTools(server: McpServer, userId: number): void { tripId: z.number().int().positive(), itemId: z.number().int().positive(), }, + annotations: TOOL_ANNOTATIONS_DELETE, }, async ({ tripId, itemId }) => { if (isDemoUser(userId)) return demoDenied(); @@ -414,6 +458,7 @@ export function registerTools(server: McpServer, userId: number): void { check_out: z.string().max(10).optional().describe('Check-out time (e.g. "11:00", hotel type only)'), assignment_id: z.number().int().positive().optional().describe('Link to a day assignment (restaurant, train, car, cruise, event, tour, activity, other)'), }, + annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, }, async ({ tripId, title, type, reservation_time, location, confirmation_number, notes, day_id, place_id, start_day_id, end_day_id, check_in, check_out, assignment_id }) => { if (isDemoUser(userId)) return demoDenied(); @@ -457,6 +502,7 @@ export function registerTools(server: McpServer, userId: number): void { tripId: z.number().int().positive(), reservationId: z.number().int().positive(), }, + annotations: TOOL_ANNOTATIONS_DELETE, }, async ({ tripId, reservationId }) => { if (isDemoUser(userId)) return demoDenied(); @@ -484,6 +530,7 @@ export function registerTools(server: McpServer, userId: number): void { check_in: z.string().max(10).optional().describe('Check-in time (e.g. "15:00")'), check_out: z.string().max(10).optional().describe('Check-out time (e.g. "11:00")'), }, + annotations: TOOL_ANNOTATIONS_WRITE, }, async ({ tripId, reservationId, place_id, start_day_id, end_day_id, check_in, check_out }) => { if (isDemoUser(userId)) return demoDenied(); @@ -525,6 +572,7 @@ export function registerTools(server: McpServer, userId: number): void { place_time: z.string().max(50).nullable().optional().describe('Start time (e.g. "09:00"), or null to clear'), end_time: z.string().max(50).nullable().optional().describe('End time (e.g. "11:00"), or null to clear'), }, + annotations: TOOL_ANNOTATIONS_WRITE, }, async ({ tripId, assignmentId, place_time, end_time }) => { if (isDemoUser(userId)) return demoDenied(); @@ -550,6 +598,7 @@ export function registerTools(server: McpServer, userId: number): void { dayId: z.number().int().positive(), title: z.string().max(200).nullable().describe('Day title, or null to clear it'), }, + annotations: TOOL_ANNOTATIONS_WRITE, }, async ({ tripId, dayId, title }) => { if (isDemoUser(userId)) return demoDenied(); @@ -581,6 +630,7 @@ export function registerTools(server: McpServer, userId: number): void { place_id: z.number().int().positive().nullable().optional().describe('Link to a place (use for hotel type), or null to unlink'), assignment_id: z.number().int().positive().nullable().optional().describe('Link to a day assignment (use for restaurant, train, car, cruise, event, tour, activity, other), or null to unlink'), }, + annotations: TOOL_ANNOTATIONS_WRITE, }, async ({ tripId, reservationId, title, type, reservation_time, location, confirmation_number, notes, status, place_id, assignment_id }) => { if (isDemoUser(userId)) return demoDenied(); @@ -619,6 +669,7 @@ export function registerTools(server: McpServer, userId: number): void { days: z.number().int().positive().nullable().optional(), note: z.string().max(500).nullable().optional(), }, + annotations: TOOL_ANNOTATIONS_WRITE, }, async ({ tripId, itemId, name, category, total_price, persons, days, note }) => { if (isDemoUser(userId)) return demoDenied(); @@ -642,6 +693,7 @@ export function registerTools(server: McpServer, userId: number): void { name: z.string().min(1).max(200).optional(), category: z.string().max(100).optional(), }, + annotations: TOOL_ANNOTATIONS_WRITE, }, async ({ tripId, itemId, name, category }) => { if (isDemoUser(userId)) return demoDenied(); @@ -665,6 +717,7 @@ export function registerTools(server: McpServer, userId: number): void { dayId: z.number().int().positive(), assignmentIds: z.array(z.number().int().positive()).min(1).max(200).describe('Assignment IDs in desired display order'), }, + annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, }, async ({ tripId, dayId, assignmentIds }) => { if (isDemoUser(userId)) return demoDenied(); @@ -685,6 +738,7 @@ export function registerTools(server: McpServer, userId: number): void { inputSchema: { tripId: z.number().int().positive(), }, + annotations: TOOL_ANNOTATIONS_READONLY, }, async ({ tripId }) => { if (!canAccessTrip(tripId, userId)) return noAccess(); @@ -707,6 +761,7 @@ export function registerTools(server: McpServer, userId: number): void { country_code: z.string().length(2).toUpperCase().optional().describe('ISO 3166-1 alpha-2 country code'), notes: z.string().max(1000).optional(), }, + annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, }, async ({ name, lat, lng, country_code, notes }) => { if (isDemoUser(userId)) return demoDenied(); @@ -722,6 +777,7 @@ export function registerTools(server: McpServer, userId: number): void { inputSchema: { itemId: z.number().int().positive(), }, + annotations: TOOL_ANNOTATIONS_DELETE, }, async ({ itemId }) => { if (isDemoUser(userId)) return demoDenied(); @@ -740,6 +796,7 @@ export function registerTools(server: McpServer, userId: number): void { inputSchema: { country_code: z.string().length(2).toUpperCase().describe('ISO 3166-1 alpha-2 country code (e.g. "FR", "JP")'), }, + annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, }, async ({ country_code }) => { if (isDemoUser(userId)) return demoDenied(); @@ -755,6 +812,7 @@ export function registerTools(server: McpServer, userId: number): void { inputSchema: { country_code: z.string().length(2).toUpperCase().describe('ISO 3166-1 alpha-2 country code'), }, + annotations: TOOL_ANNOTATIONS_DELETE, }, async ({ country_code }) => { if (isDemoUser(userId)) return demoDenied(); @@ -776,6 +834,7 @@ export function registerTools(server: McpServer, userId: number): void { category: z.string().max(100).optional().describe('Note category (e.g. "Ideas", "To-do", "General")'), color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe('Hex color for the note card'), }, + annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, }, async ({ tripId, title, content, category, color }) => { if (isDemoUser(userId)) return demoDenied(); @@ -799,6 +858,7 @@ export function registerTools(server: McpServer, userId: number): void { color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe('Hex color for the note card'), pinned: z.boolean().optional().describe('Pin the note to the top'), }, + annotations: TOOL_ANNOTATIONS_WRITE, }, async ({ tripId, noteId, title, content, category, color, pinned }) => { if (isDemoUser(userId)) return demoDenied(); @@ -818,6 +878,7 @@ export function registerTools(server: McpServer, userId: number): void { tripId: z.number().int().positive(), noteId: z.number().int().positive(), }, + annotations: TOOL_ANNOTATIONS_DELETE, }, async ({ tripId, noteId }) => { if (isDemoUser(userId)) return demoDenied(); @@ -842,6 +903,7 @@ export function registerTools(server: McpServer, userId: number): void { time: z.string().max(150).optional().describe('Time label (e.g. "09:00" or "Morning")'), icon: z.string().optional().describe('Emoji icon for the note'), }, + annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, }, async ({ tripId, dayId, text, time, icon }) => { if (isDemoUser(userId)) return demoDenied(); @@ -865,6 +927,7 @@ export function registerTools(server: McpServer, userId: number): void { time: z.string().max(150).nullable().optional().describe('Time label (e.g. "09:00" or "Morning"), or null to clear'), icon: z.string().optional().describe('Emoji icon for the note'), }, + annotations: TOOL_ANNOTATIONS_WRITE, }, async ({ tripId, dayId, noteId, text, time, icon }) => { if (isDemoUser(userId)) return demoDenied(); @@ -886,6 +949,7 @@ export function registerTools(server: McpServer, userId: number): void { dayId: z.number().int().positive(), noteId: z.number().int().positive(), }, + annotations: TOOL_ANNOTATIONS_DELETE, }, async ({ tripId, dayId, noteId }) => { if (isDemoUser(userId)) return demoDenied(); @@ -897,4 +961,114 @@ export function registerTools(server: McpServer, userId: number): void { return ok({ success: true }); } ); + + // --- PROMPTS --- + + server.registerPrompt( + 'trip-summary', + { + title: 'Trip Summary', + description: 'Load a full summary of a trip for context before planning or modifications', + argsSchema: { + tripId: z.number().int().positive().describe('Trip ID to summarize'), + }, + }, + async ({ tripId }) => { + if (!canAccessTrip(tripId, userId)) { + return { messages: [{ role: 'user', content: { type: 'text', text: 'Trip not found or access denied.' } }] }; + } + const summary = getTripSummary(tripId); + if (!summary) { + return { messages: [{ role: 'user', content: { type: 'text', text: 'Trip not found.' } }] }; + } + const { trip, days, members, budget, packing, reservations, collabNotes } = summary; + const packingStats = packing ? { total: packing.length, packed: packing.filter((p: any) => p.checked).length } : { total: 0, packed: 0 }; + const budgetTotal = budget?.reduce((sum: number, b: any) => sum + (b.total_price || 0), 0) || 0; + const text = `Trip: ${trip?.title || 'Untitled'}${trip?.description ? `\n${trip.description}` : ''} +Dates: ${trip?.start_date || '?'} to ${trip?.end_date || '?'} +Members: ${members?.length || 0} (${members?.map((m: any) => m.name || m.email).join(', ') || 'none'}) +Days: ${days?.length || 0} +Packing: ${packingStats.packed}/${packingStats.total} items packed +Budget: ${budgetTotal} ${trip?.currency || 'EUR'} total +Reservations: ${reservations?.length || 0} +Collab Notes: ${collabNotes?.length || 0} +${days?.map((d: any, i: number) => `Day ${i + 1} (${d.date}): ${d.assignments?.length || 0} places${d.title ? ` - ${d.title}` : ''}`).join('\n') || 'No days yet'}`; + return { + description: `Summary of trip "${trip?.title || tripId}"`, + messages: [{ role: 'user', content: { type: 'text', text } }], + }; + } + ); + + server.registerPrompt( + 'packing-list', + { + title: 'Packing List', + description: 'Get a formatted packing checklist for a trip', + argsSchema: { + tripId: z.number().int().positive().describe('Trip ID'), + }, + }, + async ({ tripId }) => { + if (!canAccessTrip(tripId, userId)) { + return { messages: [{ role: 'user', content: { type: 'text', text: 'Trip not found or access denied.' } }] }; + } + const { listPackingItems } = await import('../services/packingService'); + const items = listPackingItems(tripId); + if (!items.length) { + return { messages: [{ role: 'user', content: { type: 'text', text: 'No packing items found for this trip.' } }] }; + } + const grouped = items.reduce((acc: Record, item: any) => { + const cat = item.category || 'General'; + if (!acc[cat]) acc[cat] = []; + acc[cat].push(item); + return acc; + }, {}); + const lines = Object.entries(grouped).map(([cat, items]) => + `## ${cat}\n${(items as any[]).map((i: any) => `- [${i.checked ? 'x' : ' '}] ${i.name}`).join('\n')}` + ).join('\n\n'); + const { trip } = getTripSummary(tripId) || {}; + return { + description: `Packing list for "${trip?.title || tripId}"`, + messages: [{ role: 'user', content: { type: 'text', text: `# Packing List: ${trip?.title || 'Trip'}\n\n${lines}\n\n_${items.length} items across ${Object.keys(grouped).length} categories_` } }], + }; + } + ); + + server.registerPrompt( + 'budget-overview', + { + title: 'Budget Overview', + description: 'Get a formatted budget summary for a trip', + argsSchema: { + tripId: z.number().int().positive().describe('Trip ID'), + }, + }, + async ({ tripId }) => { + if (!canAccessTrip(tripId, userId)) { + return { messages: [{ role: 'user', content: { type: 'text', text: 'Trip not found or access denied.' } }] }; + } + const summary = getTripSummary(tripId); + if (!summary) { + return { messages: [{ role: 'user', content: { type: 'text', text: 'Trip not found.' } }] }; + } + const { trip, budget } = summary; + const currency = trip?.currency || 'EUR'; + const byCategory = (budget || []).reduce((acc: Record, item: any) => { + const cat = item.category || 'Uncategorized'; + acc[cat] = (acc[cat] || 0) + (item.total_price || 0); + return acc; + }, {} as Record); + const total = Object.values(byCategory).reduce((s, v) => s + v, 0); + const lines = Object.entries(byCategory) + .sort(([, a], [, b]) => b - a) + .map(([cat, amount]) => `- ${cat}: ${amount} ${currency}`) + .join('\n'); + const perPerson = (summary.members?.length || 1) > 0 ? (total / (summary.members?.length || 1)).toFixed(2) : total.toFixed(2); + return { + description: `Budget overview for "${trip?.title || tripId}"`, + messages: [{ role: 'user', content: { type: 'text', text: `# Budget: ${trip?.title || 'Trip'}\n\n**Total: ${total} ${currency}** (${perPerson} ${currency} per person)\n\n${lines || 'No expenses recorded.'}` } }], + }; + } + ); }