mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
830f6c0706
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>
139 lines
5.2 KiB
TypeScript
139 lines
5.2 KiB
TypeScript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
|
import { z } from 'zod';
|
|
import { canAccessTrip } from '../../db/database';
|
|
import { isDemoUser } from '../../services/authService';
|
|
import {
|
|
createBudgetItem, updateBudgetItem, deleteBudgetItem,
|
|
updateMembers as updateBudgetMembers,
|
|
toggleMemberPaid,
|
|
} from '../../services/budgetService';
|
|
import {
|
|
safeBroadcast, TOOL_ANNOTATIONS_WRITE, TOOL_ANNOTATIONS_DELETE,
|
|
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, scopes: string[] | null): void {
|
|
const W = canWrite(scopes, 'budget');
|
|
|
|
if (isAddonEnabled(ADDON_IDS.BUDGET)) {
|
|
// --- BUDGET ---
|
|
|
|
if (W) server.registerTool(
|
|
'create_budget_item',
|
|
{
|
|
description: 'Add a budget/expense item to a trip.',
|
|
inputSchema: {
|
|
tripId: z.number().int().positive(),
|
|
name: z.string().min(1).max(200),
|
|
category: z.string().max(100).optional().describe('Budget category (e.g. Accommodation, Food, Transport)'),
|
|
total_price: z.number().nonnegative(),
|
|
note: z.string().max(500).optional(),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
|
},
|
|
async ({ tripId, name, category, total_price, note }) => {
|
|
if (isDemoUser(userId)) return demoDenied();
|
|
if (!canAccessTrip(tripId, userId)) return noAccess();
|
|
const item = createBudgetItem(tripId, { category, name, total_price, note });
|
|
safeBroadcast(tripId, 'budget:created', { item });
|
|
return ok({ item });
|
|
}
|
|
);
|
|
|
|
if (W) server.registerTool(
|
|
'delete_budget_item',
|
|
{
|
|
description: 'Delete a budget item from a trip.',
|
|
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();
|
|
const deleted = deleteBudgetItem(itemId, tripId);
|
|
if (!deleted) return { content: [{ type: 'text' as const, text: 'Budget item not found.' }], isError: true };
|
|
safeBroadcast(tripId, 'budget:deleted', { itemId });
|
|
return ok({ success: true });
|
|
}
|
|
);
|
|
|
|
// --- BUDGET (update) ---
|
|
|
|
if (W) server.registerTool(
|
|
'update_budget_item',
|
|
{
|
|
description: 'Update an existing budget/expense item in a trip.',
|
|
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(),
|
|
total_price: z.number().nonnegative().optional(),
|
|
persons: z.number().int().positive().nullable().optional(),
|
|
days: z.number().int().positive().nullable().optional(),
|
|
note: z.string().max(500).nullable().optional(),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_WRITE,
|
|
},
|
|
async ({ tripId, itemId, name, category, total_price, persons, days, note }) => {
|
|
if (isDemoUser(userId)) return demoDenied();
|
|
if (!canAccessTrip(tripId, userId)) return noAccess();
|
|
const item = updateBudgetItem(itemId, tripId, { name, category, total_price, persons, days, note });
|
|
if (!item) return { content: [{ type: 'text' as const, text: 'Budget item not found.' }], isError: true };
|
|
safeBroadcast(tripId, 'budget:updated', { item });
|
|
return ok({ item });
|
|
}
|
|
);
|
|
|
|
// --- BUDGET ADVANCED ---
|
|
|
|
if (W) server.registerTool(
|
|
'set_budget_item_members',
|
|
{
|
|
description: 'Set which trip members are splitting a budget item (replaces current member list).',
|
|
inputSchema: {
|
|
tripId: z.number().int().positive(),
|
|
itemId: z.number().int().positive(),
|
|
userIds: z.array(z.number().int().positive()).describe('User IDs splitting this item; empty array clears all'),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_WRITE,
|
|
},
|
|
async ({ tripId, itemId, userIds }) => {
|
|
if (isDemoUser(userId)) return demoDenied();
|
|
if (!canAccessTrip(tripId, userId)) return noAccess();
|
|
const item = updateBudgetMembers(itemId, tripId, userIds);
|
|
safeBroadcast(tripId, 'budget:members-updated', { item });
|
|
return ok({ item });
|
|
}
|
|
);
|
|
|
|
if (W) server.registerTool(
|
|
'toggle_budget_member_paid',
|
|
{
|
|
description: 'Mark or unmark a member as having paid their share of a budget item.',
|
|
inputSchema: {
|
|
tripId: z.number().int().positive(),
|
|
itemId: z.number().int().positive(),
|
|
memberId: z.number().int().positive().describe('User ID of the member'),
|
|
paid: z.boolean(),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_WRITE,
|
|
},
|
|
async ({ tripId, itemId, memberId, paid }) => {
|
|
if (isDemoUser(userId)) return demoDenied();
|
|
if (!canAccessTrip(tripId, userId)) return noAccess();
|
|
const member = toggleMemberPaid(itemId, memberId, paid);
|
|
safeBroadcast(tripId, 'budget:member-paid-updated', { itemId, member });
|
|
return ok({ member });
|
|
}
|
|
);
|
|
} // isAddonEnabled(BUDGET)
|
|
}
|