11import * as OtelTracer from "@effect/opentelemetry/Tracer" ;
2- import { Effect , Predicate } from "effect" ;
2+ import { Effect , Layer , Predicate } from "effect" ;
33
44import {
55 McpAuthProvider ,
88 defaultMcpResource ,
99 UNAVAILABLE_RETRY_AFTER_SECONDS ,
1010 type AuthOutcome ,
11+ type McpModernServerBuilder ,
1112 type McpResource ,
1213} from "@executor-js/host-mcp" ;
1314import { requestBodyFromRequest } from "@executor-js/host-mcp/tool-server" ;
@@ -32,9 +33,53 @@ import { createMcpSessionStub, mcpSessionStub } from "@executor-js/cloudflare/mc
3233import { wrapMcpSseResponse } from "../observability/memory-metrics" ;
3334import { WorkerTelemetryLive } from "../observability/telemetry" ;
3435import { cloudMcpAuth } from "./auth-provider" ;
35- import { makeCloudModernMcpServerBuilder } from "./session-durable-object" ;
3636import { parseTraceparent } from "./traceparent" ;
3737
38+ const DEAD_SESSION_CACHE_TTL_MS = 5 * 60 * 1_000 ;
39+ const DEAD_SESSION_CACHE_MAX_ENTRIES = 4_096 ;
40+ const deadSessionExpiries = new Map < string , number > ( ) ;
41+ const timedOutSessionIds = new Set < string > ( ) ;
42+
43+ type DeadSessionReason = "not_found" | "timed_out" ;
44+
45+ const isDeadSessionCached = ( sessionId : string , now = Date . now ( ) ) : boolean => {
46+ const expiry = deadSessionExpiries . get ( sessionId ) ;
47+ if ( expiry === undefined ) return false ;
48+ if ( expiry > now ) return true ;
49+ deadSessionExpiries . delete ( sessionId ) ;
50+ timedOutSessionIds . delete ( sessionId ) ;
51+ return false ;
52+ } ;
53+
54+ const cacheDeadSession = ( sessionId : string , reason : DeadSessionReason , now = Date . now ( ) ) : void => {
55+ deadSessionExpiries . delete ( sessionId ) ;
56+ if ( deadSessionExpiries . size >= DEAD_SESSION_CACHE_MAX_ENTRIES ) {
57+ const oldestSessionId = deadSessionExpiries . keys ( ) . next ( ) . value ;
58+ if ( oldestSessionId !== undefined ) {
59+ deadSessionExpiries . delete ( oldestSessionId ) ;
60+ timedOutSessionIds . delete ( oldestSessionId ) ;
61+ }
62+ }
63+ deadSessionExpiries . set ( sessionId , now + DEAD_SESSION_CACHE_TTL_MS ) ;
64+ if ( reason === "timed_out" ) timedOutSessionIds . add ( sessionId ) ;
65+ else timedOutSessionIds . delete ( sessionId ) ;
66+ } ;
67+
68+ const cachedDeadSessionMessage = ( sessionId : string ) : string =>
69+ timedOutSessionIds . has ( sessionId ) ? "Session timed out, please reconnect" : "Session not found" ;
70+
71+ /** Test-only access to reset and verify the isolate-local dead-session cache. */
72+ export const cloudDeadSessionCacheForTest = {
73+ clear : ( ) : void => {
74+ deadSessionExpiries . clear ( ) ;
75+ timedOutSessionIds . clear ( ) ;
76+ } ,
77+ remember : ( sessionId : string , now ?: number ) : void =>
78+ cacheDeadSession ( sessionId , "not_found" , now ) ,
79+ has : isDeadSessionCached ,
80+ size : ( ) : number => deadSessionExpiries . size ,
81+ } ;
82+
3883const jsonRpcResponse = (
3984 status : number ,
4085 code : number ,
@@ -97,12 +142,12 @@ const renderAuthError = (
97142 } ) ;
98143} ;
99144
100- const authenticate = ( request : Request ) =>
145+ const authenticate = ( request : Request , authProvider : Layer . Layer < McpAuthProvider > ) =>
101146 Effect . gen ( function * ( ) {
102147 const auth = yield * McpAuthProvider ;
103148 const outcome = yield * auth . authenticate ( request ) ;
104149 return { auth, outcome } ;
105- } ) . pipe ( Effect . provide ( cloudMcpAuth ) ) ;
150+ } ) . pipe ( Effect . provide ( authProvider ) ) ;
106151
107152// The earlier shared envelope ran the MCP auth path inside the Effect app, whose
108153// HttpMiddleware provided the OTEL tracer — that is where the `mcp.request`
@@ -126,6 +171,21 @@ const runTraced = <A>(request: Request, program: Effect.Effect<A>): Promise<A> =
126171 ) ;
127172} ;
128173
174+ type TraceCloudMcpRequest = (
175+ request : Request ,
176+ env : Env ,
177+ ctx : ExecutionContext ,
178+ handle : ( tracedRequest : Request ) => Promise < Response > ,
179+ ) => Promise < Response > ;
180+
181+ interface CloudMcpAgentHandlerOptions {
182+ readonly makeModernServerBuilder : (
183+ session : McpSessionProps [ "session" ] ,
184+ ) => McpModernServerBuilder [ "Service" ] ;
185+ readonly authProvider ?: Layer . Layer < McpAuthProvider > ;
186+ readonly traceRequest ?: TraceCloudMcpRequest ;
187+ }
188+
129189// The MCP resource the request targets. `server.ts` routes both the bare `/mcp`
130190// and `/mcp/toolkits/<slug>` to this handler (`prepareMcpOrgScope` strips the org
131191// selector but keeps the toolkit segment), so a session minted on a toolkit path
@@ -158,11 +218,14 @@ const propsForPrincipal = (
158218 } ;
159219 } ) ;
160220
161- export const makeCloudMcpAgentHandler = ( ) => {
221+ /** Build the cloud worker's authenticated legacy/modern MCP request handler. */
222+ export const makeCloudMcpAgentHandler = ( options : CloudMcpAgentHandlerOptions ) => {
223+ const authProvider = options . authProvider ?? cloudMcpAuth ;
224+ const traceRequest = options . traceRequest ?? ( ( request , _env , _ctx , handle ) => handle ( request ) ) ;
162225 const modern = makeMcpModernRequestRouter ( ) ;
163226 const ALLOWED_METHODS = new Set ( [ "GET" , "POST" , "DELETE" , "OPTIONS" ] ) ;
164227
165- return async ( request : Request , env : Env , ctx : ExecutionContext ) : Promise < Response > => {
228+ const handle = async ( request : Request , env : Env , ctx : ExecutionContext ) : Promise < Response > => {
166229 if ( request . method === "OPTIONS" ) {
167230 return mcpCorsPreflightResponse ( request . headers . get ( "access-control-request-headers" ) ) ;
168231 }
@@ -173,7 +236,7 @@ export const makeCloudMcpAgentHandler = () => {
173236 }
174237 const sessionId = request . headers . get ( "mcp-session-id" ) ;
175238
176- const { auth, outcome } = await runTraced ( request , authenticate ( request ) ) ;
239+ const { auth, outcome } = await runTraced ( request , authenticate ( request , authProvider ) ) ;
177240 if ( ! Predicate . isTagged ( outcome , "Authenticated" ) ) {
178241 // Destroying a live session on auth grounds requires a POSITIVE
179242 // determination that access is genuinely gone — only `Forbidden` carries
@@ -218,7 +281,7 @@ export const makeCloudMcpAgentHandler = () => {
218281 resource,
219282 props,
220283 requestStateSigningKey : requireMcpRequestStateKey ( env . MCP_REQUEST_STATE_KEY ) ,
221- builder : makeCloudModernMcpServerBuilder ( props . session ) ,
284+ builder : options . makeModernServerBuilder ( props . session ) ,
222285 sessions : env . MCP_SESSION ,
223286 executionOwners : mcpExecutionOwnerDirectoryFromNamespace ( env . MCP_EXECUTION_OWNER ) ,
224287 } ) ;
@@ -238,18 +301,20 @@ export const makeCloudMcpAgentHandler = () => {
238301 if ( sessionId && ! existingSession ) {
239302 return deadSessionResponse ( request . method , "Session not found" ) ;
240303 }
241- if ( existingSession ) {
304+ if ( existingSession && sessionId ) {
242305 const owner = await existingSession . validateMcpSessionOwner ( {
243306 accountId : outcome . principal . accountId ,
244307 organizationId : outcome . principal . organizationId ,
245308 } ) ;
246309 if ( owner === "not_found" ) {
310+ cacheDeadSession ( sessionId , "not_found" ) ;
247311 return deadSessionResponse ( request . method , "Session not found" ) ;
248312 }
249313 if ( owner === "terminated" ) {
250314 // DELETE-condemned but the deferred destroy alarm hasn't wiped storage
251315 // yet. Same envelope as the post-destroy race below: the client must
252316 // treat the id as dead and reconnect.
317+ cacheDeadSession ( sessionId , "timed_out" ) ;
253318 return deadSessionResponse ( request . method , "Session timed out, please reconnect" ) ;
254319 }
255320 if ( owner === "forbidden" ) {
@@ -286,11 +351,28 @@ export const makeCloudMcpAgentHandler = () => {
286351 // client to be told to reconnect, matching a timed-out session).
287352 // oxlint-disable-next-line executor/no-unknown-error-message -- adapter boundary: the abort reason is a plain runtime Error whose message IS the signal
288353 if ( Predicate . isError ( error ) && error . message === "destroyed" ) {
354+ if ( sessionId ) cacheDeadSession ( sessionId , "timed_out" ) ;
289355 return deadSessionResponse ( request . method , "Session timed out, please reconnect" ) ;
290356 }
291357 // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary: rethrow anything that isn't the condemned-DO abort to the Workers runtime unchanged
292358 throw error ;
293359 }
294360 return withMcpResponseHeaders ( wrapMcpSseResponse ( request , env , response ) ) ;
295361 } ;
362+
363+ return async ( request : Request , env : Env , ctx : ExecutionContext ) : Promise < Response > => {
364+ const sessionId = request . headers . get ( "mcp-session-id" ) ;
365+ const cacheEligible = request . method !== "OPTIONS" && ALLOWED_METHODS . has ( request . method ) ;
366+ if ( cacheEligible && sessionId && isDeadSessionCached ( sessionId ) ) {
367+ return Effect . runPromise (
368+ Effect . gen ( function * ( ) {
369+ const { auth, outcome } = yield * authenticate ( request , authProvider ) ;
370+ return Predicate . isTagged ( outcome , "Authenticated" )
371+ ? deadSessionResponse ( request . method , cachedDeadSessionMessage ( sessionId ) )
372+ : renderAuthError ( auth , request , outcome ) ;
373+ } ) . pipe ( Effect . withTracerEnabled ( false ) ) ,
374+ ) ;
375+ }
376+ return traceRequest ( request , env , ctx , ( tracedRequest ) => handle ( tracedRequest , env , ctx ) ) ;
377+ } ;
296378} ;
0 commit comments