diff --git a/server/src/nest/places/places.mcp.ts b/server/src/nest/places/places.mcp.ts index 5b4feb38b..995abf9d8 100644 --- a/server/src/nest/places/places.mcp.ts +++ b/server/src/nest/places/places.mcp.ts @@ -76,7 +76,7 @@ export class PlacesMcp { image_url: placeImageUrlSchema.optional().describe('Thumbnail for the place: an /uploads/ path, an /api/maps/place-photo/ path, an inline data: image, or an https URL'), price: z.number().nonnegative().optional().describe('Cost of this place/activity (e.g. ticket price, entry fee)'), currency: z.string().length(3).optional().describe('ISO 4217 currency code (e.g. "EUR", "USD")'), - stop_type: roadtripStopTypeSchema.optional().describe('Marks the place as a stop on a drive rather than a destination: fuel, charging, rest_area or campsite. Leave unset for an ordinary place.'), + stop_type: roadtripStopTypeSchema.optional().describe('Marks the place as a stop on a drive rather than a destination: fuel, charging, rest_area, campsite, restaurant or sights. A service stop is left out of the stop count for the day, so a day with a charger between four places still reads as four stops. Leave unset for an ordinary place.'), }, annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, access: { group: 'places', mode: 'write' }, @@ -120,16 +120,17 @@ export class PlacesMcp { assignment_notes: z.string().max(500).optional().describe('Notes for this day assignment'), price: z.number().nonnegative().optional().describe('Cost of this place/activity (e.g. ticket price, entry fee)'), currency: z.string().length(3).optional().describe('ISO 4217 currency code (e.g. "EUR", "USD")'), + stop_type: roadtripStopTypeSchema.optional().describe('Marks the place as a stop on a drive rather than a destination: fuel, charging, rest_area, campsite, restaurant or sights. A service stop is left out of the stop count for the day, so a day with a charger between four places still reads as four stops. Leave unset for an ordinary place.'), }, annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, access: { group: 'places', mode: 'write' }, }) async createAndAssignPlace( - { tripId, dayId, name, description, lat, lng, address, category_id, google_place_id, google_ftid, osm_id, place_notes, website, phone, image_url, assignment_notes, price, currency }: { + { tripId, dayId, name, description, lat, lng, address, category_id, google_place_id, google_ftid, osm_id, place_notes, website, phone, image_url, assignment_notes, price, currency, stop_type }: { tripId: number; dayId: number; name: string; description?: string; lat?: number; lng?: number; address?: string; category_id?: number; google_place_id?: string; google_ftid?: string; osm_id?: string; place_notes?: string; website?: string; phone?: string; image_url?: string; assignment_notes?: string; - price?: number; currency?: string; + price?: number; currency?: string; stop_type?: RoadtripStopType; }, ctx: McpContext, ) { @@ -139,7 +140,7 @@ export class PlacesMcp { if (!this.assignments.dayExists(dayId, tripId)) return { content: [{ type: 'text' as const, text: 'Day not found.' }], isError: true }; try { const result = this.db.transaction(() => { - const place = this.places.create(String(tripId), { name, description, lat, lng, address, category_id, google_place_id, google_ftid, osm_id, notes: place_notes, website, phone, image_url, price, currency }); + const place = this.places.create(String(tripId), { name, description, lat, lng, address, category_id, google_place_id, google_ftid, osm_id, notes: place_notes, website, phone, image_url, price, currency, stop_type }); const assignment = this.assignments.createAssignment(dayId, place.id, assignment_notes ?? null); return { place, assignment }; }); @@ -177,25 +178,26 @@ export class PlacesMcp { osm_id: z.string().optional().describe('OpenStreetMap ID (e.g. "way:12345")'), google_place_id: z.string().optional().describe('Google Place ID (e.g. "ChIJd8BlQ2BZwokRAFUEcm_qrcA")'), google_ftid: z.string().optional().describe('Google Maps feature ID (e.g. "0x89c259b7abdd4769:0x103aaf1c8bf8a050")'), + stop_type: roadtripStopTypeSchema.nullable().optional().describe('What kind of stop on a drive this is: fuel, charging, rest_area, campsite, restaurant or sights. Pass null to turn a service stop back into an ordinary place.'), }, annotations: TOOL_ANNOTATIONS_WRITE, access: { group: 'places', mode: 'write' }, }) async updatePlace( - { tripId, placeId, name, description, lat, lng, address, category_id, price, currency, place_time, end_time, duration_minutes, notes, website, phone, image_url, transport_mode, osm_id, google_place_id, google_ftid }: { + { tripId, placeId, name, description, lat, lng, address, category_id, price, currency, place_time, end_time, duration_minutes, notes, website, phone, image_url, transport_mode, osm_id, google_place_id, google_ftid, stop_type }: { tripId: number; placeId: number; name?: string; description?: string; lat?: number; lng?: number; address?: string; category_id?: number; price?: number; currency?: string; place_time?: string; end_time?: string; duration_minutes?: number; notes?: string; website?: string; phone?: string; image_url?: string | null; transport_mode?: 'walking' | 'driving' | 'cycling' | 'transit' | 'flight'; osm_id?: string; - google_place_id?: string; google_ftid?: string; + google_place_id?: string; google_ftid?: string; stop_type?: RoadtripStopType | null; }, ctx: McpContext, ) { if (this.auth.isDemoUser(ctx.userId)) return demoDenied(); if (!this.db.canAccessTrip(tripId, ctx.userId)) return noAccess(); if (!this.guards.hasTripPermission('place_edit', tripId, ctx.userId)) return permissionDenied(); - const place = await this.places.update(String(tripId), String(placeId), { name, description, lat, lng, address, category_id, price, currency, place_time, end_time, duration_minutes, notes, website, phone, image_url, transport_mode, osm_id, google_place_id, google_ftid }); + const place = await this.places.update(String(tripId), String(placeId), { name, description, lat, lng, address, category_id, price, currency, place_time, end_time, duration_minutes, notes, website, phone, image_url, transport_mode, osm_id, google_place_id, google_ftid, stop_type }); if (!place) return { content: [{ type: 'text' as const, text: 'Place not found.' }], isError: true }; this.guards.safeBroadcast(tripId, 'place:updated', { place }); return ok({ place }); @@ -421,16 +423,17 @@ export class PlacesMcp { phone: z.string().max(50).optional(), image_url: placeImageUrlSchema.nullable().optional().describe('Thumbnail for every listed place: an /uploads/ path, an /api/maps/place-photo/ path, an inline data: image, or an https URL. Pass null to strip the pictures off a batch at once'), description: z.string().max(2000).optional(), + stop_type: roadtripStopTypeSchema.nullable().optional().describe('What kind of stop on a drive this is: fuel, charging, rest_area, campsite, restaurant or sights. Pass null to turn a service stop back into an ordinary place.'), }, annotations: TOOL_ANNOTATIONS_WRITE, access: { group: 'places', mode: 'write' }, }) async bulkUpdatePlaces( - { tripId, placeIds, category_id, price, currency, transport_mode, place_time, end_time, duration_minutes, notes, website, phone, image_url, description }: { + { tripId, placeIds, category_id, price, currency, transport_mode, place_time, end_time, duration_minutes, notes, website, phone, image_url, description, stop_type }: { tripId: number; placeIds: number[]; category_id?: number; price?: number; currency?: string; transport_mode?: 'walking' | 'driving' | 'cycling' | 'transit' | 'flight'; place_time?: string; end_time?: string; duration_minutes?: number; notes?: string; website?: string; phone?: string; - image_url?: string | null; description?: string; + image_url?: string | null; description?: string; stop_type?: RoadtripStopType | null; }, ctx: McpContext, ) { @@ -438,7 +441,7 @@ export class PlacesMcp { if (!this.db.canAccessTrip(tripId, ctx.userId)) return noAccess(); if (!this.guards.hasTripPermission('place_edit', tripId, ctx.userId)) return permissionDenied(); - const fields = { category_id, price, currency, transport_mode, place_time, end_time, duration_minutes, notes, website, phone, image_url, description }; + const fields = { category_id, price, currency, transport_mode, place_time, end_time, duration_minutes, notes, website, phone, image_url, description, stop_type }; if (Object.values(fields).every(v => v === undefined)) { return { content: [{ type: 'text' as const, text: 'Provide at least one field to update.' }], isError: true }; }