mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
1bddb3c588
Replace all db.prepare() calls in mcp/index.ts, mcp/resources.ts, and mcp/tools.ts with calls to the service layer. Add missing service functions: - authService: isDemoUser, verifyMcpToken, verifyJwtToken - adminService: isAddonEnabled - atlasService: listVisitedCountries - tripService: getTripSummary, listTrips with null archived param Also fix getAssignmentWithPlace and formatAssignmentWithPlace to expose place_id, assignment_time, and assignment_end_time at the top level, and fix updateDay to correctly handle null title for clearing. Add comprehensive unit and integration test suite for the MCP layer (821 tests all passing).
218 lines
7.5 KiB
TypeScript
218 lines
7.5 KiB
TypeScript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp';
|
|
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 { listReservations } from '../services/reservationService';
|
|
import { listNotes as listDayNotes } from '../services/dayNoteService';
|
|
import { listNotes as listCollabNotes } from '../services/collabService';
|
|
import { listCategories } from '../services/categoryService';
|
|
import { listBucketList, listVisitedCountries } from '../services/atlasService';
|
|
|
|
function parseId(value: string | string[]): number | null {
|
|
const n = Number(Array.isArray(value) ? value[0] : value);
|
|
return Number.isInteger(n) && n > 0 ? n : null;
|
|
}
|
|
|
|
function accessDenied(uri: string) {
|
|
return {
|
|
contents: [{
|
|
uri,
|
|
mimeType: 'application/json',
|
|
text: JSON.stringify({ error: 'Trip not found or access denied' }),
|
|
}],
|
|
};
|
|
}
|
|
|
|
function jsonContent(uri: string, data: unknown) {
|
|
return {
|
|
contents: [{
|
|
uri,
|
|
mimeType: 'application/json',
|
|
text: JSON.stringify(data, null, 2),
|
|
}],
|
|
};
|
|
}
|
|
|
|
export function registerResources(server: McpServer, userId: number): void {
|
|
// List all accessible trips
|
|
server.registerResource(
|
|
'trips',
|
|
'trek://trips',
|
|
{ description: 'All trips the user owns or is a member of' },
|
|
async (uri) => {
|
|
const trips = listTrips(userId, 0);
|
|
return jsonContent(uri.href, trips);
|
|
}
|
|
);
|
|
|
|
// Single trip detail
|
|
server.registerResource(
|
|
'trip',
|
|
new ResourceTemplate('trek://trips/{tripId}', { list: undefined }),
|
|
{ description: 'A single trip with metadata and member count' },
|
|
async (uri, { tripId }) => {
|
|
const id = parseId(tripId);
|
|
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
|
|
const trip = getTrip(id, userId);
|
|
return jsonContent(uri.href, trip);
|
|
}
|
|
);
|
|
|
|
// Days with assigned places
|
|
server.registerResource(
|
|
'trip-days',
|
|
new ResourceTemplate('trek://trips/{tripId}/days', { list: undefined }),
|
|
{ description: 'Days of a trip with their assigned places' },
|
|
async (uri, { tripId }) => {
|
|
const id = parseId(tripId);
|
|
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
|
|
|
|
const { days } = listDays(id);
|
|
return jsonContent(uri.href, days);
|
|
}
|
|
);
|
|
|
|
// Places in a trip
|
|
server.registerResource(
|
|
'trip-places',
|
|
new ResourceTemplate('trek://trips/{tripId}/places', { list: undefined }),
|
|
{ description: 'All places/POIs saved in a trip' },
|
|
async (uri, { tripId }) => {
|
|
const id = parseId(tripId);
|
|
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
|
|
const places = listPlaces(String(id), {});
|
|
return jsonContent(uri.href, places);
|
|
}
|
|
);
|
|
|
|
// Budget items
|
|
server.registerResource(
|
|
'trip-budget',
|
|
new ResourceTemplate('trek://trips/{tripId}/budget', { list: undefined }),
|
|
{ description: 'Budget and expense items for a trip' },
|
|
async (uri, { tripId }) => {
|
|
const id = parseId(tripId);
|
|
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
|
|
const items = listBudgetItems(id);
|
|
return jsonContent(uri.href, items);
|
|
}
|
|
);
|
|
|
|
// Packing checklist
|
|
server.registerResource(
|
|
'trip-packing',
|
|
new ResourceTemplate('trek://trips/{tripId}/packing', { list: undefined }),
|
|
{ description: 'Packing checklist for a trip' },
|
|
async (uri, { tripId }) => {
|
|
const id = parseId(tripId);
|
|
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
|
|
const items = listPackingItems(id);
|
|
return jsonContent(uri.href, items);
|
|
}
|
|
);
|
|
|
|
// Reservations (flights, hotels, restaurants)
|
|
server.registerResource(
|
|
'trip-reservations',
|
|
new ResourceTemplate('trek://trips/{tripId}/reservations', { list: undefined }),
|
|
{ description: 'Reservations (flights, hotels, restaurants) for a trip' },
|
|
async (uri, { tripId }) => {
|
|
const id = parseId(tripId);
|
|
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
|
|
const reservations = listReservations(id);
|
|
return jsonContent(uri.href, reservations);
|
|
}
|
|
);
|
|
|
|
// Day notes
|
|
server.registerResource(
|
|
'day-notes',
|
|
new ResourceTemplate('trek://trips/{tripId}/days/{dayId}/notes', { list: undefined }),
|
|
{ description: 'Notes for a specific day in a trip' },
|
|
async (uri, { tripId, dayId }) => {
|
|
const tId = parseId(tripId);
|
|
const dId = parseId(dayId);
|
|
if (tId === null || dId === null || !canAccessTrip(tId, userId)) return accessDenied(uri.href);
|
|
const notes = listDayNotes(dId, tId);
|
|
return jsonContent(uri.href, notes);
|
|
}
|
|
);
|
|
|
|
// Accommodations (hotels, rentals) per trip
|
|
server.registerResource(
|
|
'trip-accommodations',
|
|
new ResourceTemplate('trek://trips/{tripId}/accommodations', { list: undefined }),
|
|
{ description: 'Accommodations (hotels, rentals) for a trip with check-in/out details' },
|
|
async (uri, { tripId }) => {
|
|
const id = parseId(tripId);
|
|
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
|
|
const accommodations = listAccommodations(id);
|
|
return jsonContent(uri.href, accommodations);
|
|
}
|
|
);
|
|
|
|
// Trip members (owner + collaborators)
|
|
server.registerResource(
|
|
'trip-members',
|
|
new ResourceTemplate('trek://trips/{tripId}/members', { list: undefined }),
|
|
{ description: 'Owner and collaborators of a trip' },
|
|
async (uri, { tripId }) => {
|
|
const id = parseId(tripId);
|
|
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
|
|
const ownerRow = getTripOwner(id);
|
|
if (!ownerRow) return accessDenied(uri.href);
|
|
const { owner, members } = listMembers(id, ownerRow.user_id);
|
|
return jsonContent(uri.href, { owner, members });
|
|
}
|
|
);
|
|
|
|
// Collab notes for a trip
|
|
server.registerResource(
|
|
'trip-collab-notes',
|
|
new ResourceTemplate('trek://trips/{tripId}/collab-notes', { list: undefined }),
|
|
{ description: 'Shared collaborative notes for a trip' },
|
|
async (uri, { tripId }) => {
|
|
const id = parseId(tripId);
|
|
if (id === null || !canAccessTrip(id, userId)) return accessDenied(uri.href);
|
|
const notes = listCollabNotes(id);
|
|
return jsonContent(uri.href, notes);
|
|
}
|
|
);
|
|
|
|
// All place categories (global, no trip filter)
|
|
server.registerResource(
|
|
'categories',
|
|
'trek://categories',
|
|
{ description: 'All available place categories (id, name, color, icon) for use when creating places' },
|
|
async (uri) => {
|
|
const categories = listCategories();
|
|
return jsonContent(uri.href, categories);
|
|
}
|
|
);
|
|
|
|
// User's bucket list
|
|
server.registerResource(
|
|
'bucket-list',
|
|
'trek://bucket-list',
|
|
{ description: 'Your personal travel bucket list' },
|
|
async (uri) => {
|
|
const items = listBucketList(userId);
|
|
return jsonContent(uri.href, items);
|
|
}
|
|
);
|
|
|
|
// User's visited countries
|
|
server.registerResource(
|
|
'visited-countries',
|
|
'trek://visited-countries',
|
|
{ description: 'Countries you have marked as visited in Atlas' },
|
|
async (uri) => {
|
|
const countries = listVisitedCountries(userId);
|
|
return jsonContent(uri.href, countries);
|
|
}
|
|
);
|
|
}
|