mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-23 07:11:46 +00:00
fix(mcp): add RFC 9728 PRM, RFC 8707 audience binding, and collab sub-feature gating
Root cause: claude.ai's MCP connector (spec 2025-06-18) requires the resource server to publish Protected Resource Metadata and return WWW-Authenticate on 401s to bind the /mcp endpoint to its AS. Without these, it silently shows no tools after OAuth. - Add /.well-known/oauth-protected-resource (RFC 9728) with addon gating - Emit WWW-Authenticate: Bearer resource_metadata=... on 401/auth-failure 403s - Open CORS (origin: *) on both .well-known/* endpoints per RFC 8414/9728 - Accept resource parameter at authorize + token endpoints (RFC 8707) - Store audience on oauth_tokens; validate on every MCP request - Refresh tokens inherit audience; add resource_parameter_supported to AS metadata - DB migration: ADD COLUMN audience TEXT to oauth_tokens - Gate collab MCP tools/resources by chat/notes/polls sub-features individually - Invalidate MCP sessions when collab sub-features are toggled in admin - Update test mocks and MCP.md
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
listPolls, createPoll, votePoll, closePoll, deletePoll,
|
||||
listMessages, createMessage, deleteMessage, addOrRemoveReaction,
|
||||
} from '../../services/collabService';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { isAddonEnabled, getCollabFeatures } from '../../services/adminService';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import {
|
||||
safeBroadcast, TOOL_ANNOTATIONS_WRITE, TOOL_ANNOTATIONS_DELETE,
|
||||
@@ -22,9 +22,11 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
|
||||
if (!isAddonEnabled(ADDON_IDS.COLLAB)) return;
|
||||
|
||||
const features = getCollabFeatures();
|
||||
|
||||
// --- COLLAB NOTES ---
|
||||
|
||||
if (W) server.registerTool(
|
||||
if (features.notes && W) server.registerTool(
|
||||
'create_collab_note',
|
||||
{
|
||||
description: 'Create a shared collaborative note on a trip (visible to all trip members in the Collab tab).',
|
||||
@@ -47,7 +49,7 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
}
|
||||
);
|
||||
|
||||
if (W) server.registerTool(
|
||||
if (features.notes && W) server.registerTool(
|
||||
'update_collab_note',
|
||||
{
|
||||
description: 'Edit an existing collaborative note on a trip.',
|
||||
@@ -72,7 +74,7 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
}
|
||||
);
|
||||
|
||||
if (W) server.registerTool(
|
||||
if (features.notes && W) server.registerTool(
|
||||
'delete_collab_note',
|
||||
{
|
||||
description: 'Delete a collaborative note from a trip.',
|
||||
@@ -94,7 +96,7 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
|
||||
// --- COLLAB POLLS & CHAT ---
|
||||
|
||||
if (R) server.registerTool(
|
||||
if (features.polls && R) server.registerTool(
|
||||
'list_collab_polls',
|
||||
{
|
||||
description: 'List all polls for a trip.',
|
||||
@@ -110,7 +112,7 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
}
|
||||
);
|
||||
|
||||
if (W) server.registerTool(
|
||||
if (features.polls && W) server.registerTool(
|
||||
'create_collab_poll',
|
||||
{
|
||||
description: 'Create a new poll in the collab panel.',
|
||||
@@ -132,7 +134,7 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
}
|
||||
);
|
||||
|
||||
if (W) server.registerTool(
|
||||
if (features.polls && W) server.registerTool(
|
||||
'vote_collab_poll',
|
||||
{
|
||||
description: 'Vote on a poll option (or remove vote if already voted for that option).',
|
||||
@@ -152,7 +154,7 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
}
|
||||
);
|
||||
|
||||
if (W) server.registerTool(
|
||||
if (features.polls && W) server.registerTool(
|
||||
'close_collab_poll',
|
||||
{
|
||||
description: 'Close a poll so no more votes can be cast.',
|
||||
@@ -172,7 +174,7 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
}
|
||||
);
|
||||
|
||||
if (W) server.registerTool(
|
||||
if (features.polls && W) server.registerTool(
|
||||
'delete_collab_poll',
|
||||
{
|
||||
description: 'Delete a poll and all its votes.',
|
||||
@@ -192,7 +194,7 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
}
|
||||
);
|
||||
|
||||
if (R) server.registerTool(
|
||||
if (features.chat && R) server.registerTool(
|
||||
'list_collab_messages',
|
||||
{
|
||||
description: 'List chat messages for a trip (most recent 100, oldest-first).',
|
||||
@@ -209,7 +211,7 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
}
|
||||
);
|
||||
|
||||
if (W) server.registerTool(
|
||||
if (features.chat && W) server.registerTool(
|
||||
'send_collab_message',
|
||||
{
|
||||
description: "Send a chat message to a trip's collab channel.",
|
||||
@@ -230,7 +232,7 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
}
|
||||
);
|
||||
|
||||
if (W) server.registerTool(
|
||||
if (features.chat && W) server.registerTool(
|
||||
'delete_collab_message',
|
||||
{
|
||||
description: 'Delete a chat message (only the message owner can delete their own messages).',
|
||||
@@ -250,7 +252,7 @@ export function registerCollabTools(server: McpServer, userId: number, scopes: s
|
||||
}
|
||||
);
|
||||
|
||||
if (W) server.registerTool(
|
||||
if (features.chat && W) server.registerTool(
|
||||
'react_collab_message',
|
||||
{
|
||||
description: 'Toggle a reaction emoji on a chat message (adds if not present, removes if already reacted).',
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import {
|
||||
createOrUpdateShareLink, getShareLink, deleteShareLink,
|
||||
} from '../../services/shareService';
|
||||
import { isAddonEnabled } from '../../services/adminService';
|
||||
import { isAddonEnabled, getCollabFeatures } from '../../services/adminService';
|
||||
import { ADDON_IDS } from '../../addons';
|
||||
import { countMessages, listPolls } from '../../services/collabService';
|
||||
import {
|
||||
@@ -161,6 +161,7 @@ export function registerTripTools(server: McpServer, userId: number, scopes: str
|
||||
const packingEnabled = isAddonEnabled(ADDON_IDS.PACKING);
|
||||
const budgetEnabled = isAddonEnabled(ADDON_IDS.BUDGET);
|
||||
const collabEnabled = isAddonEnabled(ADDON_IDS.COLLAB);
|
||||
const collabFeatures = collabEnabled ? getCollabFeatures() : null;
|
||||
// Scope gates — sections not covered by the client's OAuth scopes are omitted.
|
||||
// Core trip data (metadata, days, members, accommodations) is always included
|
||||
// because this tool is always registered and needed for navigation.
|
||||
@@ -173,16 +174,16 @@ export function registerTripTools(server: McpServer, userId: number, scopes: str
|
||||
let pollCount = 0;
|
||||
let messageCount = 0;
|
||||
if (canReadCollab) {
|
||||
pollCount = listPolls(tripId).length;
|
||||
messageCount = countMessages(tripId);
|
||||
if (collabFeatures?.polls) pollCount = listPolls(tripId).length;
|
||||
if (collabFeatures?.chat) messageCount = countMessages(tripId);
|
||||
}
|
||||
const notice = getDeprecationNotice();
|
||||
const data = {
|
||||
const summaryData = {
|
||||
...summary,
|
||||
reservations: canReadRes ? summary.reservations : undefined,
|
||||
packing: canReadPacking ? summary.packing : undefined,
|
||||
budget: canReadBudget ? summary.budget : undefined,
|
||||
collab_notes: canReadCollab ? summary.collab_notes : [],
|
||||
reservations: canReadRes ? summary.reservations : undefined,
|
||||
packing: canReadPacking ? summary.packing : undefined,
|
||||
budget: canReadBudget ? summary.budget : undefined,
|
||||
collab_notes: canReadCollab && collabFeatures?.notes ? summary.collab_notes : [],
|
||||
todos,
|
||||
pollCount,
|
||||
messageCount,
|
||||
@@ -191,19 +192,10 @@ export function registerTripTools(server: McpServer, userId: number, scopes: str
|
||||
isError: true as const,
|
||||
content: [
|
||||
{ type: 'text' as const, text: notice },
|
||||
{ type: 'text' as const, text: JSON.stringify(data, null, 2) },
|
||||
{ type: 'text' as const, text: JSON.stringify(summaryData, null, 2) },
|
||||
],
|
||||
};
|
||||
return ok({
|
||||
...summary,
|
||||
reservations: canReadRes ? summary.reservations : undefined,
|
||||
packing: canReadPacking ? summary.packing : undefined,
|
||||
budget: canReadBudget ? summary.budget : undefined,
|
||||
collab_notes: canReadCollab ? summary.collab_notes : [],
|
||||
todos,
|
||||
pollCount,
|
||||
messageCount,
|
||||
});
|
||||
return ok(summaryData);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user