2026-03-30 09:53:45 +08:00
import { Request , Response } from 'express' ;
2026-04-05 00:12:14 +08:00
import { randomUUID } from 'crypto' ;
2026-03-30 09:53:45 +08:00
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp' ;
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp' ;
import { User } from '../types' ;
2026-04-05 00:12:14 +08:00
import { verifyMcpToken , verifyJwtToken } from '../services/authService' ;
2026-04-10 04:25:58 +08:00
import { getUserByAccessToken } from '../services/oauthService' ;
2026-04-05 00:12:14 +08:00
import { isAddonEnabled } from '../services/adminService' ;
2026-04-10 04:25:58 +08:00
import { ADDON_IDS } from '../addons' ;
2026-03-30 09:53:45 +08:00
import { registerResources } from './resources' ;
import { registerTools } from './tools' ;
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
import { McpSession , sessions , revokeUserSessions , revokeUserSessionsForClient } from './sessionManager' ;
2026-03-30 09:53:45 +08:00
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
export { revokeUserSessions , revokeUserSessionsForClient } ;
2026-03-30 09:53:45 +08:00
feat(mcp): add base server instructions for all MCP sessions
Injects a structured BASE_MCP_INSTRUCTIONS string into every session's
initialize response so Claude has data model, workflow, and behavioral
context without needing to infer it from tool names alone.
Covers: data model hierarchy (trip→day→place→assignment), key discovery
workflow (list_trips → get_trip_summary), correct place-to-itinerary
flow (search_place → create_place → assign_place_to_day), accommodation
creation order, access rules, date/time format, add-on feature list,
and common pitfalls (e.g. don't skip search_place, confirm before bulk
deletes).
Static token deprecation notice is appended on top when applicable.
2026-04-10 08:23:15 +08:00
// ---------------------------------------------------------------------------
// Base instructions injected into every MCP session via the initialize response.
// Claude and other clients use these as system-level context before any tool call.
// Keep this actionable and concise — vague prose doesn't help the model.
// ---------------------------------------------------------------------------
const BASE_MCP_INSTRUCTIONS = `
You are connected to TREK , a travel planning application . Below is a compact reference of the data model , key workflows , and behavioral rules you must follow .
# # Data model
- * * Trip * * — top - level container . Has dates , currency , members ( owner + collaborators ) , and optional add - ons .
- * * Day * * — one calendar day within a trip ( YYYY - MM - DD ) . Days are generated automatically when a trip is created with start / end dates .
- * * Place * * — a point of interest ( POI ) stored in the trip ' s place pool . A place is NOT on the itinerary until it is assigned to a day .
- * * Assignment * * — links a Place to a Day ( ordered , with optional start / end time ) . This is what builds the daily itinerary .
- * * Accommodation * * — a hotel or rental linked to a Place and a check - in / c h e c k - o u t d a y r a n g e .
- * * Reservation * * — a booking record ( flight , train , restaurant , etc . ) with confirmation details , linked to a day .
- * * Day note * * — a free - text annotation attached to a day ( with optional time label and emoji icon ) .
- * * Budget item * * — an expense entry for a trip ( amount , category , payer , split between members ) .
- * * Packing item * * — a checklist entry grouped into bags and categories .
- * * Todo * * — a task ( not packing - specific ) attached to a trip , ordered and togglable .
- * * Tag * * — a label that can be applied to places for filtering .
- * * Collab note / poll / message * * — shared notes , decision polls , and chat messages for group trips .
- * * Atlas * * — global travel journal : bucket list , visited countries and regions .
- * * Vacay * * — vacation - day planner that tracks leave across team members and years .
# # Key workflows
* * Discovering trips : * * Always call \ ` list_trips \` first when no trip ID has been provided. Never assume a trip ID.
* * Loading trip context : * * Before planning or modifying a trip , call \ ` get_trip_summary \` once. It returns all days (with assignments and notes), accommodations, budget, packing, reservations, collab notes, and todos in a single round-trip. Use this data to answer follow-up questions without extra tool calls.
* * Adding a place to the itinerary ( correct order ) : * *
1 . \ ` search_place \` — find the real-world POI; note the \` osm_id \` and/or \` google_place_id \` in the result.
2 . \ ` create_place \` — add it to the trip's place pool, passing the IDs from step 1 (enables opening hours, ratings, and map linking in the app).
3 . \ ` assign_place_to_day \` — schedule it on the desired day using the dayId from \` get_trip_summary \` .
* * Creating an accommodation : * * A place must exist in the trip first . Create the place ( or reuse an existing one ) , then call \ ` create_accommodation \` with that \` place_id \` and the \` start_day_id \` / \` end_day_id \` .
* * Reordering : * * Assignments , todos , packing items , and reservations all support positional reordering via dedicated reorder tools . Always read the current order from \ ` get_trip_summary \` before reordering.
# # Access rules
- The authenticated user can only access trips they own or are a member of . Never guess at trip IDs .
- Only the trip owner can delete the trip , add members , or remove members .
- Deleting a place removes all of its day assignments as well — warn the user before doing this .
- Trips created via MCP are capped at 90 days .
# # Dates and times
- All dates use ISO format : * * YYYY - MM - DD * * .
- Times are strings like * * "09:00" * * or * * "14:30" * * ( 24 - hour ) . Pass \ ` null \` to clear a time.
- When displaying dates to users , use a friendly human - readable format ( e . g . "Mon, Apr 14" ) .
# # Add - on features
The following features are optional and may not be available on every TREK instance . Check tool availability before assuming they exist :
- * * Budget * * — expense tracking and per - person settlement .
- * * Packing * * — checklist with bags , categories , and templates .
- * * Collab * * — shared notes , polls , and chat messages for group trips .
- * * Atlas * * — bucket list and visited - country / region tracking .
- * * Vacay * * — team vacation - day planner with public holiday integration .
# # Behavioral rules
- Prefer \ ` get_trip_summary \` over individual list tools when you need a full picture — it is one call instead of many.
- Use \ ` search_place \` before \` create_place \` so the app gets structured POI data (coordinates, address, opening hours). Do not skip this step.
- When the user asks to "add X to day Y" , resolve both the place ( search + create if needed ) and the day ID before calling \ ` assign_place_to_day \` .
- Do not batch destructive operations ( delete trip , delete day , delete place ) without explicit user confirmation for each .
- Present budget amounts with the trip ' s currency . Use \ ` get_trip_summary \` to read the currency field.
- For group trips , always check member IDs via \ ` list_trip_members \` before calling tools that require a \` userId \` (e.g. budget splits, assignment participants).
` .trim();
const STATIC_TOKEN_DEPRECATION_NOTICE =
2026-04-10 09:09:49 +08:00
'⚠️ This MCP session is authenticated with a static API token (trek_…). ' +
feat(mcp): add base server instructions for all MCP sessions
Injects a structured BASE_MCP_INSTRUCTIONS string into every session's
initialize response so Claude has data model, workflow, and behavioral
context without needing to infer it from tool names alone.
Covers: data model hierarchy (trip→day→place→assignment), key discovery
workflow (list_trips → get_trip_summary), correct place-to-itinerary
flow (search_place → create_place → assign_place_to_day), accommodation
creation order, access rules, date/time format, add-on feature list,
and common pitfalls (e.g. don't skip search_place, confirm before bulk
deletes).
Static token deprecation notice is appended on top when applicable.
2026-04-10 08:23:15 +08:00
'Static token authentication is deprecated and will be removed in a future version of TREK. ' +
2026-04-10 09:09:49 +08:00
'Inform the user they must migrate to OAuth 2.1: Settings → Integrations → MCP → OAuth Clients in TREK. ' +
'This warning fires once per session. Retry the tool call now to continue normally.' ;
feat(mcp): add base server instructions for all MCP sessions
Injects a structured BASE_MCP_INSTRUCTIONS string into every session's
initialize response so Claude has data model, workflow, and behavioral
context without needing to infer it from tool names alone.
Covers: data model hierarchy (trip→day→place→assignment), key discovery
workflow (list_trips → get_trip_summary), correct place-to-itinerary
flow (search_place → create_place → assign_place_to_day), accommodation
creation order, access rules, date/time format, add-on feature list,
and common pitfalls (e.g. don't skip search_place, confirm before bulk
deletes).
Static token deprecation notice is appended on top when applicable.
2026-04-10 08:23:15 +08:00
2026-03-30 09:53:45 +08:00
const SESSION_TTL_MS = 60 * 60 * 1000 ; // 1 hour
2026-04-06 06:08:17 +08:00
const sessionParsed = Number . parseInt ( process . env . MCP_MAX_SESSION_PER_USER ? ? "" ) ;
const MAX_SESSIONS_PER_USER = Number . isFinite ( sessionParsed ) && sessionParsed > 0 ? sessionParsed : 5 ;
2026-03-30 12:59:24 +08:00
const RATE_LIMIT_WINDOW_MS = 60 * 1000 ; // 1 minute
2026-04-03 21:43:58 +08:00
const parsed = Number . parseInt ( process . env . MCP_RATE_LIMIT ? ? "" ) ;
const RATE_LIMIT_MAX = Number . isFinite ( parsed ) && parsed > 0 ? parsed : 60 ; // requests per minute per user
2026-03-30 12:59:24 +08:00
interface RateLimitEntry {
count : number ;
windowStart : number ;
}
const rateLimitMap = new Map < number , RateLimitEntry > ( ) ;
function isRateLimited ( userId : number ) : boolean {
const now = Date . now ( ) ;
const entry = rateLimitMap . get ( userId ) ;
if ( ! entry || now - entry . windowStart > RATE_LIMIT_WINDOW_MS ) {
rateLimitMap . set ( userId , { count : 1 , windowStart : now } ) ;
return false ;
}
entry . count += 1 ;
return entry . count > RATE_LIMIT_MAX ;
}
function countSessionsForUser ( userId : number ) : number {
const cutoff = Date . now ( ) - SESSION_TTL_MS ;
let count = 0 ;
for ( const session of sessions . values ( ) ) {
if ( session . userId === userId && session . lastActivity >= cutoff ) count ++ ;
}
return count ;
}
2026-03-30 09:53:45 +08:00
const sessionSweepInterval = setInterval ( ( ) = > {
const cutoff = Date . now ( ) - SESSION_TTL_MS ;
2026-04-06 21:21:55 +08:00
let cleaned = 0 ;
2026-03-30 09:53:45 +08:00
for ( const [ sid , session ] of sessions ) {
if ( session . lastActivity < cutoff ) {
2026-03-30 12:59:24 +08:00
try { session . server . close ( ) ; } catch { /* ignore */ }
2026-03-30 09:53:45 +08:00
try { session . transport . close ( ) ; } catch { /* ignore */ }
sessions . delete ( sid ) ;
2026-04-06 21:21:55 +08:00
cleaned ++ ;
2026-03-30 09:53:45 +08:00
}
}
2026-03-30 12:59:24 +08:00
const rateCutoff = Date . now ( ) - RATE_LIMIT_WINDOW_MS ;
for ( const [ uid , entry ] of rateLimitMap ) {
if ( entry . windowStart < rateCutoff ) rateLimitMap . delete ( uid ) ;
}
2026-04-06 21:21:55 +08:00
if ( cleaned > 0 || sessions . size > 0 ) {
console . log ( ` [MCP] Session sweep: cleaned ${ cleaned } , active ${ sessions . size } ` ) ;
}
2026-03-30 09:53:45 +08:00
} , 10 * 60 * 1000 ) ; // sweep every 10 minutes
// Prevent the interval from keeping the process alive if nothing else is running
sessionSweepInterval . unref ( ) ;
2026-04-10 04:25:58 +08:00
interface VerifyTokenResult {
user : User ;
/** null = full access (static token or JWT); string[] = OAuth 2.1 scoped access */
scopes : string [ ] | null ;
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
/** OAuth client_id when authenticated via OAuth 2.1; null otherwise */
clientId : string | null ;
2026-04-10 04:25:58 +08:00
isStaticToken : boolean ;
}
function verifyToken ( authHeader : string | undefined ) : VerifyTokenResult | null {
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
if ( ! authHeader ) return null ;
// M8: strictly require "Bearer" scheme (RFC 6750)
const spaceIdx = authHeader . indexOf ( ' ' ) ;
if ( spaceIdx === - 1 ) return null ;
const scheme = authHeader . slice ( 0 , spaceIdx ) ;
const token = authHeader . slice ( spaceIdx + 1 ) ;
if ( scheme . toLowerCase ( ) !== 'bearer' || ! token ) return null ;
2026-03-30 09:53:45 +08:00
2026-04-10 04:25:58 +08:00
// OAuth 2.1 access token (trekoa_...)
if ( token . startsWith ( 'trekoa_' ) ) {
const result = getUserByAccessToken ( token ) ;
if ( ! result ) return null ;
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
return { user : result.user , scopes : result.scopes , clientId : result.clientId , isStaticToken : false } ;
2026-04-10 04:25:58 +08:00
}
// Long-lived static MCP token (trek_...) — full access + deprecation notice
2026-03-30 09:53:45 +08:00
if ( token . startsWith ( 'trek_' ) ) {
2026-04-10 04:25:58 +08:00
const user = verifyMcpToken ( token ) ;
if ( ! user ) return null ;
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
return { user , scopes : null , clientId : null , isStaticToken : true } ;
2026-03-30 09:53:45 +08:00
}
2026-04-10 04:25:58 +08:00
// Short-lived JWT (TREK web session used directly) — full access, no notice
const user = verifyJwtToken ( token ) ;
if ( ! user ) return null ;
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
return { user , scopes : null , clientId : null , isStaticToken : false } ;
2026-03-30 09:53:45 +08:00
}
export async function mcpHandler ( req : Request , res : Response ) : Promise < void > {
2026-04-10 04:25:58 +08:00
if ( ! isAddonEnabled ( ADDON_IDS . MCP ) ) {
2026-03-30 09:53:45 +08:00
res . status ( 403 ) . json ( { error : 'MCP is not enabled' } ) ;
return ;
}
2026-04-10 04:25:58 +08:00
const tokenResult = verifyToken ( req . headers [ 'authorization' ] ) ;
if ( ! tokenResult ) {
2026-03-30 09:53:45 +08:00
res . status ( 401 ) . json ( { error : 'Access token required' } ) ;
return ;
}
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
const { user , scopes , clientId , isStaticToken } = tokenResult ;
2026-03-30 09:53:45 +08:00
2026-03-30 12:59:24 +08:00
if ( isRateLimited ( user . id ) ) {
res . status ( 429 ) . json ( { error : 'Too many requests. Please slow down.' } ) ;
return ;
}
2026-03-30 09:53:45 +08:00
const sessionId = req . headers [ 'mcp-session-id' ] as string | undefined ;
// Resume an existing session
if ( sessionId ) {
const session = sessions . get ( sessionId ) ;
if ( ! session ) {
res . status ( 404 ) . json ( { error : 'Session not found' } ) ;
return ;
}
if ( session . userId !== user . id ) {
res . status ( 403 ) . json ( { error : 'Session belongs to a different user' } ) ;
return ;
}
session . lastActivity = Date . now ( ) ;
2026-04-06 21:21:55 +08:00
try {
await session . transport . handleRequest ( req , res , req . body ) ;
} catch ( err ) {
console . error ( '[MCP] transport.handleRequest error:' , err ) ;
if ( ! res . headersSent ) {
2026-04-09 18:56:05 +08:00
res . status ( 500 ) . json ( { error : 'Internal MCP error' } ) ;
2026-04-06 21:21:55 +08:00
}
}
2026-03-30 09:53:45 +08:00
return ;
}
// Only POST can initialize a new session
if ( req . method !== 'POST' ) {
res . status ( 400 ) . json ( { error : 'Missing mcp-session-id header' } ) ;
return ;
}
2026-03-30 12:59:24 +08:00
if ( countSessionsForUser ( user . id ) >= MAX_SESSIONS_PER_USER ) {
res . status ( 429 ) . json ( { error : 'Session limit reached. Close an existing session before opening a new one.' } ) ;
return ;
}
2026-03-30 09:53:45 +08:00
// Create a new per-user MCP server and session
2026-04-10 08:17:54 +08:00
const server = new McpServer (
{
name : 'TREK MCP' ,
version : '1.0.0' ,
MCP: add tool annotations, prompts, mimeType, and capabilities
- Add tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) to all 40+ tools
- Register 3 MCP prompts: trip-summary, packing-list, budget-overview
- Add explicit mimeType: application/json to all resource registrations
- Announce capabilities with listChanged on resources, tools, prompts
- Update server name to 'TREK MCP' in MCP initialization
2026-04-06 16:43:31 +08:00
} ,
2026-04-10 08:17:54 +08:00
{
capabilities : {
resources : { listChanged : true } ,
tools : { listChanged : true } ,
prompts : { listChanged : true } ,
} ,
feat(mcp): add base server instructions for all MCP sessions
Injects a structured BASE_MCP_INSTRUCTIONS string into every session's
initialize response so Claude has data model, workflow, and behavioral
context without needing to infer it from tool names alone.
Covers: data model hierarchy (trip→day→place→assignment), key discovery
workflow (list_trips → get_trip_summary), correct place-to-itinerary
flow (search_place → create_place → assign_place_to_day), accommodation
creation order, access rules, date/time format, add-on feature list,
and common pitfalls (e.g. don't skip search_place, confirm before bulk
deletes).
Static token deprecation notice is appended on top when applicable.
2026-04-10 08:23:15 +08:00
instructions : BASE_MCP_INSTRUCTIONS + ( isStaticToken ? STATIC_TOKEN_DEPRECATION_NOTICE : '' ) ,
2026-04-10 08:17:54 +08:00
}
) ;
2026-04-10 08:44:45 +08:00
// Per-session closure: fires the deprecation notice once, on the first tool call.
// Tool results are the only mechanism Claude reliably surfaces to the user;
// the instructions field is only background context and won't trigger a proactive warning.
let _noticeEmitted = false ;
const getDeprecationNotice = ( ) : string | null = > {
if ( ! isStaticToken || _noticeEmitted ) return null ;
_noticeEmitted = true ;
return STATIC_TOKEN_DEPRECATION_NOTICE ;
} ;
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
registerResources ( server , user . id , scopes ) ;
2026-04-10 08:44:45 +08:00
registerTools ( server , user . id , scopes , isStaticToken , getDeprecationNotice ) ;
2026-03-30 09:53:45 +08:00
const transport = new StreamableHTTPServerTransport ( {
sessionIdGenerator : ( ) = > randomUUID ( ) ,
onsessioninitialized : ( sid ) = > {
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
sessions . set ( sid , { server , transport , userId : user.id , scopes , clientId , isStaticToken , lastActivity : Date.now ( ) } ) ;
2026-04-10 04:25:58 +08:00
const authMethod = isStaticToken ? 'static-token' : scopes ? ` oauth( ${ scopes . join ( ',' ) } ) ` : 'jwt' ;
console . log ( ` [MCP] Session ${ sid } created for user ${ user . id } [ ${ authMethod } ]. Active sessions: ${ sessions . size } ` ) ;
2026-03-30 09:53:45 +08:00
} ,
onsessionclosed : ( sid ) = > {
sessions . delete ( sid ) ;
} ,
} ) ;
2026-04-06 21:21:55 +08:00
try {
await server . connect ( transport ) ;
await transport . handleRequest ( req , res , req . body ) ;
} catch ( err ) {
console . error ( '[MCP] transport.handleRequest error:' , err ) ;
if ( ! res . headersSent ) {
res . status ( 500 ) . json ( { error : 'Internal MCP error' , detail : String ( err ) } ) ;
}
}
2026-03-30 09:53:45 +08:00
}
/** Close all active MCP sessions (call during graceful shutdown). */
export function closeMcpSessions ( ) : void {
clearInterval ( sessionSweepInterval ) ;
for ( const [ , session ] of sessions ) {
2026-03-30 12:59:24 +08:00
try { session . server . close ( ) ; } catch { /* ignore */ }
2026-03-30 09:53:45 +08:00
try { session . transport . close ( ) ; } catch { /* ignore */ }
}
sessions . clear ( ) ;
2026-03-30 12:59:24 +08:00
rateLimitMap . clear ( ) ;
2026-03-30 09:53:45 +08:00
}