mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
feat(mcp): booking link and end time on a reservation
url is persisted on create and update and rendered as a link by the planner, and reservation_end_time is what makes a dinner run 19:00-21:30 rather than just starting. Neither was in a tool schema, so a booking written by an assistant lost both - which for an imported itinerary is every confirmation link it came with. url validates through reservationUrlSchema from @trek/shared, the same refinement the REST route applies, rather than a second hand-written one.
This commit is contained in:
@@ -13,7 +13,7 @@ import { DaysService } from '../days/days.service';
|
||||
import { findByIata } from '../airports/airports.data';
|
||||
import type { EndpointInput } from './reservations.service';
|
||||
import { AssignmentsService } from '../assignments/assignments.service';
|
||||
import { transportLegsInputSchema, type TransportLegInput } from '@trek/shared';
|
||||
import { transportLegsInputSchema, reservationUrlSchema, type TransportLegInput } from '@trek/shared';
|
||||
|
||||
// What counts as a transport booking, for the update_transport gate. Every value
|
||||
// ReservationsPanel renders with a transport icon, so a stored `transit` row is
|
||||
@@ -34,6 +34,15 @@ const LEG_TRANSPORT_TYPES = ['flight', 'train'] as const;
|
||||
/** Everything the picker offers that is not a transport — create_reservation's half. */
|
||||
const BOOKING_TYPES = ['hotel', 'restaurant', 'event', 'tour', 'activity', 'parking', 'other'] as const;
|
||||
|
||||
/**
|
||||
* The booking link. Same refinement the REST contract applies
|
||||
* (shared/src/reservation/reservation.schema.ts), so a javascript:/data:/vbscript:
|
||||
* URL is refused on this surface too rather than being stored and later rendered
|
||||
* as an href.
|
||||
*/
|
||||
const urlField = reservationUrlSchema.max(2000).optional()
|
||||
.describe('Link to the booking — the airline/hotel confirmation page, the ticket. Shown as a link on the booking.');
|
||||
|
||||
type TransportType = typeof CREATABLE_TRANSPORT_TYPES[number];
|
||||
type BookingType = typeof BOOKING_TYPES[number];
|
||||
|
||||
@@ -324,6 +333,8 @@ export class ReservationsMcp {
|
||||
title: z.string().min(1).max(200),
|
||||
type: z.enum(BOOKING_TYPES).describe('Reservation type: "hotel", "restaurant", "event", "tour", "activity", "parking", or "other"'),
|
||||
reservation_time: z.string().optional().describe('ISO 8601 datetime or time string'),
|
||||
reservation_end_time: z.string().optional().describe('When it ends — a dinner from 19:00 to 21:30, a tour that runs all afternoon. Ignored for hotels, whose span is the check-in/check-out days.'),
|
||||
url: urlField,
|
||||
location: z.string().max(500).optional(),
|
||||
confirmation_number: z.string().max(100).optional(),
|
||||
notes: z.string().max(1000).optional(),
|
||||
@@ -341,9 +352,9 @@ export class ReservationsMcp {
|
||||
access: { group: 'reservations', mode: 'write' },
|
||||
})
|
||||
async createReservation(
|
||||
{ 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, price, budget_category }: {
|
||||
{ tripId, title, type, reservation_time, reservation_end_time, url, location, confirmation_number, notes, day_id, place_id, start_day_id, end_day_id, check_in, check_out, assignment_id, price, budget_category }: {
|
||||
tripId: number; title: string; type: BookingType;
|
||||
reservation_time?: string; location?: string; confirmation_number?: string; notes?: string;
|
||||
reservation_time?: string; reservation_end_time?: string; url?: string; location?: string; confirmation_number?: string; notes?: string;
|
||||
day_id?: number; place_id?: number; start_day_id?: number; end_day_id?: number;
|
||||
check_in?: string; check_out?: string; assignment_id?: number; price?: number; budget_category?: string;
|
||||
},
|
||||
@@ -372,7 +383,7 @@ export class ReservationsMcp {
|
||||
const metadata = price != null ? { price: String(price) } : undefined;
|
||||
|
||||
const { reservation, accommodationCreated } = this.reservations.create(tripId, {
|
||||
title, type, reservation_time, location, confirmation_number,
|
||||
title, type, reservation_time, reservation_end_time, url, location, confirmation_number,
|
||||
notes, day_id, place_id, assignment_id,
|
||||
create_accommodation: createAccommodation,
|
||||
metadata,
|
||||
@@ -404,6 +415,8 @@ export class ReservationsMcp {
|
||||
title: z.string().min(1).max(200).optional(),
|
||||
type: z.enum(BOOKING_TYPES).optional().describe('Reservation type: "hotel", "restaurant", "event", "tour", "activity", "parking", or "other"'),
|
||||
reservation_time: z.string().optional().describe('ISO 8601 datetime or time string'),
|
||||
reservation_end_time: z.string().optional().describe('When it ends. Ignored for hotels, whose span is the check-in/check-out days.'),
|
||||
url: urlField,
|
||||
location: z.string().max(500).optional(),
|
||||
confirmation_number: z.string().max(100).optional(),
|
||||
notes: z.string().max(1000).optional(),
|
||||
@@ -415,10 +428,10 @@ export class ReservationsMcp {
|
||||
access: { group: 'reservations', mode: 'write' },
|
||||
})
|
||||
async updateReservation(
|
||||
{ tripId, reservationId, title, type, reservation_time, location, confirmation_number, notes, status, place_id, assignment_id }: {
|
||||
{ tripId, reservationId, title, type, reservation_time, reservation_end_time, url, location, confirmation_number, notes, status, place_id, assignment_id }: {
|
||||
tripId: number; reservationId: number; title?: string;
|
||||
type?: BookingType;
|
||||
reservation_time?: string; location?: string; confirmation_number?: string; notes?: string;
|
||||
reservation_time?: string; reservation_end_time?: string; url?: string; location?: string; confirmation_number?: string; notes?: string;
|
||||
status?: 'pending' | 'confirmed' | 'cancelled'; place_id?: number | null; assignment_id?: number | null;
|
||||
},
|
||||
ctx: McpContext,
|
||||
@@ -435,7 +448,7 @@ export class ReservationsMcp {
|
||||
return errorResult('assignment_id does not belong to this trip.');
|
||||
|
||||
const { reservation } = this.reservations.update(reservationId, tripId, {
|
||||
title, type, reservation_time, location, confirmation_number, notes, status,
|
||||
title, type, reservation_time, reservation_end_time, url, location, confirmation_number, notes, status,
|
||||
place_id: place_id !== undefined ? place_id ?? undefined : undefined,
|
||||
assignment_id: assignment_id !== undefined ? assignment_id ?? undefined : undefined,
|
||||
}, existing);
|
||||
@@ -635,6 +648,7 @@ export class ReservationsMcp {
|
||||
reservation_time: z.string().optional().describe('ISO 8601 datetime or time string for departure'),
|
||||
reservation_end_time: z.string().optional().describe('ISO 8601 datetime or time string for arrival'),
|
||||
confirmation_number: z.string().max(100).optional(),
|
||||
url: urlField,
|
||||
notes: z.string().max(1000).optional(),
|
||||
metadata: z.record(z.string(), z.string()).optional().describe('Type-specific metadata: flights → { airline, flight_number, departure_airport, arrival_airport }; trains → { train_number, platform, seat }. Values are plain strings, so per-segment detail belongs in legs[], not here.'),
|
||||
endpoints: endpointSchema,
|
||||
@@ -647,10 +661,10 @@ export class ReservationsMcp {
|
||||
access: { group: 'reservations', mode: 'write' },
|
||||
})
|
||||
async createTransport(
|
||||
{ tripId, type, title, status, start_day_id, end_day_id, reservation_time, reservation_end_time, confirmation_number, notes, metadata, endpoints, legs, needs_review, price, budget_category }: {
|
||||
{ tripId, type, title, status, start_day_id, end_day_id, reservation_time, reservation_end_time, confirmation_number, url, notes, metadata, endpoints, legs, needs_review, price, budget_category }: {
|
||||
tripId: number; type: TransportType; title: string;
|
||||
status?: 'pending' | 'confirmed' | 'cancelled'; start_day_id?: number; end_day_id?: number;
|
||||
reservation_time?: string; reservation_end_time?: string; confirmation_number?: string; notes?: string;
|
||||
reservation_time?: string; reservation_end_time?: string; confirmation_number?: string; url?: string; notes?: string;
|
||||
metadata?: Record<string, string>; endpoints?: TransportEndpoint[]; legs?: TransportLegInput[]; needs_review?: boolean;
|
||||
price?: number; budget_category?: string;
|
||||
},
|
||||
@@ -708,6 +722,7 @@ export class ReservationsMcp {
|
||||
reservation_end_time: arrivalTime,
|
||||
location: undefined,
|
||||
confirmation_number,
|
||||
url,
|
||||
notes,
|
||||
day_id: dayId,
|
||||
end_day_id: spanEndDayId ?? dayId,
|
||||
@@ -744,6 +759,7 @@ export class ReservationsMcp {
|
||||
reservation_time: z.string().optional().describe('ISO 8601 datetime or time string for departure'),
|
||||
reservation_end_time: z.string().optional().describe('ISO 8601 datetime or time string for arrival'),
|
||||
confirmation_number: z.string().max(100).optional(),
|
||||
url: urlField,
|
||||
notes: z.string().max(1000).optional(),
|
||||
metadata: z.record(z.string(), z.string()).optional().describe('Type-specific metadata: flights → { airline, flight_number, departure_airport, arrival_airport }; trains → { train_number, platform, seat }. Replaces the stored metadata wholesale. Values are plain strings, so per-segment detail belongs in legs[], not here.'),
|
||||
endpoints: endpointSchema,
|
||||
@@ -754,10 +770,10 @@ export class ReservationsMcp {
|
||||
access: { group: 'reservations', mode: 'write' },
|
||||
})
|
||||
async updateTransport(
|
||||
{ tripId, reservationId, type, title, status, start_day_id, end_day_id, reservation_time, reservation_end_time, confirmation_number, notes, metadata, endpoints, legs, needs_review }: {
|
||||
{ tripId, reservationId, type, title, status, start_day_id, end_day_id, reservation_time, reservation_end_time, confirmation_number, url, notes, metadata, endpoints, legs, needs_review }: {
|
||||
tripId: number; reservationId: number; type?: TransportType; title?: string;
|
||||
status?: 'pending' | 'confirmed' | 'cancelled'; start_day_id?: number; end_day_id?: number;
|
||||
reservation_time?: string; reservation_end_time?: string; confirmation_number?: string; notes?: string;
|
||||
reservation_time?: string; reservation_end_time?: string; confirmation_number?: string; url?: string; notes?: string;
|
||||
metadata?: Record<string, string>; endpoints?: TransportEndpoint[]; legs?: TransportLegInput[]; needs_review?: boolean;
|
||||
},
|
||||
ctx: McpContext,
|
||||
@@ -834,6 +850,7 @@ export class ReservationsMcp {
|
||||
reservation_time: departureTime,
|
||||
reservation_end_time: arrivalTime,
|
||||
confirmation_number,
|
||||
url,
|
||||
notes,
|
||||
day_id: dayId,
|
||||
end_day_id: spanEndDayId,
|
||||
|
||||
Reference in New Issue
Block a user