@@ -6,6 +6,13 @@ import { initializeMcpServer } from '../src/mcp-handler.js';
66import { Ratelimit } from '@upstash/ratelimit' ;
77import { Redis } from '@upstash/redis' ;
88import { isJwtToken , verifyOAuthToken } from '../src/utils/auth.js' ;
9+ import {
10+ buildMcpClientMetadata ,
11+ extractInitializeClientInfo ,
12+ MCP_CLIENT_SESSION_TTL_SECONDS ,
13+ sanitizeMcpClientMetadata ,
14+ type McpClientMetadata ,
15+ } from '../src/utils/mcpClientMetadata.js' ;
916
1017// Origin: '*' is safe — auth is per-request via headers/query, never cookies.
1118const CORS_HEADERS : Record < string , string > = {
@@ -50,6 +57,59 @@ let dailyLimiter: Ratelimit | null = null;
5057let rateLimitersInitialized = false ;
5158let redisClient : Redis | null = null ;
5259
60+ function getMcpClientSessionKey ( sessionId : string ) : string {
61+ return `exa-mcp:client:${ sessionId } ` ;
62+ }
63+
64+ async function saveMcpClientMetadata ( sessionId : string | undefined , metadata : McpClientMetadata | undefined , debug : boolean ) : Promise < void > {
65+ if ( ! sessionId || ! metadata ?. clientInfo ) {
66+ return ;
67+ }
68+
69+ initializeRateLimiters ( ) ;
70+
71+ if ( ! redisClient ) {
72+ return ;
73+ }
74+
75+ try {
76+ await redisClient . set ( getMcpClientSessionKey ( sessionId ) , JSON . stringify ( metadata ) , {
77+ ex : MCP_CLIENT_SESSION_TTL_SECONDS ,
78+ } ) ;
79+ } catch ( error ) {
80+ if ( debug ) {
81+ console . error ( '[EXA-MCP] Failed to save MCP client metadata:' , error ) ;
82+ }
83+ }
84+ }
85+
86+ async function loadMcpClientMetadata ( sessionId : string | undefined , debug : boolean ) : Promise < McpClientMetadata | undefined > {
87+ if ( ! sessionId ) {
88+ return undefined ;
89+ }
90+
91+ initializeRateLimiters ( ) ;
92+
93+ if ( ! redisClient ) {
94+ return undefined ;
95+ }
96+
97+ try {
98+ const value = await redisClient . get < string > ( getMcpClientSessionKey ( sessionId ) ) ;
99+ if ( typeof value !== 'string' ) {
100+ return undefined ;
101+ }
102+
103+ const parsed : unknown = JSON . parse ( value ) ;
104+ return sanitizeMcpClientMetadata ( parsed ) ;
105+ } catch ( error ) {
106+ if ( debug ) {
107+ console . error ( '[EXA-MCP] Failed to load MCP client metadata:' , error ) ;
108+ }
109+ return undefined ;
110+ }
111+ }
112+
53113function initializeRateLimiters ( ) : boolean {
54114 if ( rateLimitersInitialized ) {
55115 return qpsLimiter !== null ;
@@ -299,6 +359,7 @@ interface RequestConfig {
299359 authMethod : 'oauth' | 'api_key' | 'free_tier' ;
300360 exaSource ?: string ;
301361 mcpSessionId ?: string ;
362+ mcpClient ?: McpClientMetadata ;
302363 defaultSearchType ?: 'auto' | 'fast' ;
303364 /** True when a Bearer token was a JWT but failed OAuth verification (expired, bad sig, wrong issuer/audience). */
304365 invalidOAuthJwt : boolean ;
@@ -417,7 +478,7 @@ async function getConfigFromRequest(request: Request): Promise<RequestConfig> {
417478 * configuration (tools and API key). This prevents API key leakage between
418479 * different users who might pass different keys via URL.
419480 */
420- function createHandler ( config : { exaApiKey ?: string ; enabledTools ?: string [ ] ; debug : boolean ; userProvidedApiKey : boolean ; exaSource ?: string ; mcpSessionId ?: string ; defaultSearchType ?: 'auto' | 'fast' } ) {
481+ function createHandler ( config : { exaApiKey ?: string ; enabledTools ?: string [ ] ; debug : boolean ; userProvidedApiKey : boolean ; exaSource ?: string ; mcpSessionId ?: string ; mcpClient ?: McpClientMetadata ; defaultSearchType ?: 'auto' | 'fast' } ) {
421482 return createMcpHandler (
422483 ( server : any ) => {
423484 initializeMcpServer ( server , config ) ;
@@ -484,8 +545,9 @@ async function handleRequest(request: Request, options?: { forceOAuth?: boolean
484545 */
485546async function processRequest ( request : Request , options ?: { forceOAuth ?: boolean } ) : Promise < Response > {
486547 const debug = process . env . DEBUG === 'true' ;
487- const isInitializeRequest =
488- request . method === 'POST' ? isInitializeMethod ( await request . clone ( ) . text ( ) ) : false ;
548+ const body = request . method === 'POST' ? await request . clone ( ) . text ( ) : undefined ;
549+ const isInitializeRequest = isInitializeMethod ( body ?? '' ) ;
550+ const initializeClientInfo = extractInitializeClientInfo ( body ) ;
489551
490552 // Check user-agent bypass BEFORE the 401 gate so bypass clients never see auth prompts
491553 const userAgent = request . headers . get ( 'user-agent' ) || '' ;
@@ -518,6 +580,15 @@ async function processRequest(request: Request, options?: { forceOAuth?: boolean
518580 return create401Response ( ) ;
519581 }
520582
583+ const storedMcpClient = isInitializeRequest ? undefined : await loadMcpClientMetadata ( config . mcpSessionId , config . debug ) ;
584+ config . mcpClient = buildMcpClientMetadata ( {
585+ source : config . exaSource ,
586+ sessionId : config . mcpSessionId ,
587+ clientInfo : initializeClientInfo ,
588+ stored : storedMcpClient ,
589+ userAgent,
590+ } ) ;
591+
521592 if ( config . debug ) {
522593 console . log ( `[EXA-MCP] Request URL: ${ request . url } ` ) ;
523594 console . log ( `[EXA-MCP] Enabled tools: ${ config . enabledTools ?. join ( ', ' ) || 'default' } ` ) ;
@@ -536,12 +607,8 @@ async function processRequest(request: Request, options?: { forceOAuth?: boolean
536607 // Rate limit users who didn't provide their own API key (including bypass users)
537608 // Only rate limit actual tool calls (tools/call), not protocol methods like tools/list
538609 if ( ! config . userProvidedApiKey && request . method === 'POST' ) {
539- // Clone the request to read the body without consuming it
540- const clonedRequest = request . clone ( ) ;
541- const body = await clonedRequest . text ( ) ;
542-
543610 // Only rate limit actual tool calls, not protocol methods
544- if ( isRateLimitedMethod ( body ) ) {
611+ if ( isRateLimitedMethod ( body ?? '' ) ) {
545612 // Initialize rate limiters on first request (lazy init)
546613 initializeRateLimiters ( ) ;
547614
@@ -591,9 +658,22 @@ async function processRequest(request: Request, options?: { forceOAuth?: boolean
591658
592659 const response = withCors ( await handler ( request ) ) ;
593660
594- if ( isInitializeRequest && response . ok && ! response . headers . has ( 'Mcp-Session-Id' ) ) {
661+ if ( isInitializeRequest && response . ok ) {
662+ const responseSessionId = response . headers . get ( 'Mcp-Session-Id' ) ?? config . mcpSessionId ?? randomUUID ( ) ;
663+ const metadata = buildMcpClientMetadata ( {
664+ source : config . exaSource ,
665+ sessionId : responseSessionId ,
666+ clientInfo : initializeClientInfo ,
667+ userAgent,
668+ } ) ;
669+ await saveMcpClientMetadata ( responseSessionId , metadata , config . debug ) ;
670+
671+ if ( response . headers . has ( 'Mcp-Session-Id' ) ) {
672+ return response ;
673+ }
674+
595675 const headers = new Headers ( response . headers ) ;
596- headers . set ( 'Mcp-Session-Id' , randomUUID ( ) ) ;
676+ headers . set ( 'Mcp-Session-Id' , responseSessionId ) ;
597677 return new Response ( response . body , {
598678 status : response . status ,
599679 statusText : response . statusText ,
0 commit comments