mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31: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>
113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
|
import { z } from 'zod';
|
|
import { searchPlaces, getPlaceDetails, reverseGeocode, resolveGoogleMapsUrl } from '../../services/mapsService';
|
|
import { getWeather, getDetailedWeather } from '../../services/weatherService';
|
|
import {
|
|
TOOL_ANNOTATIONS_READONLY,
|
|
ok,
|
|
} from './_shared';
|
|
import { canRead } from '../scopes';
|
|
|
|
export function registerMapsWeatherTools(server: McpServer, userId: number, scopes: string[] | null): void {
|
|
if (!canRead(scopes, 'media')) return;
|
|
|
|
// --- MAPS EXTRAS ---
|
|
|
|
server.registerTool(
|
|
'get_place_details',
|
|
{
|
|
description: 'Fetch detailed information about a place by its Google Place ID.',
|
|
inputSchema: {
|
|
placeId: z.string().describe('Google Place ID'),
|
|
lang: z.string().optional().default('en'),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_READONLY,
|
|
},
|
|
async ({ placeId, lang }) => {
|
|
const details = await getPlaceDetails(userId, placeId, lang ?? 'en');
|
|
if (!details) return { content: [{ type: 'text' as const, text: 'Place not found or maps service not configured.' }], isError: true };
|
|
return ok({ details });
|
|
}
|
|
);
|
|
|
|
server.registerTool(
|
|
'reverse_geocode',
|
|
{
|
|
description: 'Get a human-readable address for given coordinates.',
|
|
inputSchema: {
|
|
lat: z.number(),
|
|
lng: z.number(),
|
|
lang: z.string().optional().default('en'),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_READONLY,
|
|
},
|
|
async ({ lat, lng, lang }) => {
|
|
const result = await reverseGeocode(String(lat), String(lng), lang ?? 'en');
|
|
if (!result) return { content: [{ type: 'text' as const, text: 'Reverse geocode failed or maps service not configured.' }], isError: true };
|
|
return ok(result);
|
|
}
|
|
);
|
|
|
|
server.registerTool(
|
|
'resolve_maps_url',
|
|
{
|
|
description: 'Resolve a Google Maps share URL to coordinates and place name.',
|
|
inputSchema: {
|
|
url: z.string().describe('Google Maps share URL'),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_READONLY,
|
|
},
|
|
async ({ url }) => {
|
|
const result = await resolveGoogleMapsUrl(url);
|
|
if (!result) return { content: [{ type: 'text' as const, text: 'Could not resolve URL or maps service not configured.' }], isError: true };
|
|
return ok(result);
|
|
}
|
|
);
|
|
|
|
// --- WEATHER ---
|
|
|
|
server.registerTool(
|
|
'get_weather',
|
|
{
|
|
description: 'Get weather forecast for a location and date.',
|
|
inputSchema: {
|
|
lat: z.number(),
|
|
lng: z.number(),
|
|
date: z.string().describe('ISO date YYYY-MM-DD'),
|
|
lang: z.string().optional().default('en'),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_READONLY,
|
|
},
|
|
async ({ lat, lng, date, lang }) => {
|
|
try {
|
|
const weather = await getWeather(String(lat), String(lng), date, lang ?? 'en');
|
|
return ok({ weather });
|
|
} catch (err: any) {
|
|
return { content: [{ type: 'text' as const, text: err?.message ?? 'Weather service not available.' }], isError: true };
|
|
}
|
|
}
|
|
);
|
|
|
|
server.registerTool(
|
|
'get_detailed_weather',
|
|
{
|
|
description: 'Get hourly/detailed weather forecast for a location and date.',
|
|
inputSchema: {
|
|
lat: z.number(),
|
|
lng: z.number(),
|
|
date: z.string().describe('ISO date YYYY-MM-DD'),
|
|
lang: z.string().optional().default('en'),
|
|
},
|
|
annotations: TOOL_ANNOTATIONS_READONLY,
|
|
},
|
|
async ({ lat, lng, date, lang }) => {
|
|
try {
|
|
const weather = await getDetailedWeather(String(lat), String(lng), date, lang ?? 'en');
|
|
return ok({ weather });
|
|
} catch (err: any) {
|
|
return { content: [{ type: 'text' as const, text: err?.message ?? 'Weather service not available.' }], isError: true };
|
|
}
|
|
}
|
|
);
|
|
}
|