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
+172 -4
View File
@@ -3,13 +3,18 @@ import { canAccessTrip } from '../db/database';
import { listTrips, getTrip, getTripOwner, listMembers } from '../services/tripService';
import { listDays, listAccommodations } from '../services/dayService';
import { listPlaces } from '../services/placeService';
import { listBudgetItems } from '../services/budgetService';
import { listItems as listPackingItems } from '../services/packingService';
import { listBudgetItems, getPerPersonSummary, calculateSettlement } from '../services/budgetService';
import { listItems as listPackingItems, listBags } from '../services/packingService';
import { listReservations } from '../services/reservationService';
import { listNotes as listDayNotes } from '../services/dayNoteService';
import { listNotes as listCollabNotes } from '../services/collabService';
import { listNotes as listCollabNotes, listPolls, listMessages } from '../services/collabService';
import { listItems as listTodoItems } from '../services/todoService';
import { listFiles } from '../services/fileService';
import { listCategories } from '../services/categoryService';
import { listBucketList, listVisitedCountries } from '../services/atlasService';
import { listBucketList, listVisitedCountries, getStats as getAtlasStats, listManuallyVisitedRegions } from '../services/atlasService';
import { getNotifications } from '../services/inAppNotifications';
import { getActivePlanId, getActivePlan, getPlanData, getEntries as getVacayEntries, getHolidays } from '../services/vacayService';
import { isAddonEnabled } from '../services/adminService';
function parseId(value: string | string[]): number | null {
const n = Number(Array.isArray(value) ? value[0] : value);
@@ -183,6 +188,32 @@ export function registerResources(server: McpServer, userId: number): void {
}
);
// Trip files (active, not trash)
server.registerResource(
'trip-files',
new ResourceTemplate('trek://trips/{tripId}/files', { list: undefined }),
{ description: 'Active files attached to a trip (excludes trash)', mimeType: 'application/json' },
async (uri, { tripId }) => {
const id = parseId(tripId);
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
const files = listFiles(id, false);
return jsonContent(uri.href, files);
}
);
// Trip to-do list
server.registerResource(
'trip-todos',
new ResourceTemplate('trek://trips/{tripId}/todos', { list: undefined }),
{ description: 'To-do items for a trip, ordered by position', mimeType: 'application/json' },
async (uri, { tripId }) => {
const id = parseId(tripId);
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
const items = listTodoItems(id);
return jsonContent(uri.href, items);
}
);
// All place categories (global, no trip filter)
server.registerResource(
'categories',
@@ -215,4 +246,141 @@ export function registerResources(server: McpServer, userId: number): void {
return jsonContent(uri.href, countries);
}
);
// Budget per-person summary
server.registerResource(
'trip-budget-per-person',
new ResourceTemplate('trek://trips/{tripId}/budget/per-person', { list: undefined }),
{ description: 'Per-person budget summary for a trip (total spent per member, split breakdown)', mimeType: 'application/json' },
async (uri, { tripId }) => {
const id = parseId(tripId);
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
const summary = getPerPersonSummary(id);
return jsonContent(uri.href, summary);
}
);
// Budget settlement
server.registerResource(
'trip-budget-settlement',
new ResourceTemplate('trek://trips/{tripId}/budget/settlement', { list: undefined }),
{ description: 'Suggested settlement transactions to balance who owes whom', mimeType: 'application/json' },
async (uri, { tripId }) => {
const id = parseId(tripId);
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
const settlement = calculateSettlement(id);
return jsonContent(uri.href, settlement);
}
);
// Packing bags
server.registerResource(
'trip-packing-bags',
new ResourceTemplate('trek://trips/{tripId}/packing/bags', { list: undefined }),
{ description: 'All packing bags for a trip with their members', mimeType: 'application/json' },
async (uri, { tripId }) => {
const id = parseId(tripId);
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
const bags = listBags(id);
return jsonContent(uri.href, bags);
}
);
// In-app notifications
server.registerResource(
'notifications-in-app',
'trek://notifications/in-app',
{ description: "The current user's in-app notifications (most recent 50, unread first)", mimeType: 'application/json' },
async (uri) => {
const result = getNotifications(userId, { limit: 50 });
return jsonContent(uri.href, result);
}
);
// Atlas stats and regions (addon-gated)
if (isAddonEnabled('atlas')) {
server.registerResource(
'atlas-stats',
'trek://atlas/stats',
{ description: "User's atlas statistics — visited country counts and breakdown", mimeType: 'application/json' },
async (uri) => {
const stats = await getAtlasStats(userId);
return jsonContent(uri.href, stats);
}
);
server.registerResource(
'atlas-regions',
'trek://atlas/regions',
{ description: 'List of manually visited regions for the current user', mimeType: 'application/json' },
async (uri) => {
const regions = listManuallyVisitedRegions(userId);
return jsonContent(uri.href, regions);
}
);
}
// Collab polls & messages (addon-gated)
if (isAddonEnabled('collab')) {
server.registerResource(
'trip-collab-polls',
new ResourceTemplate('trek://trips/{tripId}/collab/polls', { list: undefined }),
{ description: 'All polls for a trip with vote counts per option', mimeType: 'application/json' },
async (uri, { tripId }) => {
const id = parseId(tripId);
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
const polls = listPolls(id);
return jsonContent(uri.href, polls);
}
);
server.registerResource(
'trip-collab-messages',
new ResourceTemplate('trek://trips/{tripId}/collab/messages', { list: undefined }),
{ description: 'Most recent 100 chat messages for a trip', mimeType: 'application/json' },
async (uri, { tripId }) => {
const id = parseId(tripId);
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
const messages = listMessages(id);
return jsonContent(uri.href, messages);
}
);
}
// Vacay resources (addon-gated)
if (isAddonEnabled('vacay')) {
server.registerResource(
'vacay-plan',
'trek://vacay/plan',
{ description: "Full snapshot of the user's active vacation plan (members, years, settings)", mimeType: 'application/json' },
async (uri) => {
const plan = getPlanData(userId);
return jsonContent(uri.href, plan);
}
);
server.registerResource(
'vacay-entries',
new ResourceTemplate('trek://vacay/entries/{year}', { list: undefined }),
{ description: 'All vacation entries for the active plan and a specific year', mimeType: 'application/json' },
async (uri, { year }) => {
const planId = getActivePlanId(userId);
const entries = getVacayEntries(planId, Array.isArray(year) ? year[0] : year);
return jsonContent(uri.href, entries);
}
);
server.registerResource(
'vacay-holidays',
new ResourceTemplate('trek://vacay/holidays/{year}', { list: undefined }),
{ description: "Cached public holidays for the plan's configured region and year", mimeType: 'application/json' },
async (uri, { year }) => {
const plan = getActivePlan(userId);
if (!plan.holidays_enabled || !plan.holidays_region) return jsonContent(uri.href, []);
const yearStr = Array.isArray(year) ? year[0] : year;
const result = await getHolidays(yearStr, plan.holidays_region);
return jsonContent(uri.href, result.data ?? []);
}
);
}
}