mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
refactor(server): decorate the last three MCP prompts
The prompts registrar is gone. Each prompt moves to the domain that owns its data instead of a shared file that had to import three bridges to serve them. packing-list goes to PackingMcp and budget-overview to BudgetMcp, both reusing the `when` addon gate their tools already carry — so a disabled addon keeps the prompt out of the listing, exactly as the `if (isAddonEnabled(...))` around the registration call did. Both keep taking the trip title through trips.bridge: TripsModule imports PackingModule and BudgetModule, so injecting TripsService would close a cycle. Same documented in-container exception as atlas.mcp.ts, and the registrar reached for it the same way. token_auth_notice goes to a new AuthMcp — it is a statement about how the session authenticated, not about trip data, and it injects nothing because the message is static. Its `if (isStaticToken)` guard becomes `when: (ctx) => ctx.isStaticToken`; the registry hands `when` the session context, so an OAuth 2.1 session never sees the prompt listed at all, which is what the registrar achieved by skipping registration. src/mcp/tools/ is down to _shared.ts and journey.ts. journey stays until services/journeyService folds, which waits on memories.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||
import { registerJourneyTools } from './tools/journey';
|
||||
import { registerMcpPrompts } from './tools/prompts';
|
||||
import { getMcpRegistry } from './registry-handoff';
|
||||
|
||||
export function registerTools(server: McpServer, userId: number, scopes: string[] | null, isStaticToken = false, getDeprecationNotice: () => string | null = () => null): void {
|
||||
@@ -59,7 +58,10 @@ export function registerTools(server: McpServer, userId: number, scopes: string[
|
||||
// The todo tools moved to the DI-discovered src/nest/todo/todo.mcp.ts
|
||||
// (@McpController, attached via the nest-mcp registry below).
|
||||
|
||||
registerMcpPrompts(server, userId, isStaticToken);
|
||||
// The prompts moved to the DI-discovered @McpController classes:
|
||||
// packing-list to packing.mcp.ts, budget-overview to budget.mcp.ts and the
|
||||
// static-token notice to auth.mcp.ts (its `if (isStaticToken)` became a
|
||||
// `when` gate — the registry hands `when` the session context).
|
||||
|
||||
// Decorator-registered domains (@trek/nest-mcp) — migrating off the legacy
|
||||
// registrar fan-out above, one domain at a time. Unset registry (direct
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||
import { z } from 'zod';
|
||||
import { canAccessTrip } from '../../db/database';
|
||||
import { getTripSummary } from '../../nest/trips/trips.bridge';
|
||||
import { listItems as listPackingItems } from '../../nest/packing/packing.bridge';
|
||||
import { isAddonEnabled } from '../../nest/addons/addons.bridge';
|
||||
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;
|
||||
|
||||
// The trip-summary prompt moved to the DI-discovered
|
||||
// src/nest/trips/trips.mcp.ts (@Prompt, attached via the nest-mcp registry
|
||||
// in registerTools).
|
||||
|
||||
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.' } }] };
|
||||
}
|
||||
// Hide other members' private items (#858) from the requesting user.
|
||||
const items = listPackingItems(tripId, userId);
|
||||
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, userId) || {};
|
||||
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, userId);
|
||||
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.'}` } }],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { McpController, Prompt, type McpContext } from '@trek/nest-mcp';
|
||||
|
||||
/**
|
||||
* Auth-related MCP surface. One prompt today, moved 1:1 from the legacy
|
||||
* registrar src/mcp/tools/prompts.ts.
|
||||
*
|
||||
* It lives in the auth domain rather than a domain of its own because it is a
|
||||
* statement about how the session authenticated, not about any trip data — and
|
||||
* it injects nothing, since the message is static.
|
||||
*
|
||||
* The registrar gated it with an `if (isStaticToken)` around the registration
|
||||
* call. `when` receives the session context, so that becomes a declarative gate
|
||||
* with the same effect: OAuth 2.1 sessions never see the prompt listed at all.
|
||||
*/
|
||||
@McpController()
|
||||
export class AuthMcp {
|
||||
@Prompt({
|
||||
name: 'token_auth_notice',
|
||||
title: 'Static Token Auth Notice',
|
||||
description: 'Notification that this session uses a static API token which will be deprecated',
|
||||
argsSchema: {},
|
||||
when: (ctx: McpContext) => ctx.isStaticToken,
|
||||
})
|
||||
async tokenAuthNotice() {
|
||||
return {
|
||||
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.',
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { AuthController } from './auth.controller';
|
||||
import { PasskeyController } from './passkey.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { PasskeyService } from './passkey.service';
|
||||
import { AuthMcp } from './auth.mcp';
|
||||
import { UserCleanupService } from './user-cleanup.service';
|
||||
import { WebauthnConfigService } from './webauthn-config.service';
|
||||
import { AppConfigModule } from '../app-config/app-config.module';
|
||||
@@ -33,7 +34,7 @@ import { TripMembershipModule } from '../trip-membership/trip-membership.module'
|
||||
@Module({
|
||||
imports: [RateLimitModule, AuditModule, PermissionsModule, AtlasModule, TripMembershipModule, MailerModule, AppConfigModule],
|
||||
controllers: [AuthPublicController, AuthController, PasskeyController],
|
||||
providers: [AuthService, PasskeyService, UserCleanupService, WebauthnConfigService],
|
||||
providers: [AuthService, PasskeyService, UserCleanupService, WebauthnConfigService, AuthMcp],
|
||||
exports: [AuthService, PasskeyService, UserCleanupService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
McpController, Tool, ResourceTemplate, type McpContext,
|
||||
McpController, Prompt, Tool, ResourceTemplate, type McpContext,
|
||||
TOOL_ANNOTATIONS_READONLY, TOOL_ANNOTATIONS_WRITE,
|
||||
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
||||
demoDenied, errorResult, ok,
|
||||
@@ -9,7 +9,7 @@ import { AuthService } from '../auth/auth.service';
|
||||
import { isAddonEnabled } from '../addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { safeBroadcast, noAccess, hasTripPermission, permissionDenied } from '../../mcp/tools/_shared';
|
||||
import { getTripOwner, listMembers } from '../trips/trips.bridge';
|
||||
import { getTripOwner, listMembers, getTripSummary } from '../trips/trips.bridge';
|
||||
import { DatabaseService } from '../database/database.service';
|
||||
import { BudgetService } from './budget.service';
|
||||
import { ExchangeRatesService } from './exchange-rates.service';
|
||||
@@ -466,4 +466,47 @@ export class BudgetMcp {
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Moved 1:1 from the legacy registrar src/mcp/tools/prompts.ts. The
|
||||
* registration-time `if (isAddonEnabled(BUDGET))` became the same `when` gate
|
||||
* the tools above use, and the trip lookup keeps going through trips.bridge —
|
||||
* injecting TripsService here would close a TripsModule/BudgetModule cycle.
|
||||
*/
|
||||
@Prompt({
|
||||
name: 'budget-overview',
|
||||
title: 'Budget Overview',
|
||||
description: 'Get a formatted budget summary for a trip',
|
||||
argsSchema: {
|
||||
tripId: z.number().int().positive().describe('Trip ID'),
|
||||
},
|
||||
when: budgetAddonOn,
|
||||
})
|
||||
async budgetOverviewPrompt({ tripId }: { tripId: number }, ctx: McpContext) {
|
||||
if (!this.budget.verifyTripAccess(tripId, ctx.userId)) {
|
||||
return { messages: [{ role: 'user' as const, content: { type: 'text' as const, text: 'Trip not found or access denied.' } }] };
|
||||
}
|
||||
const summary = getTripSummary(tripId, ctx.userId);
|
||||
if (!summary) {
|
||||
return { messages: [{ role: 'user' as const, content: { type: 'text' as const, text: 'Trip not found.' } }] };
|
||||
}
|
||||
const { trip, budget } = summary;
|
||||
const currency = trip?.currency || 'EUR';
|
||||
const byCategory = (budget?.items || []).reduce((acc: Record<string, number>, item: { category?: string; total_price?: number }) => {
|
||||
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((sum, v) => sum + 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' as const, content: { type: 'text' as const, text: `# Budget: ${trip?.title || 'Trip'}\n\n**Total: ${total} ${currency}** (${perPerson} ${currency} per person)\n\n${lines || 'No expenses recorded.'}` } }],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
McpController, Tool, ResourceTemplate, type McpContext,
|
||||
McpController, Prompt, Tool, ResourceTemplate, type McpContext,
|
||||
TOOL_ANNOTATIONS_READONLY, TOOL_ANNOTATIONS_WRITE,
|
||||
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
||||
demoDenied, errorResult, ok,
|
||||
@@ -10,6 +10,10 @@ import { isAddonEnabled } from '../addons/addons.bridge';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { safeBroadcast, noAccess, hasTripPermission, permissionDenied, isAdminUser, adminRequired } from '../../mcp/tools/_shared';
|
||||
import { PackingService } from './packing.service';
|
||||
// trips.bridge, not an injected TripsService: TripsModule imports PackingModule,
|
||||
// so injecting it here would close a module cycle. Same documented in-container
|
||||
// exception as atlas.mcp.ts, and the legacy registrar reached for it the same way.
|
||||
import { getTripSummary } from '../trips/trips.bridge';
|
||||
|
||||
/** Legacy registrar gate: the whole packing surface rides the packing addon. */
|
||||
const packingAddonOn = () => isAddonEnabled(ADDON_IDS.PACKING);
|
||||
@@ -463,4 +467,43 @@ export class PackingMcp {
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Moved 1:1 from the legacy registrar src/mcp/tools/prompts.ts. The
|
||||
* registration-time `if (isAddonEnabled(PACKING))` became the same `when`
|
||||
* gate the tools above use.
|
||||
*/
|
||||
@Prompt({
|
||||
name: 'packing-list',
|
||||
title: 'Packing List',
|
||||
description: 'Get a formatted packing checklist for a trip',
|
||||
argsSchema: {
|
||||
tripId: z.number().int().positive().describe('Trip ID'),
|
||||
},
|
||||
when: packingAddonOn,
|
||||
})
|
||||
async packingListPrompt({ tripId }: { tripId: number }, ctx: McpContext) {
|
||||
if (!this.packing.verifyTripAccess(tripId, ctx.userId)) {
|
||||
return { messages: [{ role: 'user' as const, content: { type: 'text' as const, text: 'Trip not found or access denied.' } }] };
|
||||
}
|
||||
// Hide other members' private items (#858) from the requesting user.
|
||||
const items = this.packing.listItems(tripId, ctx.userId);
|
||||
if (!items.length) {
|
||||
return { messages: [{ role: 'user' as const, content: { type: 'text' as const, text: 'No packing items found for this trip.' } }] };
|
||||
}
|
||||
const grouped = items.reduce((acc: Record<string, unknown[]>, item: { category?: string }) => {
|
||||
const cat = item.category || 'General';
|
||||
if (!acc[cat]) acc[cat] = [];
|
||||
acc[cat].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
const lines = Object.entries(grouped).map(([cat, catItems]) =>
|
||||
`## ${cat}\n${(catItems as { checked?: unknown; name?: string }[]).map((i) => `- [${i.checked ? 'x' : ' '}] ${i.name}`).join('\n')}`
|
||||
).join('\n\n');
|
||||
const { trip } = getTripSummary(tripId, ctx.userId) || {};
|
||||
return {
|
||||
description: `Packing list for "${trip?.title || tripId}"`,
|
||||
messages: [{ role: 'user' as const, content: { type: 'text' as const, text: `# Packing List: ${trip?.title || 'Trip'}\n\n${lines}\n\n_${items.length} items across ${Object.keys(grouped).length} categories_` } }],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { MapsMcp } from '../../src/nest/maps/maps.mcp';
|
||||
import { WeatherMcp } from '../../src/nest/weather/weather.mcp';
|
||||
import { WeatherService } from '../../src/nest/weather/weather.service';
|
||||
import { AirportsMcp } from '../../src/nest/airports/airports.mcp';
|
||||
import { AuthMcp } from '../../src/nest/auth/auth.mcp';
|
||||
import { MapsService } from '../../src/nest/maps/maps.service';
|
||||
import { NotificationsMcp } from '../../src/nest/notifications/notifications.mcp';
|
||||
import { NotificationsService } from '../../src/nest/notifications/notifications.service';
|
||||
@@ -95,6 +96,7 @@ export function createMcpTestRegistry(): McpRegistry {
|
||||
// The weather and airport tools left the legacy mapsWeather registrar.
|
||||
new WeatherMcp(new WeatherService()),
|
||||
new AirportsMcp(),
|
||||
new AuthMcp(),
|
||||
new TodoMcp(todoService, authService),
|
||||
new PackingMcp(packingService, authService),
|
||||
new BudgetMcp(budgetService, exchangeRatesService, dbService, authService),
|
||||
|
||||
@@ -53,16 +53,27 @@ const { mockGetTripSummary } = vi.hoisted(() => ({
|
||||
// prompts.ts consumes getTripSummary via the trips bridge since the trip fold.
|
||||
vi.mock('../../../src/nest/trips/trips.bridge', () => ({
|
||||
getTripSummary: mockGetTripSummary,
|
||||
getTripOwner: vi.fn(),
|
||||
listMembers: vi.fn(() => ({ members: [] })),
|
||||
}));
|
||||
|
||||
import { createTables } from '../../../src/db/schema';
|
||||
import { runMigrations } from '../../../src/db/migrations';
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createUser, createTrip, addTripMember, createPackingItem, createBudgetItem } from '../../helpers/factories';
|
||||
import { registerMcpPrompts } from '../../../src/mcp/tools/prompts';
|
||||
import { createTestRegistry } from '@trek/nest-mcp';
|
||||
import { trekMcpAccessPolicy, trekMcpValidateAccess } from '../../../src/mcp/nest-mcp-policy';
|
||||
import { TripsMcp } from '../../../src/nest/trips/trips.mcp';
|
||||
import { PackingMcp } from '../../../src/nest/packing/packing.mcp';
|
||||
import { PackingService } from '../../../src/nest/packing/packing.service';
|
||||
import { BudgetMcp } from '../../../src/nest/budget/budget.mcp';
|
||||
import { BudgetService } from '../../../src/nest/budget/budget.service';
|
||||
import { ExchangeRatesService } from '../../../src/nest/budget/exchange-rates.service';
|
||||
import { AuthMcp } from '../../../src/nest/auth/auth.mcp';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { PermissionsService } from '../../../src/nest/permissions/permissions.service';
|
||||
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
|
||||
import type { AuthService } from '../../../src/nest/auth/auth.service';
|
||||
import type { TripsService } from '../../../src/nest/trips/trips.service';
|
||||
import type { TodoService } from '../../../src/nest/todo/todo.service';
|
||||
import type { CollabService } from '../../../src/nest/collab/collab.service';
|
||||
@@ -76,6 +87,22 @@ const tripsStub = {
|
||||
} as unknown as TripsService;
|
||||
const tripsMcp = new TripsMcp(tripsStub, { listItems: () => [] } as unknown as TodoService, { listPolls: () => [], countMessages: () => 0 } as unknown as CollabService);
|
||||
|
||||
// The three remaining prompts moved to their domains' @McpController classes:
|
||||
// packing-list, budget-overview and the static-token notice. Built over the same
|
||||
// in-memory DB so the cases below keep asserting real rows.
|
||||
const promptDbs = () => new DatabaseService(testDb);
|
||||
const authStub = { isDemoUser: () => false } as unknown as AuthService;
|
||||
const packingMcp = new PackingMcp(
|
||||
new PackingService(promptDbs(), new PermissionsService(promptDbs()), new RealtimeService()),
|
||||
authStub,
|
||||
);
|
||||
const budgetMcp = new BudgetMcp(
|
||||
new BudgetService(promptDbs(), new PermissionsService(promptDbs()), new ExchangeRatesService(), new RealtimeService()),
|
||||
new ExchangeRatesService(),
|
||||
promptDbs(),
|
||||
);
|
||||
const authMcp = new AuthMcp();
|
||||
|
||||
beforeAll(() => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
@@ -125,10 +152,9 @@ afterAll(() => {
|
||||
/** Build a fresh McpServer with prompts registered for the given userId. */
|
||||
function buildServer(userId: number, opts: { isStaticToken?: boolean } = {}): McpServer {
|
||||
const server = new McpServer({ name: 'trek-test', version: '1.0.0' });
|
||||
registerMcpPrompts(server, userId, opts.isStaticToken ?? false);
|
||||
// Attach the DI-discovered trip surface (carries the trip-summary @Prompt)
|
||||
// the same way registerTools does in production.
|
||||
createTestRegistry([tripsMcp], { accessPolicy: trekMcpAccessPolicy, validateAccess: trekMcpValidateAccess })
|
||||
// Every prompt is DI-discovered now; attach them the way registerTools does in
|
||||
// production, including the isStaticToken flag the notice's `when` gate reads.
|
||||
createTestRegistry([tripsMcp, packingMcp, budgetMcp, authMcp], { accessPolicy: trekMcpAccessPolicy, validateAccess: trekMcpValidateAccess })
|
||||
.attach(server, { userId, scopes: null, isStaticToken: opts.isStaticToken ?? false });
|
||||
return server;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user