@@ -4,38 +4,37 @@ import { Effect, Predicate } from "effect";
44import {
55 McpAuthProvider ,
66 jsonRpcErrorBody ,
7+ mcpModernDisabledResponse ,
78 defaultMcpResource ,
89 UNAVAILABLE_RETRY_AFTER_SECONDS ,
910 type AuthOutcome ,
1011 type McpResource ,
1112} from "@executor-js/host-mcp" ;
13+ import { requestBodyFromRequest } from "@executor-js/host-mcp/tool-server" ;
1214import {
1315 currentPropagationHeaders ,
1416 readArtifactsEnabled ,
1517 readElicitationMode ,
18+ withMcpResponseHeaders ,
19+ withPropagationHeaders ,
1620 withVerifiedIdentityHeaders ,
1721} from "@executor-js/cloudflare/mcp/do-headers" ;
1822import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object" ;
19- import { mcpSessionStub } from "@executor-js/cloudflare/mcp/session-stub" ;
23+ import {
24+ classifyMcpProtocolEra ,
25+ makeMcpModernRequestRouter ,
26+ mcpCorsPreflightResponse ,
27+ requireMcpRequestStateKey ,
28+ } from "@executor-js/cloudflare/mcp/modern-request-router" ;
29+ import { mcpExecutionOwnerDirectoryFromNamespace } from "@executor-js/cloudflare/mcp/execution-owner-directory" ;
30+ import { createMcpSessionStub , mcpSessionStub } from "@executor-js/cloudflare/mcp/session-stub" ;
2031
2132import { wrapMcpSseResponse } from "../observability/memory-metrics" ;
2233import { WorkerTelemetryLive } from "../observability/telemetry" ;
2334import { cloudMcpAuth } from "./auth-provider" ;
24- import { McpSessionDOSqlite } from "./session-durable-object" ;
35+ import { makeCloudModernMcpServerBuilder } from "./session-durable-object" ;
2536import { parseTraceparent } from "./traceparent" ;
2637
27- const corsPreflightResponse = ( ) : Response =>
28- new Response ( null , {
29- status : 204 ,
30- headers : {
31- "access-control-allow-origin" : "*" ,
32- "access-control-allow-methods" : "GET, POST, DELETE, OPTIONS" ,
33- "access-control-allow-headers" :
34- "content-type, authorization, mcp-session-id, accept, mcp-protocol-version" ,
35- "access-control-expose-headers" : "mcp-session-id, WWW-Authenticate" ,
36- } ,
37- } ) ;
38-
3938const jsonRpcResponse = (
4039 status : number ,
4140 code : number ,
@@ -86,7 +85,7 @@ const authenticate = (request: Request) =>
8685 return { auth, outcome } ;
8786 } ) . pipe ( Effect . provide ( cloudMcpAuth ) ) ;
8887
89- // The pre-Agents envelope ran the MCP auth path inside the Effect app, whose
88+ // The earlier shared envelope ran the MCP auth path inside the Effect app, whose
9089// HttpMiddleware provided the OTEL tracer — that is where the `mcp.request`
9190// span (client fingerprint, rpc method, auth outcome) exported from. This
9291// handler dispatches from the raw worker entry instead, so a bare
@@ -141,29 +140,15 @@ const propsForPrincipal = (
141140 } ) ;
142141
143142export const makeCloudMcpAgentHandler = ( ) => {
144- const serveOptions = {
145- binding : "MCP_SESSION" ,
146- transport : "streamable-http" ,
147- } as const ;
148- // The agents SDK builds an exact-match `URLPattern` from the path handed to
149- // `serve` (see `createStreamingHttpHandler` in `agents/dist/mcp/index.js`) —
150- // a single `/mcp` handler never matches `/mcp/toolkits/<slug>` and falls
151- // through to its own internal 404. A second `serve` mounted on the
152- // parameterized path picks it up (`URLPattern` supports `:slug` segments);
153- // the auth/ownership/props logic above is unchanged and shared, only the
154- // final dispatch target differs.
155- const serve = McpSessionDOSqlite . serve ( "/mcp" , serveOptions ) ;
156- const serveToolkit = McpSessionDOSqlite . serve ( "/mcp/toolkits/:slug" , serveOptions ) ;
157-
143+ const modern = makeMcpModernRequestRouter ( ) ;
158144 const ALLOWED_METHODS = new Set ( [ "GET" , "POST" , "DELETE" , "OPTIONS" ] ) ;
159145
160146 return async ( request : Request , env : Env , ctx : ExecutionContext ) : Promise < Response > => {
161- if ( request . method === "OPTIONS" ) return corsPreflightResponse ( ) ;
162- // The old envelope (packages/hosts/mcp/src/envelope.ts) answered anything
163- // outside GET/POST/DELETE/OPTIONS with a JSON-RPC 405; the agents SDK
164- // handler only understands its own transport verbs and falls through to
165- // a bare 404. Reject before authenticating so PUT/PATCH/etc never reach
166- // the session engine.
147+ if ( request . method === "OPTIONS" ) {
148+ return mcpCorsPreflightResponse ( request . headers . get ( "access-control-request-headers" ) ) ;
149+ }
150+ // Preserve the old envelope's JSON-RPC 405 before authenticating, so
151+ // unsupported methods never reach the session engine.
167152 if ( ! ALLOWED_METHODS . has ( request . method ) ) {
168153 return jsonRpcResponse ( 405 , - 32001 , "Method not allowed" ) ;
169154 }
@@ -177,17 +162,49 @@ export const makeCloudMcpAgentHandler = () => {
177162 // / JWKS failure) and `Unauthorized` (retry with a fresh token) must leave
178163 // the session intact, so the condemn path is gated on `Forbidden` alone.
179164 if ( Predicate . isTagged ( outcome , "Forbidden" ) && sessionId ) {
165+ const session = mcpSessionStub ( env . MCP_SESSION , sessionId ) ;
180166 await Effect . runPromise (
181167 Effect . ignore (
182- Effect . tryPromise ( ( ) =>
183- mcpSessionStub ( env . MCP_SESSION , sessionId ) . _cf_scheduleDestroy ( ) ,
184- ) ,
168+ session ? Effect . tryPromise ( ( ) => session . _cf_scheduleDestroy ( ) ) : Effect . void ,
185169 ) ,
186170 ) ;
187171 }
188172 return renderAuthError ( auth , request , outcome ) ;
189173 }
190174
175+ const parsedBody = await Effect . runPromise ( requestBodyFromRequest ( request ) ) ;
176+ const era = await classifyMcpProtocolEra ( request , parsedBody ) ;
177+ if ( era === "modern" ) {
178+ if ( env . MCP_2026_07_28_ENABLED === "false" ) {
179+ return mcpModernDisabledResponse ( ) ;
180+ }
181+ const resource = resourceFromPath ( request ) ;
182+ const props = await runTraced (
183+ request ,
184+ propsForPrincipal ( request , outcome . principal , resource ) ,
185+ ) ;
186+ ( ctx as ExecutionContext & { props ?: McpSessionProps } ) . props = props ;
187+ const forwarded = withVerifiedIdentityHeaders (
188+ request ,
189+ {
190+ accountId : outcome . principal . accountId ,
191+ organizationId : outcome . principal . organizationId ,
192+ } ,
193+ resource ,
194+ ) ;
195+ return modern . fetch ( {
196+ request : forwarded ,
197+ parsedBody,
198+ principal : outcome . principal ,
199+ resource,
200+ props,
201+ requestStateSigningKey : requireMcpRequestStateKey ( env . MCP_REQUEST_STATE_KEY ) ,
202+ builder : makeCloudModernMcpServerBuilder ( props . session ) ,
203+ sessions : env . MCP_SESSION ,
204+ executionOwners : mcpExecutionOwnerDirectoryFromNamespace ( env . MCP_EXECUTION_OWNER ) ,
205+ } ) ;
206+ }
207+
191208 if ( ! sessionId && request . method === "DELETE" ) {
192209 // Matches the old envelope's contract (@modelcontextprotocol/sdk's
193210 // `WebStandardStreamableHTTPServerTransport.handleDeleteRequest`): 200,
@@ -198,8 +215,12 @@ export const makeCloudMcpAgentHandler = () => {
198215 } ) ;
199216 }
200217
201- if ( sessionId ) {
202- const owner = await mcpSessionStub ( env . MCP_SESSION , sessionId ) . validateMcpSessionOwner ( {
218+ const existingSession = sessionId ? mcpSessionStub ( env . MCP_SESSION , sessionId ) : null ;
219+ if ( sessionId && ! existingSession ) {
220+ return jsonRpcResponse ( 404 , - 32001 , "Session not found" ) ;
221+ }
222+ if ( existingSession ) {
223+ const owner = await existingSession . validateMcpSessionOwner ( {
203224 accountId : outcome . principal . accountId ,
204225 organizationId : outcome . principal . organizationId ,
205226 } ) ;
@@ -218,27 +239,29 @@ export const makeCloudMcpAgentHandler = () => {
218239 }
219240
220241 const resource = resourceFromPath ( request ) ;
221- const props = await runTraced ( request , propsForPrincipal ( request , outcome . principal , resource ) ) ;
222- ( ctx as ExecutionContext & { props ?: McpSessionProps } ) . props = props ;
223- const forwarded = withVerifiedIdentityHeaders (
224- request ,
225- {
226- accountId : outcome . principal . accountId ,
227- organizationId : outcome . principal . organizationId ,
228- } ,
229- resource ,
242+ const propagation = await runTraced ( request , currentPropagationHeaders ( request ) ) ;
243+ const forwarded = withPropagationHeaders (
244+ withVerifiedIdentityHeaders (
245+ request ,
246+ {
247+ accountId : outcome . principal . accountId ,
248+ organizationId : outcome . principal . organizationId ,
249+ } ,
250+ resource ,
251+ ) ,
252+ propagation ,
230253 ) ;
231- const target = resource . kind === "toolkit" ? serveToolkit : serve ;
254+ const target = existingSession ?? createMcpSessionStub ( env . MCP_SESSION ) . stub ;
232255 let response : Response ;
233- // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary: the agents SDK aborts the isolate (throws) instead of returning a response for a condemned session
256+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary: a condemned DO abort can reject its direct fetch
234257 try {
235- response = await target . fetch ( forwarded , env , ctx ) ;
258+ response = await target . fetch ( forwarded ) ;
236259 } catch ( error ) {
237260 // `_cf_scheduleDestroy` (called above via DELETE) marks the DO
238- // condemned and schedules its alarm; the alarm's `destroy()` then
261+ // condemned and schedules its alarm; the alarm's storage wipe then
239262 // `ctx.abort("destroyed")`s the isolate. A request that lands after the
240263 // alarm has already fired — same DO, same tick budget as the DELETE in
241- // tests — throws that abort reason out of `serve .fetch` instead of the
264+ // tests — throws that abort reason out of `stub .fetch` instead of the
242265 // DO ever getting to answer. Map it to the old envelope's reconnect
243266 // error for a dead session (e2e/cloud/mcp-protocol.test.ts expects the
244267 // client to be told to reconnect, matching a timed-out session).
@@ -249,11 +272,6 @@ export const makeCloudMcpAgentHandler = () => {
249272 // 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
250273 throw error ;
251274 }
252- // The agents SDK answers a bare DELETE with 204; the old envelope's
253- // contract (see above) was 200 — rewrite for consistency.
254- if ( request . method === "DELETE" && response . status === 204 ) {
255- return new Response ( null , { status : 200 , headers : response . headers } ) ;
256- }
257- return wrapMcpSseResponse ( request , env , response ) ;
275+ return withMcpResponseHeaders ( wrapMcpSseResponse ( request , env , response ) ) ;
258276 } ;
259277} ;
0 commit comments