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 { | |
| createItem as createPackingItem, updateItem as updatePackingItem, | |
| deleteItem as deletePackingItem, | |
| reorderItems as reorderPackingItems, | |
| listBags, createBag, updateBag, deleteBag, setBagMembers, | |
| getCategoryAssignees as getPackingCategoryAssignees, | |
| updateCategoryAssignees as updatePackingCategoryAssignees, | |
| applyTemplate, saveAsTemplate, bulkImport, | |
| } from '../../services/packingService'; | |
| 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'; | |
| import { isAddonEnabled } from '../../services/adminService'; | |
| import { ADDON_IDS } from '../../addons'; | |
| export function registerPackingTools(server: McpServer, userId: number, scopes: string[] | null): void { | |
| const R = canRead(scopes, 'packing'); | |
| const W = canWrite(scopes, 'packing'); | |
| if (!isAddonEnabled(ADDON_IDS.PACKING)) return; | |
| // --- PACKING --- | |
| if (W) server.registerTool( | |
| 'create_packing_item', | |
| { | |
| description: 'Add an item to the packing checklist for a trip.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| 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(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| const item = createPackingItem(tripId, { name, category: category || 'General' }); | |
| safeBroadcast(tripId, 'packing:created', { item }); | |
| return ok({ item }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'toggle_packing_item', | |
| { | |
| description: 'Check or uncheck a packing item.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| itemId: z.number().int().positive(), | |
| checked: z.boolean(), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_WRITE, | |
| }, | |
| async ({ tripId, itemId, checked }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| const item = updatePackingItem(tripId, itemId, { checked: checked ? 1 : 0 }, ['checked']); | |
| if (!item) return { content: [{ type: 'text' as const, text: 'Packing item not found.' }], isError: true }; | |
| safeBroadcast(tripId, 'packing:updated', { item }); | |
| return ok({ item }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'delete_packing_item', | |
| { | |
| description: 'Remove an item from the packing checklist.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| itemId: z.number().int().positive(), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_DELETE, | |
| }, | |
| async ({ tripId, itemId }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| const deleted = deletePackingItem(tripId, itemId); | |
| if (!deleted) return { content: [{ type: 'text' as const, text: 'Packing item not found.' }], isError: true }; | |
| safeBroadcast(tripId, 'packing:deleted', { itemId }); | |
| return ok({ success: true }); | |
| } | |
| ); | |
| // --- PACKING (update) --- | |
| if (W) server.registerTool( | |
| 'update_packing_item', | |
| { | |
| description: 'Rename a packing item or change its category.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| itemId: z.number().int().positive(), | |
| 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(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| const bodyKeys = ['name', 'category'].filter(k => k === 'name' ? name !== undefined : category !== undefined); | |
| const item = updatePackingItem(tripId, itemId, { name, category }, bodyKeys); | |
| if (!item) return { content: [{ type: 'text' as const, text: 'Packing item not found.' }], isError: true }; | |
| safeBroadcast(tripId, 'packing:updated', { item }); | |
| return ok({ item }); | |
| } | |
| ); | |
| // --- PACKING ADVANCED --- | |
| if (W) server.registerTool( | |
| 'reorder_packing_items', | |
| { | |
| description: 'Set the display order of packing items within a trip.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| orderedIds: z.array(z.number().int().positive()).describe('Packing item IDs in desired order'), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_WRITE, | |
| }, | |
| async ({ tripId, orderedIds }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| reorderPackingItems(tripId, orderedIds); | |
| safeBroadcast(tripId, 'packing:reordered', { orderedIds }); | |
| return ok({ success: true }); | |
| } | |
| ); | |
| if (R) server.registerTool( | |
| 'list_packing_bags', | |
| { | |
| description: 'List all packing bags for a trip.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_READONLY, | |
| }, | |
| async ({ tripId }) => { | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| const bags = listBags(tripId); | |
| return ok({ bags }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'create_packing_bag', | |
| { | |
| description: 'Create a new packing bag (e.g. "Carry-on", "Checked bag").', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| name: z.string().min(1).max(100), | |
| color: z.string().optional(), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, | |
| }, | |
| async ({ tripId, name, color }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| const bag = createBag(tripId, { name, color }); | |
| safeBroadcast(tripId, 'packing:bag-created', { bag }); | |
| return ok({ bag }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'update_packing_bag', | |
| { | |
| description: 'Rename or recolor a packing bag.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| bagId: z.number().int().positive(), | |
| name: z.string().optional(), | |
| color: z.string().optional(), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_WRITE, | |
| }, | |
| async ({ tripId, bagId, name, color }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| const fields: Record<string, unknown> = {}; | |
| const bodyKeys: string[] = []; | |
| if (name !== undefined) { fields.name = name; bodyKeys.push('name'); } | |
| if (color !== undefined) { fields.color = color; bodyKeys.push('color'); } | |
| const bag = updateBag(tripId, bagId, fields, bodyKeys); | |
| safeBroadcast(tripId, 'packing:bag-updated', { bag }); | |
| return ok({ bag }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'delete_packing_bag', | |
| { | |
| description: 'Delete a packing bag (items in the bag are unassigned, not deleted).', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| bagId: z.number().int().positive(), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_DELETE, | |
| }, | |
| async ({ tripId, bagId }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| deleteBag(tripId, bagId); | |
| safeBroadcast(tripId, 'packing:bag-deleted', { id: bagId }); | |
| return ok({ success: true }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'set_bag_members', | |
| { | |
| description: 'Assign trip members to a packing bag (determines who packs what bag).', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| bagId: z.number().int().positive(), | |
| userIds: z.array(z.number().int().positive()), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_WRITE, | |
| }, | |
| async ({ tripId, bagId, userIds }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| setBagMembers(tripId, bagId, userIds); | |
| safeBroadcast(tripId, 'packing:bag-members-updated', { bagId, userIds }); | |
| return ok({ success: true }); | |
| } | |
| ); | |
| if (R) server.registerTool( | |
| 'get_packing_category_assignees', | |
| { | |
| description: 'Get which trip members are assigned to each packing category.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_READONLY, | |
| }, | |
| async ({ tripId }) => { | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| const assignees = getPackingCategoryAssignees(tripId); | |
| return ok({ assignees }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'set_packing_category_assignees', | |
| { | |
| description: 'Assign trip members to a packing category.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| categoryName: z.string().min(1).max(100), | |
| userIds: z.array(z.number().int().positive()), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_WRITE, | |
| }, | |
| async ({ tripId, categoryName, userIds }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| updatePackingCategoryAssignees(tripId, categoryName, userIds); | |
| safeBroadcast(tripId, 'packing:assignees', { categoryName, userIds }); | |
| return ok({ success: true }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'apply_packing_template', | |
| { | |
| description: 'Apply a packing template to a trip (adds items from the template).', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| templateId: z.number().int().positive(), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, | |
| }, | |
| async ({ tripId, templateId }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| const applied = applyTemplate(tripId, templateId); | |
| if (applied === null) return { content: [{ type: 'text' as const, text: 'Template not found.' }], isError: true }; | |
| safeBroadcast(tripId, 'packing:template-applied', { templateId }); | |
| return ok({ success: true }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'save_packing_template', | |
| { | |
| description: 'Save the current packing list as a reusable template.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| templateName: z.string().min(1).max(100), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, | |
| }, | |
| async ({ tripId, templateName }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| saveAsTemplate(tripId, userId, templateName); | |
| return ok({ success: true }); | |
| } | |
| ); | |
| if (W) server.registerTool( | |
| 'bulk_import_packing', | |
| { | |
| description: 'Import multiple packing items at once from a list.', | |
| inputSchema: { | |
| tripId: z.number().int().positive(), | |
| items: z.array(z.object({ | |
| name: z.string().min(1).max(200), | |
| category: z.string().optional(), | |
| quantity: z.number().int().positive().optional(), | |
| })).min(1), | |
| }, | |
| annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT, | |
| }, | |
| async ({ tripId, items }) => { | |
| if (isDemoUser(userId)) return demoDenied(); | |
| if (!canAccessTrip(tripId, userId)) return noAccess(); | |
| if (!hasTripPermission('packing_edit', tripId, userId)) return permissionDenied(); | |
| bulkImport(tripId, items); | |
| safeBroadcast(tripId, 'packing:updated', {}); | |
| return ok({ success: true, count: items.length }); | |
| } | |
| ); | |
| } | |