mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
830f6c0706
OAuth 2.1 authentication for MCP:
- Add OAuth 2.1 authorization server with PKCE support (routes/oauth.ts)
- Add OAuth service for client CRUD, auth-code flow, and token management (services/oauthService.ts)
- Add typed scope definitions and enforcement helpers (mcp/scopes.ts)
- Add OAuth consent UI page (OAuthAuthorizePage.tsx)
- Add client-side scope labels and descriptions (api/oauthScopes.ts)
- Integrate OAuth token auth into MCP handler alongside existing static tokens
- All OAuth endpoints gated on `mcp` addon
Addon gating across MCP tools, resources, and prompts:
- Add typed ADDON_IDS constant (server/src/addons.ts) replacing all string literals
- Gate budget tools and resources (trip-budget, per-person, settlement) on `budget` addon
- Gate packing tools and resources (trip-packing, trip-packing-bags, trip-todos) on `packing` addon
- Gate todos tools on `packing` addon (mirrors web UI Lists tab behavior)
- Expand atlas gate to cover full tool body (bucket-list + country tools no longer leak)
- Expand collab gate to cover full tool body (collab notes no longer leak)
- Gate packing-list and budget-overview MCP prompts on their respective addons
- Gate get_trip_summary sections per addon; blank packing/budget/collab_notes/todos when disabled
- Remove trip-files resource and files field from get_trip_summary
- Replace all isAddonEnabled('literal') calls with ADDON_IDS constants
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
|
import { z } from 'zod';
|
|
import { isDemoUser } from '../../services/authService';
|
|
import {
|
|
getNotifications, getUnreadCount,
|
|
markRead as markNotificationRead, markUnread as markNotificationUnread,
|
|
markAllRead,
|
|
} from '../../services/inAppNotifications';
|
|
import {
|
|
TOOL_ANNOTATIONS_READONLY, TOOL_ANNOTATIONS_WRITE,
|
|
TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
|
demoDenied, ok,
|
|
} from './_shared';
|
|
import { canRead, canWrite } from '../scopes';
|
|
|
|
export function registerNotificationTools(server: McpServer, userId: number, scopes: string[] | null): void {
|
|
const R = canRead(scopes, 'notifications');
|
|
const W = canWrite(scopes, 'notifications');
|
|
|
|
// --- NOTIFICATIONS ---
|
|
|
|
if (R) server.registerTool(
|
|
'list_notifications',
|
|
{
|
|
description: 'List in-app notifications for the current user.',
|
|
inputSchema: {
|
|
limit: z.number().int().positive().optional().default(20),
|
|
offset: z.number().int().min(0).optional().default(0),
|
|
unread_only: z.boolean().optional().default(false),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_READONLY,
|
|
},
|
|
async ({ limit, offset, unread_only }) => {
|
|
const result = getNotifications(userId, { limit: limit ?? 20, offset: offset ?? 0, unreadOnly: unread_only ?? false });
|
|
return ok(result);
|
|
}
|
|
);
|
|
|
|
if (R) server.registerTool(
|
|
'get_unread_notification_count',
|
|
{
|
|
description: 'Get the number of unread in-app notifications.',
|
|
inputSchema: {},
|
|
annotations: TOOL_ANNOTATIONS_READONLY,
|
|
},
|
|
async () => {
|
|
const count = getUnreadCount(userId);
|
|
return ok({ count });
|
|
}
|
|
);
|
|
|
|
if (W) server.registerTool(
|
|
'mark_notification_read',
|
|
{
|
|
description: 'Mark a single notification as read.',
|
|
inputSchema: {
|
|
notificationId: z.number().int().positive(),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_WRITE,
|
|
},
|
|
async ({ notificationId }) => {
|
|
if (isDemoUser(userId)) return demoDenied();
|
|
const success = markNotificationRead(notificationId, userId);
|
|
if (!success) return { content: [{ type: 'text' as const, text: 'Notification not found.' }], isError: true };
|
|
return ok({ success: true });
|
|
}
|
|
);
|
|
|
|
if (W) server.registerTool(
|
|
'mark_notification_unread',
|
|
{
|
|
description: 'Mark a single notification as unread.',
|
|
inputSchema: {
|
|
notificationId: z.number().int().positive(),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_WRITE,
|
|
},
|
|
async ({ notificationId }) => {
|
|
if (isDemoUser(userId)) return demoDenied();
|
|
const success = markNotificationUnread(notificationId, userId);
|
|
if (!success) return { content: [{ type: 'text' as const, text: 'Notification not found.' }], isError: true };
|
|
return ok({ success: true });
|
|
}
|
|
);
|
|
|
|
if (W) server.registerTool(
|
|
'mark_all_notifications_read',
|
|
{
|
|
description: "Mark all of the current user's notifications as read.",
|
|
inputSchema: {},
|
|
annotations: TOOL_ANNOTATIONS_WRITE,
|
|
},
|
|
async () => {
|
|
if (isDemoUser(userId)) return demoDenied();
|
|
const count = markAllRead(userId);
|
|
return ok({ success: true, count });
|
|
}
|
|
);
|
|
}
|