mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
093e069ccc
* refactor(auth): session token validation and password-change consistency * refactor(journey): entry field allow-list and public share-link consistency * refactor(mcp): align tool authorization with the REST permission checks * chore: input validation and sanitisation touch-ups (uploads, pdf, maps, backup, csp)
190 lines
8.4 KiB
TypeScript
190 lines
8.4 KiB
TypeScript
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 });
|
|
}
|
|
);
|
|
}
|