Spaces:
Sleeping
Sleeping
File size: 6,363 Bytes
57a889c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
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, 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(
'trip-summary',
{
title: 'Trip Summary',
description: 'Load a full summary of a trip for context before planning or modifications',
argsSchema: {
tripId: z.number().int().positive().describe('Trip ID to summarize'),
},
},
async ({ tripId }) => {
if (!canAccessTrip(tripId, userId)) {
return { messages: [{ role: 'user', content: { type: 'text', text: 'Trip not found or access denied.' } }] };
}
const summary = getTripSummary(tripId);
if (!summary) {
return { messages: [{ role: 'user', content: { type: 'text', text: 'Trip not found.' } }] };
}
const { trip, days, members, budget, packing, reservations, collab_notes } = summary;
const memberList = [members?.owner, ...(members?.collaborators || [])].filter(Boolean);
const text = `Trip: ${trip?.title || 'Untitled'}${trip?.description ? `\n${trip.description}` : ''}
Dates: ${trip?.start_date || '?'} to ${trip?.end_date || '?'}
Members: ${memberList.length} (${memberList.map((m: any) => m.name || m.email).join(', ') || 'none'})
Days: ${days?.length || 0}
Packing: ${packing?.checked || 0}/${packing?.total || 0} items packed
Budget: ${budget?.total || 0} ${trip?.currency || 'EUR'} total
Reservations: ${reservations?.length || 0}
Collab Notes: ${collab_notes?.length || 0}
${days?.map((d: any, i: number) => `Day ${i + 1} (${d.date}): ${d.assignments?.length || 0} places${d.title ? ` - ${d.title}` : ''}`).join('\n') || 'No days yet'}`;
return {
description: `Summary of trip "${trip?.title || tripId}"`,
messages: [{ role: 'user', content: { type: 'text', text } }],
};
}
);
if (isAddonEnabled(ADDON_IDS.PACKING)) server.registerPrompt(
'packing-list',
{
title: 'Packing List',
description: 'Get a formatted packing checklist for a trip',
argsSchema: {
tripId: z.number().int().positive().describe('Trip ID'),
},
},
async ({ tripId }) => {
if (!canAccessTrip(tripId, userId)) {
return { messages: [{ role: 'user', content: { type: 'text', text: 'Trip not found or access denied.' } }] };
}
const items = listPackingItems(tripId);
if (!items.length) {
return { messages: [{ role: 'user', content: { type: 'text', text: 'No packing items found for this trip.' } }] };
}
const grouped = items.reduce((acc: Record<string, any[]>, item: any) => {
const cat = item.category || 'General';
if (!acc[cat]) acc[cat] = [];
acc[cat].push(item);
return acc;
}, {});
const lines = Object.entries(grouped).map(([cat, items]) =>
`## ${cat}\n${(items as any[]).map((i: any) => `- [${i.checked ? 'x' : ' '}] ${i.name}`).join('\n')}`
).join('\n\n');
const { trip } = getTripSummary(tripId) || {};
return {
description: `Packing list for "${trip?.title || tripId}"`,
messages: [{ role: 'user', content: { type: 'text', text: `# Packing List: ${trip?.title || 'Trip'}\n\n${lines}\n\n_${items.length} items across ${Object.keys(grouped).length} categories_` } }],
};
}
);
if (isAddonEnabled(ADDON_IDS.BUDGET)) server.registerPrompt(
'budget-overview',
{
title: 'Budget Overview',
description: 'Get a formatted budget summary for a trip',
argsSchema: {
tripId: z.number().int().positive().describe('Trip ID'),
},
},
async ({ tripId }) => {
if (!canAccessTrip(tripId, userId)) {
return { messages: [{ role: 'user', content: { type: 'text', text: 'Trip not found or access denied.' } }] };
}
const summary = getTripSummary(tripId);
if (!summary) {
return { messages: [{ role: 'user', content: { type: 'text', text: 'Trip not found.' } }] };
}
const { trip, budget } = summary;
const currency = trip?.currency || 'EUR';
const byCategory = (budget?.items || []).reduce((acc: Record<string, number>, item: any) => {
const cat = item.category || 'Uncategorized';
acc[cat] = (acc[cat] || 0) + (item.total_price || 0);
return acc;
}, {} as Record<string, number>);
const total = Object.values(byCategory).reduce((s, v) => s + v, 0);
const lines = Object.entries(byCategory)
.sort(([, a], [, b]) => b - a)
.map(([cat, amount]) => `- ${cat}: ${amount} ${currency}`)
.join('\n');
const memberCount = Math.max(1, [summary.members?.owner, ...(summary.members?.collaborators || [])].filter(Boolean).length);
const perPerson = (total / memberCount).toFixed(2);
return {
description: `Budget overview for "${trip?.title || tripId}"`,
messages: [{ role: 'user', content: { type: 'text', text: `# Budget: ${trip?.title || 'Trip'}\n\n**Total: ${total} ${currency}** (${perPerson} ${currency} per person)\n\n${lines || 'No expenses recorded.'}` } }],
};
}
);
}
|