Spaces:
Sleeping
Sleeping
| import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; | |
| import { z } from 'zod'; | |
| import { canAccessTrip } from '../../db/database'; | |
| import { isDemoUser } from '../../services/authService'; | |
| import { | |
| dayExists, placeExists, createAssignment, assignmentExistsInDay, | |
| deleteAssignment, reorderAssignments, getAssignmentForTrip, updateTime, | |
| moveAssignment, | |
| getParticipants as getAssignmentParticipants, | |
| setParticipants as setAssignmentParticipants, | |
| } from '../../services/assignmentService'; | |
| import { getDay } from '../../services/dayService'; | |
| import { | |
| safeBroadcast, TOOL_ANNOTATIONS_READONLY, TOOL_ANNOTATIONS_WRITE, TOOL_ANNOTATIONS_DELETE, | |
| TOOL_ANNOTATIONS_NON_IDEMPOTENT, | |
| demoDenied, noAccess, ok, hasTripPermission, permissionDenied, | |
| } from './_shared'; | |
| import { canRead, canWrite } from '../scopes'; | |
| export function registerAssignmentTools(server: McpServer, userId: number, scopes: string[] | null): void { | |
| const R = canRead(scopes, 'places'); | |
| const W = canWrite(scopes, 'places'); | |
| // --- ASSIGNMENTS --- | |
| if (W) server.registerTool( | |
| 'assign_place_to_day', | |
| { | |
| description: 'Assign a place to a specific day in a trip.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| dayId: z.number().int().positive(), | |
| 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(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('day_edit', tripId, userId)) return permissionDenied(); | |
| if (!dayExists(dayId, tripId)) return { content: [{ type: 'text' as const, text: 'Day not found.' }], isError: true }; | |
| if (!placeExists(placeId, tripId)) return { content: [{ type: 'text' as const, text: 'Place not found.' }], isError: true }; | |
| const assignment = createAssignment(dayId, placeId, notes || null); | |
| safeBroadcast(tripId, 'assignment:created', { assignment }); | |
| return ok({ assignment }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'unassign_place', | |
| { | |
| description: 'Remove a place assignment from a day.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| dayId: z.number().int().positive(), | |
| assignmentId: z.number().int().positive(), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_DELETE, | |
| }, | |
| async ({ tripId, dayId, assignmentId }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('day_edit', tripId, userId)) return permissionDenied(); | |
| if (!assignmentExistsInDay(assignmentId, dayId, tripId)) | |
| return { content: [{ type: 'text' as const, text: 'Assignment not found.' }], isError: true }; | |
| deleteAssignment(assignmentId); | |
| safeBroadcast(tripId, 'assignment:deleted', { assignmentId, dayId }); | |
| return ok({ success: true }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'update_assignment_time', | |
| { | |
| description: 'Set the start and/or end time for a place assignment on a day (e.g. "09:00", "11:30"). Pass null to clear a time.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| assignmentId: z.number().int().positive(), | |
| 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(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('day_edit', tripId, userId)) return permissionDenied(); | |
| const existing = getAssignmentForTrip(assignmentId, tripId); | |
| if (!existing) return { content: [{ type: 'text' as const, text: 'Assignment not found.' }], isError: true }; | |
| const assignment = updateTime( | |
| assignmentId, | |
| place_time !== undefined ? place_time : (existing as any).assignment_time, | |
| end_time !== undefined ? end_time : (existing as any).assignment_end_time | |
| ); | |
| safeBroadcast(tripId, 'assignment:updated', { assignment }); | |
| return ok({ assignment }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'move_assignment', | |
| { | |
| description: 'Move a place assignment to a different day.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| assignmentId: z.number().int().positive(), | |
| newDayId: z.number().int().positive(), | |
| oldDayId: z.number().int().positive(), | |
| orderIndex: z.number().int().min(0).optional().default(0), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_WRITE, | |
| }, | |
| async ({ tripId, assignmentId, newDayId, oldDayId, orderIndex }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('day_edit', tripId, userId)) return permissionDenied(); | |
| if (!getAssignmentForTrip(assignmentId, tripId)) return { content: [{ type: 'text' as const, text: 'Assignment not found.' }], isError: true }; | |
| if (!getDay(newDayId, tripId)) return { content: [{ type: 'text' as const, text: 'Day not found.' }], isError: true }; | |
| const result = moveAssignment(assignmentId, newDayId, orderIndex ?? 0, oldDayId); | |
| safeBroadcast(tripId, 'assignment:moved', { assignment: result.assignment, oldDayId: result.oldDayId }); | |
| return ok({ assignment: result.assignment }); | |
| } | |
| ); | |
| if (R) server.registerTool( | |
| 'get_assignment_participants', | |
| { | |
| description: 'Get the list of users participating in a specific place assignment.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| assignmentId: z.number().int().positive(), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_READONLY, | |
| }, | |
| async ({ tripId, assignmentId }) => { | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!getAssignmentForTrip(assignmentId, tripId)) return { content: [{ type: 'text' as const, text: 'Assignment not found.' }], isError: true }; | |
| const participants = getAssignmentParticipants(assignmentId); | |
| return ok({ participants }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'set_assignment_participants', | |
| { | |
| description: 'Set the participants for a place assignment (replaces current list).', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| assignmentId: z.number().int().positive(), | |
| userIds: z.array(z.number().int().positive()).describe('User IDs to set as participants; empty array clears all'), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_WRITE, | |
| }, | |
| async ({ tripId, assignmentId, userIds }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('day_edit', tripId, userId)) return permissionDenied(); | |
| if (!getAssignmentForTrip(assignmentId, tripId)) return { content: [{ type: 'text' as const, text: 'Assignment not found.' }], isError: true }; | |
| const participants = setAssignmentParticipants(assignmentId, userIds); | |
| safeBroadcast(tripId, 'assignment:participants', { assignmentId, participants }); | |
| return ok({ participants }); | |
| } | |
| ); | |
| // --- REORDER --- | |
| if (W) server.registerTool( | |
| 'reorder_day_assignments', | |
| { | |
| description: 'Reorder places within a day by providing the assignment IDs in the desired order.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| 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_WRITE, | |
| }, | |
| async ({ tripId, dayId, assignmentIds }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('day_edit', tripId, userId)) return permissionDenied(); | |
| if (!getDay(dayId, tripId)) return { content: [{ type: 'text' as const, text: 'Day not found.' }], isError: true }; | |
| reorderAssignments(dayId, assignmentIds); | |
| safeBroadcast(tripId, 'assignment:reordered', { dayId, assignmentIds }); | |
| return ok({ success: true, dayId, order: assignmentIds }); | |
| } | |
| ); | |
| } | |