feat(mcp): introduce OAuth 2.1 auth and enforce addon gating

OAuth 2.1 authentication for MCP:
- Add OAuth 2.1 authorization server with PKCE support (routes/oauth.ts)
- Add OAuth service for client CRUD, auth-code flow, and token management (services/oauthService.ts)
- Add typed scope definitions and enforcement helpers (mcp/scopes.ts)
- Add OAuth consent UI page (OAuthAuthorizePage.tsx)
- Add client-side scope labels and descriptions (api/oauthScopes.ts)
- Integrate OAuth token auth into MCP handler alongside existing static tokens
- All OAuth endpoints gated on `mcp` addon

Addon gating across MCP tools, resources, and prompts:
- Add typed ADDON_IDS constant (server/src/addons.ts) replacing all string literals
- Gate budget tools and resources (trip-budget, per-person, settlement) on `budget` addon
- Gate packing tools and resources (trip-packing, trip-packing-bags, trip-todos) on `packing` addon
- Gate todos tools on `packing` addon (mirrors web UI Lists tab behavior)
- Expand atlas gate to cover full tool body (bucket-list + country tools no longer leak)
- Expand collab gate to cover full tool body (collab notes no longer leak)
- Gate packing-list and budget-overview MCP prompts on their respective addons
- Gate get_trip_summary sections per addon; blank packing/budget/collab_notes/todos when disabled
- Remove trip-files resource and files field from get_trip_summary
- Replace all isAddonEnabled('literal') calls with ADDON_IDS constants

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jubnl
2026-04-09 22:25:58 +02:00
parent 5c0d819fc1
commit 830f6c0706
32 changed files with 2589 additions and 669 deletions
+12 -8
View File
@@ -15,11 +15,15 @@ import {
TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, noAccess, ok,
} 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');
export function registerAssignmentTools(server: McpServer, userId: number): void {
// --- ASSIGNMENTS ---
server.registerTool(
if (W) server.registerTool(
'assign_place_to_day',
{
description: 'Assign a place to a specific day in a trip.',
@@ -42,7 +46,7 @@ export function registerAssignmentTools(server: McpServer, userId: number): void
}
);
server.registerTool(
if (W) server.registerTool(
'unassign_place',
{
description: 'Remove a place assignment from a day.',
@@ -64,7 +68,7 @@ export function registerAssignmentTools(server: McpServer, userId: number): void
}
);
server.registerTool(
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.',
@@ -91,7 +95,7 @@ export function registerAssignmentTools(server: McpServer, userId: number): void
}
);
server.registerTool(
if (W) server.registerTool(
'move_assignment',
{
description: 'Move a place assignment to a different day.',
@@ -113,7 +117,7 @@ export function registerAssignmentTools(server: McpServer, userId: number): void
}
);
server.registerTool(
if (R) server.registerTool(
'get_assignment_participants',
{
description: 'Get the list of users participating in a specific place assignment.',
@@ -130,7 +134,7 @@ export function registerAssignmentTools(server: McpServer, userId: number): void
}
);
server.registerTool(
if (W) server.registerTool(
'set_assignment_participants',
{
description: 'Set the participants for a place assignment (replaces current list).',
@@ -152,7 +156,7 @@ export function registerAssignmentTools(server: McpServer, userId: number): void
// --- REORDER ---
server.registerTool(
if (W) server.registerTool(
'reorder_day_assignments',
{
description: 'Reorder places within a day by providing the assignment IDs in the desired order.',
+18 -13
View File
@@ -7,16 +7,23 @@ import {
markRegionVisited, unmarkRegionVisited, getCountryPlaces, updateBucketItem,
} from '../../services/atlasService';
import { isAddonEnabled } from '../../services/adminService';
import { ADDON_IDS } from '../../addons';
import {
TOOL_ANNOTATIONS_WRITE, TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
TOOL_ANNOTATIONS_READONLY,
demoDenied, ok,
} from './_shared';
import { canRead, canWrite } from '../scopes';
export function registerAtlasTools(server: McpServer, userId: number, scopes: string[] | null): void {
const R = canRead(scopes, 'places');
const W = canWrite(scopes, 'places');
if (!isAddonEnabled(ADDON_IDS.ATLAS)) return;
export function registerAtlasTools(server: McpServer, userId: number): void {
// --- BUCKET LIST ---
server.registerTool(
if (W) server.registerTool(
'create_bucket_list_item',
{
description: 'Add a destination to your personal travel bucket list.',
@@ -36,7 +43,7 @@ export function registerAtlasTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_bucket_list_item',
{
description: 'Remove an item from your travel bucket list.',
@@ -55,7 +62,7 @@ export function registerAtlasTools(server: McpServer, userId: number): void {
// --- ATLAS ---
server.registerTool(
if (W) server.registerTool(
'mark_country_visited',
{
description: 'Mark a country as visited in your Atlas.',
@@ -71,7 +78,7 @@ export function registerAtlasTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'unmark_country_visited',
{
description: 'Remove a country from your visited countries in Atlas.',
@@ -89,8 +96,7 @@ export function registerAtlasTools(server: McpServer, userId: number): void {
// --- ATLAS EXPANDED ---
if (isAddonEnabled('atlas')) {
server.registerTool(
if (R) server.registerTool(
'get_atlas_stats',
{
description: 'Get atlas statistics — total visited countries, region counts, continent breakdown.',
@@ -103,7 +109,7 @@ export function registerAtlasTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'list_visited_regions',
{
description: 'List all manually visited sub-country regions for the current user.',
@@ -116,7 +122,7 @@ export function registerAtlasTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'mark_region_visited',
{
description: 'Mark a sub-country region as visited.',
@@ -135,7 +141,7 @@ export function registerAtlasTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'unmark_region_visited',
{
description: 'Remove a region from the visited list.',
@@ -151,7 +157,7 @@ export function registerAtlasTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'get_country_atlas_places',
{
description: 'Get places saved in the user\'s atlas for a specific country.',
@@ -166,7 +172,7 @@ export function registerAtlasTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'update_bucket_list_item',
{
description: 'Update a bucket list item (notes, name, target date, location).',
@@ -188,5 +194,4 @@ export function registerAtlasTools(server: McpServer, userId: number): void {
return ok({ item });
}
);
}
}
+13 -6
View File
@@ -12,11 +12,17 @@ import {
TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, noAccess, ok,
} from './_shared';
import { canWrite } from '../scopes';
import { isAddonEnabled } from '../../services/adminService';
import { ADDON_IDS } from '../../addons';
export function registerBudgetTools(server: McpServer, userId: number): void {
export function registerBudgetTools(server: McpServer, userId: number, scopes: string[] | null): void {
const W = canWrite(scopes, 'budget');
if (isAddonEnabled(ADDON_IDS.BUDGET)) {
// --- BUDGET ---
server.registerTool(
if (W) server.registerTool(
'create_budget_item',
{
description: 'Add a budget/expense item to a trip.',
@@ -38,7 +44,7 @@ export function registerBudgetTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_budget_item',
{
description: 'Delete a budget item from a trip.',
@@ -60,7 +66,7 @@ export function registerBudgetTools(server: McpServer, userId: number): void {
// --- BUDGET (update) ---
server.registerTool(
if (W) server.registerTool(
'update_budget_item',
{
description: 'Update an existing budget/expense item in a trip.',
@@ -88,7 +94,7 @@ export function registerBudgetTools(server: McpServer, userId: number): void {
// --- BUDGET ADVANCED ---
server.registerTool(
if (W) server.registerTool(
'set_budget_item_members',
{
description: 'Set which trip members are splitting a budget item (replaces current member list).',
@@ -108,7 +114,7 @@ export function registerBudgetTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'toggle_budget_member_paid',
{
description: 'Mark or unmark a member as having paid their share of a budget item.',
@@ -128,4 +134,5 @@ export function registerBudgetTools(server: McpServer, userId: number): void {
return ok({ member });
}
);
} // isAddonEnabled(BUDGET)
}
+21 -16
View File
@@ -8,16 +8,23 @@ import {
listMessages, createMessage, deleteMessage, addOrRemoveReaction,
} from '../../services/collabService';
import { isAddonEnabled } from '../../services/adminService';
import { ADDON_IDS } from '../../addons';
import {
safeBroadcast, TOOL_ANNOTATIONS_WRITE, TOOL_ANNOTATIONS_DELETE,
TOOL_ANNOTATIONS_NON_IDEMPOTENT, TOOL_ANNOTATIONS_READONLY,
demoDenied, noAccess, ok,
} from './_shared';
import { canRead, canWrite } from '../scopes';
export function registerCollabTools(server: McpServer, userId: number, scopes: string[] | null): void {
const R = canRead(scopes, 'collab');
const W = canWrite(scopes, 'collab');
if (!isAddonEnabled(ADDON_IDS.COLLAB)) return;
export function registerCollabTools(server: McpServer, userId: number): void {
// --- COLLAB NOTES ---
server.registerTool(
if (W) server.registerTool(
'create_collab_note',
{
description: 'Create a shared collaborative note on a trip (visible to all trip members in the Collab tab).',
@@ -40,7 +47,7 @@ export function registerCollabTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'update_collab_note',
{
description: 'Edit an existing collaborative note on a trip.',
@@ -65,7 +72,7 @@ export function registerCollabTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_collab_note',
{
description: 'Delete a collaborative note from a trip.',
@@ -87,9 +94,8 @@ export function registerCollabTools(server: McpServer, userId: number): void {
// --- COLLAB POLLS & CHAT ---
if (isAddonEnabled('collab')) {
server.registerTool(
'list_collab_polls',
if (R) server.registerTool(
'list_collab_polls',
{
description: 'List all polls for a trip.',
inputSchema: {
@@ -104,7 +110,7 @@ export function registerCollabTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'create_collab_poll',
{
description: 'Create a new poll in the collab panel.',
@@ -126,7 +132,7 @@ export function registerCollabTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'vote_collab_poll',
{
description: 'Vote on a poll option (or remove vote if already voted for that option).',
@@ -146,7 +152,7 @@ export function registerCollabTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'close_collab_poll',
{
description: 'Close a poll so no more votes can be cast.',
@@ -166,7 +172,7 @@ export function registerCollabTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_collab_poll',
{
description: 'Delete a poll and all its votes.',
@@ -186,7 +192,7 @@ export function registerCollabTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'list_collab_messages',
{
description: 'List chat messages for a trip (most recent 100, oldest-first).',
@@ -203,7 +209,7 @@ export function registerCollabTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'send_collab_message',
{
description: "Send a chat message to a trip's collab channel.",
@@ -224,7 +230,7 @@ export function registerCollabTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_collab_message',
{
description: 'Delete a chat message (only the message owner can delete their own messages).',
@@ -244,7 +250,7 @@ export function registerCollabTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'react_collab_message',
{
description: 'Toggle a reaction emoji on a chat message (adds if not present, removes if already reacted).',
@@ -264,5 +270,4 @@ export function registerCollabTools(server: McpServer, userId: number): void {
return ok({ reactions: result.reactions });
}
);
}
}
+4 -1
View File
@@ -16,8 +16,11 @@ import {
TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, noAccess, ok,
} from './_shared';
import { canWrite } from '../scopes';
export function registerDayTools(server: McpServer, userId: number, scopes: string[] | null): void {
if (!canWrite(scopes, 'trips')) return;
export function registerDayTools(server: McpServer, userId: number): void {
// --- DAYS ---
server.registerTool(
+4 -1
View File
@@ -6,8 +6,11 @@ import {
TOOL_ANNOTATIONS_READONLY,
ok,
} from './_shared';
import { canRead } from '../scopes';
export function registerMapsWeatherTools(server: McpServer, userId: number, scopes: string[] | null): void {
if (!canRead(scopes, 'media')) return;
export function registerMapsWeatherTools(server: McpServer, userId: number): void {
// --- MAPS EXTRAS ---
server.registerTool(
+10 -6
View File
@@ -11,11 +11,15 @@ import {
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, ok,
} from './_shared';
import { canRead, canWrite } from '../scopes';
export function registerNotificationTools(server: McpServer, userId: number, scopes: string[] | null): void {
const R = canRead(scopes, 'notifications');
const W = canWrite(scopes, 'notifications');
export function registerNotificationTools(server: McpServer, userId: number): void {
// --- NOTIFICATIONS ---
server.registerTool(
if (R) server.registerTool(
'list_notifications',
{
description: 'List in-app notifications for the current user.',
@@ -32,7 +36,7 @@ export function registerNotificationTools(server: McpServer, userId: number): vo
}
);
server.registerTool(
if (R) server.registerTool(
'get_unread_notification_count',
{
description: 'Get the number of unread in-app notifications.',
@@ -45,7 +49,7 @@ export function registerNotificationTools(server: McpServer, userId: number): vo
}
);
server.registerTool(
if (W) server.registerTool(
'mark_notification_read',
{
description: 'Mark a single notification as read.',
@@ -62,7 +66,7 @@ export function registerNotificationTools(server: McpServer, userId: number): vo
}
);
server.registerTool(
if (W) server.registerTool(
'mark_notification_unread',
{
description: 'Mark a single notification as unread.',
@@ -79,7 +83,7 @@ export function registerNotificationTools(server: McpServer, userId: number): vo
}
);
server.registerTool(
if (W) server.registerTool(
'mark_all_notifications_read',
{
description: "Mark all of the current user's notifications as read.",
+24 -16
View File
@@ -16,11 +16,19 @@ import {
TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, noAccess, ok,
} 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;
export function registerPackingTools(server: McpServer, userId: number): void {
// --- PACKING ---
server.registerTool(
if (W) server.registerTool(
'create_packing_item',
{
description: 'Add an item to the packing checklist for a trip.',
@@ -40,7 +48,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'toggle_packing_item',
{
description: 'Check or uncheck a packing item.',
@@ -61,7 +69,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_packing_item',
{
description: 'Remove an item from the packing checklist.',
@@ -83,7 +91,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
// --- PACKING (update) ---
server.registerTool(
if (W) server.registerTool(
'update_packing_item',
{
description: 'Rename a packing item or change its category.',
@@ -108,7 +116,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
// --- PACKING ADVANCED ---
server.registerTool(
if (W) server.registerTool(
'reorder_packing_items',
{
description: 'Set the display order of packing items within a trip.',
@@ -127,7 +135,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'list_packing_bags',
{
description: 'List all packing bags for a trip.',
@@ -143,7 +151,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'create_packing_bag',
{
description: 'Create a new packing bag (e.g. "Carry-on", "Checked bag").',
@@ -163,7 +171,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'update_packing_bag',
{
description: 'Rename or recolor a packing bag.',
@@ -188,7 +196,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_packing_bag',
{
description: 'Delete a packing bag (items in the bag are unassigned, not deleted).',
@@ -207,7 +215,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'set_bag_members',
{
description: 'Assign trip members to a packing bag (determines who packs what bag).',
@@ -227,7 +235,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'get_packing_category_assignees',
{
description: 'Get which trip members are assigned to each packing category.',
@@ -243,7 +251,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'set_packing_category_assignees',
{
description: 'Assign trip members to a packing category.',
@@ -263,7 +271,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'apply_packing_template',
{
description: 'Apply a packing template to a trip (adds items from the template).',
@@ -283,7 +291,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'save_packing_template',
{
description: 'Save the current packing list as a reusable template.',
@@ -301,7 +309,7 @@ export function registerPackingTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'bulk_import_packing',
{
description: 'Import multiple packing items at once from a list.',
+11 -7
View File
@@ -10,11 +10,15 @@ import {
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, noAccess, ok,
} from './_shared';
import { canRead, canWrite } from '../scopes';
export function registerPlaceTools(server: McpServer, userId: number, scopes: string[] | null): void {
const R = canRead(scopes, 'places');
const W = canWrite(scopes, 'places');
export function registerPlaceTools(server: McpServer, userId: number): void {
// --- PLACES ---
server.registerTool(
if (W) server.registerTool(
'create_place',
{
description: 'Add a new place/POI to a trip. Set google_place_id or osm_id (from search_place) so the app can show opening hours and ratings.',
@@ -43,7 +47,7 @@ export function registerPlaceTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'update_place',
{
description: 'Update an existing place in a trip.',
@@ -80,7 +84,7 @@ export function registerPlaceTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_place',
{
description: 'Delete a place from a trip.',
@@ -100,7 +104,7 @@ export function registerPlaceTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'list_places',
{
description: 'List all places/POIs in a trip, optionally filtered by assignment status. Use assignment=unassigned to find orphan activities not yet scheduled on any day.',
@@ -122,7 +126,7 @@ export function registerPlaceTools(server: McpServer, userId: number): void {
// --- CATEGORIES ---
server.registerTool(
if (R) server.registerTool(
'list_categories',
{
description: 'List all available place categories with their id, name, icon and color. Use category_id when creating or updating places.',
@@ -137,7 +141,7 @@ export function registerPlaceTools(server: McpServer, userId: number): void {
// --- SEARCH ---
server.registerTool(
if (R) server.registerTool(
'search_place',
{
description: 'Search for a real-world place by name or address. Returns results with osm_id (and google_place_id if configured). Use these IDs when calling create_place so the app can display opening hours and ratings.',
+25 -3
View File
@@ -3,8 +3,30 @@ import { z } from 'zod';
import { canAccessTrip } from '../../db/database';
import { getTripSummary } from '../../services/tripService';
import { listItems as listPackingItems } from '../../services/packingService';
import { isAddonEnabled } from '../../services/adminService';
import { ADDON_IDS } from '../../addons';
export function registerMcpPrompts(server: McpServer, _userId: number): void {
export function registerMcpPrompts(server: McpServer, _userId: number, isStaticToken = false): void {
if (isStaticToken) {
server.registerPrompt(
'token_auth_notice',
{
title: 'Static Token Auth Notice',
description: 'Notification that this session uses a static API token which will be deprecated',
argsSchema: {},
},
async () => ({
description: 'Static token deprecation notice',
messages: [{
role: 'user' as const,
content: {
type: 'text' as const,
text: '⚠️ This MCP connection is authenticated with a static API token (trek_…). Static token authentication will be deprecated in a future version of TREK. Please inform the user that they should migrate to OAuth 2.1 by going to Settings → Integrations → MCP → OAuth Clients in TREK and registering an OAuth 2.1 application for their MCP client.',
},
}],
})
);
}
const userId = _userId;
server.registerPrompt(
@@ -43,7 +65,7 @@ ${days?.map((d: any, i: number) => `Day ${i + 1} (${d.date}): ${d.assignments?.l
}
);
server.registerPrompt(
if (isAddonEnabled(ADDON_IDS.PACKING)) server.registerPrompt(
'packing-list',
{
title: 'Packing List',
@@ -77,7 +99,7 @@ ${days?.map((d: any, i: number) => `Day ${i + 1} (${d.date}): ${d.assignments?.l
}
);
server.registerPrompt(
if (isAddonEnabled(ADDON_IDS.BUDGET)) server.registerPrompt(
'budget-overview',
{
title: 'Budget Overview',
+4 -1
View File
@@ -13,8 +13,11 @@ import {
TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, noAccess, ok,
} from './_shared';
import { canWrite } from '../scopes';
export function registerReservationTools(server: McpServer, userId: number, scopes: string[] | null): void {
if (!canWrite(scopes, 'reservations')) return;
export function registerReservationTools(server: McpServer, userId: number): void {
server.registerTool(
'create_reservation',
+9 -5
View File
@@ -7,11 +7,15 @@ import {
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, ok,
} from './_shared';
import { canRead, canWrite } from '../scopes';
export function registerTagTools(server: McpServer, userId: number, scopes: string[] | null): void {
const R = canRead(scopes, 'places');
const W = canWrite(scopes, 'places');
export function registerTagTools(server: McpServer, userId: number): void {
// --- TAGS ---
server.registerTool(
if (R) server.registerTool(
'list_tags',
{
description: 'List all tags belonging to the current user.',
@@ -24,7 +28,7 @@ export function registerTagTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'create_tag',
{
description: 'Create a new tag (user-scoped label for places).',
@@ -41,7 +45,7 @@ export function registerTagTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'update_tag',
{
description: 'Update the name or color of an existing tag.',
@@ -60,7 +64,7 @@ export function registerTagTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_tag',
{
description: 'Delete a tag (removes it from all places it was attached to).',
+17 -9
View File
@@ -12,11 +12,19 @@ import {
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, noAccess, ok,
} from './_shared';
import { canRead, canWrite } from '../scopes';
import { isAddonEnabled } from '../../services/adminService';
import { ADDON_IDS } from '../../addons';
export function registerTodoTools(server: McpServer, userId: number, scopes: string[] | null): void {
const R = canRead(scopes, 'collab');
const W = canWrite(scopes, 'collab');
if (!isAddonEnabled(ADDON_IDS.PACKING)) return;
export function registerTodoTools(server: McpServer, userId: number): void {
// --- TODOS ---
server.registerTool(
if (R) server.registerTool(
'list_todos',
{
description: 'List all to-do items for a trip, ordered by position.',
@@ -32,7 +40,7 @@ export function registerTodoTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'create_todo',
{
description: 'Create a new to-do item for a trip.',
@@ -56,7 +64,7 @@ export function registerTodoTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'update_todo',
{
description: 'Update an existing to-do item. Only provided fields are changed; omitted fields stay as-is. Pass null to clear a nullable field.',
@@ -88,7 +96,7 @@ export function registerTodoTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'toggle_todo',
{
description: 'Mark a to-do item as checked (done) or unchecked.',
@@ -109,7 +117,7 @@ export function registerTodoTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_todo',
{
description: 'Delete a to-do item.',
@@ -129,7 +137,7 @@ export function registerTodoTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'reorder_todos',
{
description: 'Reorder to-do items within a trip by providing a new ordered list of item IDs.',
@@ -147,7 +155,7 @@ export function registerTodoTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'get_todo_category_assignees',
{
description: 'Get the default assignees configured per to-do category for a trip.',
@@ -163,7 +171,7 @@ export function registerTodoTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'set_todo_category_assignees',
{
description: 'Set the default assignees for a to-do category on a trip. Pass an empty array to clear.',
+36 -31
View File
@@ -13,22 +13,27 @@ import {
createOrUpdateShareLink, getShareLink, deleteShareLink,
} from '../../services/shareService';
import { isAddonEnabled } from '../../services/adminService';
import { ADDON_IDS } from '../../addons';
import { countMessages, listPolls } from '../../services/collabService';
import {
listItems as listTodoItems,
} from '../../services/todoService';
import { listFiles } from '../../services/fileService';
import {
safeBroadcast, MAX_MCP_TRIP_DAYS,
TOOL_ANNOTATIONS_READONLY, TOOL_ANNOTATIONS_WRITE,
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, noAccess, ok,
} from './_shared';
import { canReadTrips, canWrite, canDeleteTrips } from '../scopes';
export function registerTripTools(server: McpServer, userId: number, scopes: string[] | null): void {
const R = canReadTrips(scopes);
const W = canWrite(scopes, 'trips');
const D = canDeleteTrips(scopes);
export function registerTripTools(server: McpServer, userId: number): void {
// --- TRIPS ---
server.registerTool(
if (W) server.registerTool(
'create_trip',
{
description: 'Create a new trip. Returns the created trip with its generated days.',
@@ -61,7 +66,7 @@ export function registerTripTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'update_trip',
{
description: 'Update an existing trip\'s details.',
@@ -94,7 +99,7 @@ export function registerTripTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (D) server.registerTool(
'delete_trip',
{
description: 'Delete a trip. Only the trip owner can delete it.',
@@ -111,7 +116,7 @@ export function registerTripTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'list_trips',
{
description: 'List all trips the current user owns or is a member of. Use this for trip discovery before calling get_trip_summary.',
@@ -128,10 +133,10 @@ export function registerTripTools(server: McpServer, userId: number): void {
// --- TRIP SUMMARY ---
server.registerTool(
if (R) server.registerTool(
'get_trip_summary',
{
description: 'Get a full denormalized summary of a trip in a single call: metadata, members, days with assignments and notes, accommodations, full budget line items with totals, full packing list with checked status, reservations, collab notes, to-do items, files, and collab poll/message counts. Use this as a context loader before planning or modifying a trip.',
description: 'Get a full denormalized summary of a trip in a single call: metadata, members, days with assignments and notes, accommodations, budget line items (when enabled), packing list (when enabled), reservations, collab notes and poll/message counts (when enabled), and to-do items (when enabled). Use this as a context loader before planning or modifying a trip.',
inputSchema: {
tripId: z.number().int().positive(),
},
@@ -141,31 +146,31 @@ export function registerTripTools(server: McpServer, userId: number): void {
if (!canAccessTrip(tripId, userId)) return noAccess();
const summary = getTripSummary(tripId);
if (!summary) return noAccess();
const todos = listTodoItems(tripId);
const files = listFiles(tripId, false).map((f: any) => ({
id: f.id,
original_name: f.original_name,
mime_type: f.mime_type,
file_size: f.file_size,
starred: !!f.starred,
deleted: !!f.deleted_at,
created_at: f.created_at,
}));
const packingEnabled = isAddonEnabled(ADDON_IDS.PACKING);
const budgetEnabled = isAddonEnabled(ADDON_IDS.BUDGET);
const collabEnabled = isAddonEnabled(ADDON_IDS.COLLAB);
const todos = packingEnabled ? listTodoItems(tripId) : [];
let pollCount = 0;
if (isAddonEnabled('collab')) {
pollCount = listPolls(tripId).length;
}
let messageCount = 0;
if (isAddonEnabled('collab')) {
if (collabEnabled) {
pollCount = listPolls(tripId).length;
messageCount = countMessages(tripId);
}
return ok({ ...summary, todos, files, pollCount, messageCount });
return ok({
...summary,
packing: packingEnabled ? summary.packing : undefined,
budget: budgetEnabled ? summary.budget : undefined,
collab_notes: collabEnabled ? summary.collab_notes : [],
todos,
pollCount,
messageCount,
});
}
);
// --- TRIP MEMBERS, COPY, ICS, SHARE ---
server.registerTool(
if (R) server.registerTool(
'list_trip_members',
{
description: 'List all members of a trip (owner + collaborators).',
@@ -183,7 +188,7 @@ export function registerTripTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'add_trip_member',
{
description: 'Add a user to a trip by their username or email address. Only the trip owner can do this.',
@@ -210,7 +215,7 @@ export function registerTripTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'remove_trip_member',
{
description: 'Remove a member from a trip. Only the trip owner can do this.',
@@ -232,7 +237,7 @@ export function registerTripTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'copy_trip',
{
description: 'Duplicate a trip (all days, places, itinerary, packing, budget, reservations, day notes). Packing items are reset to unchecked. Returns the new trip.',
@@ -255,7 +260,7 @@ export function registerTripTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'export_trip_ics',
{
description: 'Export a trip\'s itinerary and reservations as iCalendar (.ics) format text. Useful for importing into calendar apps.',
@@ -275,7 +280,7 @@ export function registerTripTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'get_share_link',
{
description: 'Get the current public share link for a trip, including its permission flags. Returns null if no share link exists.',
@@ -291,7 +296,7 @@ export function registerTripTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'create_share_link',
{
description: 'Create or update the public share link for a trip. Set permission flags to control what is visible to guests.',
@@ -319,7 +324,7 @@ export function registerTripTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_share_link',
{
description: 'Revoke the public share link for a trip. Guests will no longer be able to access the shared view.',
+29 -24
View File
@@ -13,15 +13,20 @@ import {
getCountries as getHolidayCountries, getHolidays,
} from '../../services/vacayService';
import { isAddonEnabled } from '../../services/adminService';
import { ADDON_IDS } from '../../addons';
import {
TOOL_ANNOTATIONS_READONLY, TOOL_ANNOTATIONS_WRITE,
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
demoDenied, ok,
} from './_shared';
import { canRead, canWrite } from '../scopes';
export function registerVacayTools(server: McpServer, userId: number): void {
if (isAddonEnabled('vacay')) {
server.registerTool(
export function registerVacayTools(server: McpServer, userId: number, scopes: string[] | null): void {
const R = canRead(scopes, 'vacay');
const W = canWrite(scopes, 'vacay');
if (isAddonEnabled(ADDON_IDS.VACAY)) {
if (R) server.registerTool(
'get_vacay_plan',
{
description: "Get the current user's active vacation plan (own or joined).",
@@ -34,7 +39,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'update_vacay_plan',
{
description: 'Update vacation plan settings (weekends blocking, holidays, carry-over).',
@@ -55,7 +60,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'set_vacay_color',
{
description: "Set the current user's color in the vacation plan calendar.",
@@ -72,7 +77,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'get_available_vacay_users',
{
description: 'List users who can be invited to the current vacation plan.',
@@ -86,7 +91,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'send_vacay_invite',
{
description: 'Invite a user to join the vacation plan by their user ID.',
@@ -106,7 +111,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'accept_vacay_invite',
{
description: 'Accept a pending invitation to join another user\'s vacation plan.',
@@ -123,7 +128,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'decline_vacay_invite',
{
description: 'Decline a pending vacation plan invitation.',
@@ -138,7 +143,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'cancel_vacay_invite',
{
description: 'Cancel an outgoing invitation (owner cancels invite they sent).',
@@ -155,7 +160,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'dissolve_vacay_plan',
{
description: 'Dissolve the shared plan — all members are removed and everyone returns to their own individual plan.',
@@ -169,7 +174,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'list_vacay_years',
{
description: 'List calendar years tracked in the current vacation plan.',
@@ -183,7 +188,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'add_vacay_year',
{
description: 'Add a calendar year to the vacation plan.',
@@ -200,7 +205,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_vacay_year',
{
description: 'Remove a calendar year from the vacation plan.',
@@ -217,7 +222,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'get_vacay_entries',
{
description: 'Get all vacation day entries for a plan and year.',
@@ -233,7 +238,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'toggle_vacay_entry',
{
description: 'Toggle a day on or off as a vacation day for the current user.',
@@ -250,7 +255,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'toggle_company_holiday',
{
description: 'Toggle a date as a company holiday for the whole plan.',
@@ -268,7 +273,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'get_vacay_stats',
{
description: 'Get vacation statistics for a specific year (days used, remaining, carried over).',
@@ -284,7 +289,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'update_vacay_stats',
{
description: 'Update the vacation day allowance for a specific user and year.',
@@ -302,7 +307,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'add_holiday_calendar',
{
description: 'Add a public holiday calendar (by region code) to the vacation plan.',
@@ -322,7 +327,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'update_holiday_calendar',
{
description: 'Update label or color for a holiday calendar.',
@@ -342,7 +347,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (W) server.registerTool(
'delete_holiday_calendar',
{
description: 'Remove a holiday calendar from the vacation plan.',
@@ -359,7 +364,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'list_holiday_countries',
{
description: 'List countries available for public holiday calendars.',
@@ -373,7 +378,7 @@ export function registerVacayTools(server: McpServer, userId: number): void {
}
);
server.registerTool(
if (R) server.registerTool(
'list_holidays',
{
description: 'List public holidays for a country and year.',