refactor(mcp): extract all MCP tools into dedicated modules and add shared helpers

This commit is contained in:
jubnl
2026-04-09 18:09:08 +02:00
parent 91c9421b5e
commit 63784d86a3
36 changed files with 8154 additions and 1249 deletions
+131
View File
@@ -0,0 +1,131 @@
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';
export function registerBudgetTools(server: McpServer, userId: number): void {
// --- BUDGET ---
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 });
}
);
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) ---
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 ---
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 });
}
);
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 });
}
);
}