History

Serve MCP 2026-07-28 clients on the Cloudflare hosts

Rhys Sullivan committed 2 months ago

Commit 6c348fa1 parent 90ab90821 files · +1883 −126Browse files

Unified · Split

21 files
  1. apps/cloud/src/env-augment.d.ts +2 −0
  2. apps/cloud/src/mcp/agent-handler.ts +43 −14
  3. apps/cloud/src/mcp/session-durable-object.ts +166 −48
  4. apps/cloud/src/mcp/telemetry-modern.test.ts +77 −0
  5. apps/cloud/src/mcp/telemetry.ts +18 −1
  6. apps/cloud/wrangler.jsonc +4 −0
  7. apps/host-cloudflare/src/config.ts +2 −0
  8. apps/host-cloudflare/src/mcp/agent-handler.ts +39 −14
  9. apps/host-cloudflare/src/mcp/session-durable-object.ts +163 −13
  10. apps/host-cloudflare/wrangler.jsonc +5 −3
  11. bun.lock +1 −0
  12. packages/hosts/cloudflare/package.json +5 −0
  13. packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts +288 −19
  14. packages/hosts/cloudflare/src/mcp/agent-session-modern.test.ts +331 −0
  15. packages/hosts/cloudflare/src/mcp/do-headers.ts +16 −1
  16. packages/hosts/cloudflare/src/mcp/execution-owner-directory.ts +15 −0
  17. packages/hosts/cloudflare/src/mcp/modern-request-router.test.ts +381 −0
  18. packages/hosts/cloudflare/src/mcp/modern-request-router.ts +250 −0
  19. packages/hosts/cloudflare/src/mcp/session-stub.ts +23 −1
  20. packages/hosts/mcp/src/envelope.ts +2 −4
  21. packages/hosts/mcp/src/tool-server-v2.ts +52 −8
apps/cloud/src/env-augment.d.ts+2−0
@@ -67,6 +67,8 @@ declare global { · 66 unmodified lines
6767 MCP_RESOURCE_ORIGIN?: string;
6868 MCP_SESSION_TIMEOUT_MS?: string;
6969 MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS?: string;
70+ /** HMAC key for MCP 2026-07-28 continuation state (32+ byte secret). */
71+ MCP_REQUEST_STATE_KEY?: string;
7072 NODE_ENV?: string;
7173
7274 // Shared with frontend
apps/cloud/src/mcp/agent-handler.ts+43−14
@@ -9,6 +9,7 @@ import { · 8 unmodified lines
99 type AuthOutcome,
1010 type McpResource,
1111 } from "@executor-js/host-mcp";
12+import { requestBodyFromRequest } from "@executor-js/host-mcp/tool-server-v2";
1213 import {
1314 currentPropagationHeaders,
1415 readArtifactsEnabled,
@@ -16,26 +17,21 @@ import { · 1 unmodified line
1617 withVerifiedIdentityHeaders,
1718 } from "@executor-js/cloudflare/mcp/do-headers";
1819 import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";
20+import {
21+ classifyMcpProtocolEra,
22+ makeMcpModernRequestRouter,
23+ mcpCorsPreflightResponse,
24+ requireMcpRequestStateKey,
25+} from "@executor-js/cloudflare/mcp/modern-request-router";
26+import { mcpExecutionOwnerDirectoryFromNamespace } from "@executor-js/cloudflare/mcp/execution-owner-directory";
1927 import { mcpSessionStub } from "@executor-js/cloudflare/mcp/session-stub";
2028
2129 import { wrapMcpSseResponse } from "../observability/memory-metrics";
2230 import { WorkerTelemetryLive } from "../observability/telemetry";
2331 import { cloudMcpAuth } from "./auth-provider";
24−import { McpSessionDOSqlite } from "./session-durable-object";
32+import { McpSessionDOSqlite, makeCloudModernMcpServerBuilder } from "./session-durable-object";
2533 import { parseTraceparent } from "./traceparent";
2634
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−
3935 const jsonRpcResponse = (
4036 status: number,
4137 code: number,
@@ -141,6 +137,7 @@ const propsForPrincipal = ( · 99 unmodified lines
141137 });
142138
143139 export const makeCloudMcpAgentHandler = () => {
140+ const modern = makeMcpModernRequestRouter();
144141 const serveOptions = {
145142 binding: "MCP_SESSION",
146143 transport: "streamable-http",
@@ -158,7 +155,9 @@ export const makeCloudMcpAgentHandler = () => { · 11 unmodified lines
158155 const ALLOWED_METHODS = new Set(["GET", "POST", "DELETE", "OPTIONS"]);
159156
160157 return async (request: Request, env: Env, ctx: ExecutionContext): Promise<Response> => {
161− if (request.method === "OPTIONS") return corsPreflightResponse();
158+ if (request.method === "OPTIONS") {
159+ return mcpCorsPreflightResponse(request.headers.get("access-control-request-headers"));
160+ }
162161 // The old envelope (packages/hosts/mcp/src/envelope.ts) answered anything
163162 // outside GET/POST/DELETE/OPTIONS with a JSON-RPC 405; the agents SDK
164163 // handler only understands its own transport verbs and falls through to
@@ -188,6 +187,36 @@ export const makeCloudMcpAgentHandler = () => { · 23 unmodified lines
188187 return renderAuthError(auth, request, outcome);
189188 }
190189
190+ const parsedBody = await Effect.runPromise(requestBodyFromRequest(request));
191+ const era = await classifyMcpProtocolEra(request, parsedBody);
192+ if (era === "modern") {
193+ const resource = resourceFromPath(request);
194+ const props = await runTraced(
195+ request,
196+ propsForPrincipal(request, outcome.principal, resource),
197+ );
198+ (ctx as ExecutionContext & { props?: McpSessionProps }).props = props;
199+ const forwarded = withVerifiedIdentityHeaders(
200+ request,
201+ {
202+ accountId: outcome.principal.accountId,
203+ organizationId: outcome.principal.organizationId,
204+ },
205+ resource,
206+ );
207+ return modern.fetch({
208+ request: forwarded,
209+ parsedBody,
210+ principal: outcome.principal,
211+ resource,
212+ props,
213+ requestStateSigningKey: requireMcpRequestStateKey(env.MCP_REQUEST_STATE_KEY),
214+ builder: makeCloudModernMcpServerBuilder(props.session),
215+ sessions: env.MCP_SESSION,
216+ executionOwners: mcpExecutionOwnerDirectoryFromNamespace(env.MCP_EXECUTION_OWNER),
217+ });
218+ }
219+
191220 if (!sessionId && request.method === "DELETE") {
192221 // Matches the old envelope's contract (@modelcontextprotocol/sdk's
193222 // `WebStandardStreamableHTTPServerTransport.handleDeleteRequest`): 200,
apps/cloud/src/mcp/session-durable-object.ts+166−48
@@ -16,6 +16,7 @@ · 15 unmodified lines
1616 import { env } from "cloudflare:workers";
1717 import { Data, Effect, Layer } from "effect";
1818 import type { Cause } from "effect";
19+import type * as Tracer from "effect/Tracer";
1920 import * as OtelTracer from "@effect/opentelemetry/Tracer";
2021 import { drizzle } from "drizzle-orm/postgres-js";
2122 import postgres, { type Sql } from "postgres";
@@ -23,7 +24,11 @@ import postgres, { type Sql } from "postgres"; · 1 unmodified line
2324 import {
2425 PAUSED_APPROVAL_TIMEOUT_MS,
2526 createExecutorMcpServer,
27+ type PausedExecutionHooks,
28+ type ResumeFallbackOutcome,
2629 } from "@executor-js/host-mcp/tool-server";
30+import { buildMcpServerV2 } from "@executor-js/host-mcp/tool-server-v2";
31+import type { McpModernServerBuilder, Principal } from "@executor-js/host-mcp";
2732 import { buildResumeApprovalUrl } from "@executor-js/host-mcp/browser-approval";
2833 import { artifactUrlFor } from "@executor-js/host-mcp/create-artifact";
2934 import { makeAssetsShellHtmlLoader } from "@executor-js/mcp-apps-shell/worker";
@@ -31,18 +36,20 @@ import { smokeRenderArtifact } from "@executor-js/mcp-apps-shell/smoke-render"; · 1 unmodified line
3136 import {
3237 McpAgentSessionDOBase,
3338 type BuiltMcpServer,
39+ type BuiltModernMcpRuntime,
3440 type IncomingTraceHeaders,
3541 type McpApprovalOwner,
3642 type McpSessionModelResumeResult,
3743 type McpSessionInit,
3844 type SessionMeta,
3945 } from "@executor-js/cloudflare/mcp/agent-durable-object";
46+import { requireMcpRequestStateKey } from "@executor-js/cloudflare/mcp/modern-request-router";
4047 import {
4148 mcpExecutionOwnerDirectoryFromNamespace,
4249 type McpExecutionOwnerDirectory,
4350 type McpExecutionOwnerRoute,
4451 } from "@executor-js/cloudflare/mcp/execution-owner-directory";
45−import { mcpSessionStub } from "@executor-js/cloudflare/mcp/session-stub";
52+import { mcpSessionStubForOwner } from "@executor-js/cloudflare/mcp/session-stub";
4653 import { buildExecuteDescription, type ResumeResponse } from "@executor-js/execution";
4754
4855 // The DO meters executions just like the HTTP `/api/*` plane: it builds its
@@ -117,6 +124,10 @@ class McpModelResumeForwardError extends Data.TaggedError("McpModelResumeForward · 68 unmodified lines
117124 readonly cause: unknown;
118125 }> {}
119126
127+class CloudModernMcpBuildError extends Data.TaggedError("CloudModernMcpBuildError")<{
128+ readonly cause: unknown;
129+}> {}
130+
120131 /**
121132 * The DO keeps one postgres.js client for the MCP session runtime. postgres.js
122133 * closes idle sockets quickly, while the runtime object stays alive so the MCP
@@ -168,6 +179,127 @@ const loadAppShellHtml = makeAssetsShellHtmlLoader({ · 45 unmodified lines
168179 import("virtual:executor-mcp-apps-shell-dev-html").then((mod) => mod.devShellHtml),
169180 });
170181
182+const resolveCloudSessionMeta = (token: McpSessionInit, dbHandle: CloudSessionDbHandle) =>
183+ Effect.gen(function* () {
184+ const org = yield* resolveOrganization(token.organizationId);
185+ if (!org) {
186+ return yield* new OrganizationNotFoundError({ organizationId: token.organizationId });
187+ }
188+ return {
189+ organizationId: org.id,
190+ organizationName: org.name,
191+ organizationSlug: org.slug,
192+ userId: token.userId,
193+ resource: token.resource,
194+ elicitationMode: token.elicitationMode,
195+ artifactsEnabled: token.artifactsEnabled,
196+ webOrigin: token.webOrigin,
197+ } satisfies SessionMeta;
198+ }).pipe(Effect.provide(makeSessionServices(dbHandle)));
199+
200+const makeCloudExecutionRuntime = (sessionMeta: SessionMeta, dbHandle: CloudSessionDbHandle) =>
201+ Effect.gen(function* () {
202+ yield* Effect.promise(() => preloadQuickJs());
203+ const { executor, engine } = yield* makeExecutionStack(
204+ sessionMeta.userId,
205+ sessionMeta.organizationId,
206+ sessionMeta.organizationName,
207+ { mcpResource: sessionMeta.resource },
208+ ).pipe(
209+ Effect.provide(CloudMeteredExecutionStackLayer.pipe(Layer.provide(AutumnService.Default))),
210+ Effect.withSpan("McpSessionDOSqlite.makeExecutionStack"),
211+ );
212+ const description = yield* buildExecuteDescription(executor).pipe(
213+ Effect.withSpan("mcp.execute.description.build"),
214+ );
215+ return { executor, engine, description };
216+ }).pipe(Effect.provide(makeSessionServices(dbHandle)));
217+
218+type CloudExecutionRuntime = Effect.Success<ReturnType<typeof makeCloudExecutionRuntime>>;
219+
220+type CloudModernLifecycle = {
221+ readonly pausedExecutionHooks?: PausedExecutionHooks;
222+ readonly resumeFallback?: (
223+ executionId: string,
224+ response: ResumeResponse,
225+ ) => Effect.Effect<ResumeFallbackOutcome | null, unknown>;
226+ readonly parentSpan?: () => Tracer.AnySpan | undefined;
227+};
228+
229+const makeCloudModernRuntime = (
230+ sessionMeta: SessionMeta,
231+ runtime: CloudExecutionRuntime,
232+ lifecycle: CloudModernLifecycle = {},
233+): BuiltModernMcpRuntime => ({
234+ engine: runtime.engine,
235+ buildServer: (options) =>
236+ buildMcpServerV2({
237+ engine: runtime.engine,
238+ description: runtime.description,
239+ artifacts: runtime.executor.artifacts,
240+ connections: runtime.executor.connections,
241+ artifactsEnabled: sessionMeta.artifactsEnabled ?? true,
242+ loadAppShellHtml,
243+ smokeRenderArtifact,
244+ artifactUrl: artifactUrlFor(
245+ env.VITE_PUBLIC_SITE_URL ?? "https://executor.sh",
246+ sessionMeta.organizationSlug,
247+ ),
248+ debug: env.EXECUTOR_MCP_DEBUG === "true",
249+ elicitationMode: { mode: "native" },
250+ ...(lifecycle.parentSpan ? { parentSpan: lifecycle.parentSpan } : {}),
251+ ...(lifecycle.pausedExecutionHooks
252+ ? {
253+ pausedExecutionHooks: lifecycle.pausedExecutionHooks,
254+ pausedExecutionLeaseMs: PAUSED_APPROVAL_TIMEOUT_MS,
255+ }
256+ : {}),
257+ ...(lifecycle.resumeFallback ? { resumeFallback: lifecycle.resumeFallback } : {}),
258+ ...options,
259+ }),
260+});
261+
262+const closeModernServerWithDb = <Server extends { close: () => Promise<void> }>(
263+ server: Server,
264+ dbHandle: CloudSessionDbHandle,
265+): Server => {
266+ const closeServer = server.close.bind(server);
267+ server.close = () =>
268+ Effect.runPromise(
269+ Effect.promise(closeServer).pipe(Effect.ensuring(Effect.promise(() => dbHandle.end()))),
270+ );
271+ return server;
272+};
273+
274+/** Build one worker-side stateless SDK v2 server over a fresh cloud runtime. */
275+export const makeCloudModernMcpServerBuilder = (
276+ session: McpSessionInit,
277+): McpModernServerBuilder["Service"] => ({
278+ build: (principal: Principal, options) => {
279+ const dbHandle = makeEphemeralDb();
280+ const { resource, ...requestOptions } = options;
281+ const token: McpSessionInit = {
282+ ...session,
283+ userId: principal.accountId,
284+ organizationId: principal.organizationId,
285+ resource,
286+ };
287+ return resolveCloudSessionMeta(token, dbHandle).pipe(
288+ Effect.flatMap((sessionMeta) =>
289+ makeCloudExecutionRuntime(sessionMeta, dbHandle).pipe(
290+ Effect.map((runtime) => ({ runtime, sessionMeta })),
291+ ),
292+ ),
293+ Effect.flatMap(({ runtime, sessionMeta }) =>
294+ makeCloudModernRuntime(sessionMeta, runtime).buildServer(requestOptions),
295+ ),
296+ Effect.map((server) => closeModernServerWithDb(server, dbHandle)),
297+ Effect.tapCause(() => Effect.promise(() => dbHandle.end())),
298+ Effect.mapError((cause) => new CloudModernMcpBuildError({ cause })),
299+ );
300+ },
301+});
302+
171303 // ---------------------------------------------------------------------------
172304 // Durable Object
173305 // ---------------------------------------------------------------------------
@@ -195,7 +327,7 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD · 21 unmodified lines
195327 ): Effect.Effect<McpSessionModelResumeResult, unknown> {
196328 return Effect.tryPromise({
197329 try: () =>
198− mcpSessionStub(env.MCP_SESSION, owner.sessionId).resumeExecutionForModel(
330+ mcpSessionStubForOwner(env.MCP_SESSION, owner).resumeExecutionForModel(
199331 executionId,
200332 identity,
201333 response,
@@ -213,23 +345,8 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD · 11 unmodified lines
213345
214346 protected override resolveSessionMeta(token: McpSessionInit): Effect.Effect<SessionMeta> {
215347 const dbHandle = makeEphemeralDb();
216− return Effect.gen(function* () {
217− const org = yield* resolveOrganization(token.organizationId);
218− if (!org) {
219− return yield* new OrganizationNotFoundError({ organizationId: token.organizationId });
220− }
221− return {
222− organizationId: org.id,
223− organizationName: org.name,
224− organizationSlug: org.slug,
225− userId: token.userId,
226− resource: token.resource,
227− elicitationMode: token.elicitationMode,
228− artifactsEnabled: token.artifactsEnabled,
229− } satisfies SessionMeta;
230− }).pipe(
348+ return resolveCloudSessionMeta(token, dbHandle).pipe(
231349 Effect.withSpan("McpSessionDOSqlite.resolveSessionMeta"),
232− Effect.provide(makeSessionServices(dbHandle)),
233350 Effect.ensuring(Effect.promise(() => dbHandle.end())),
234351 // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: a vanished org is a defect; the worker already verified the bearer
235352 Effect.orDie,
@@ -242,34 +359,13 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD · 6 unmodified lines
242359 ): Effect.Effect<BuiltMcpServer> {
243360 const self = this;
244361 return Effect.gen(function* () {
245− // QuickJS-WASM must be loaded before anything asks for a sandbox: the
246− // default variant cannot fetch its own `.wasm` on Workers. Cloud runs
247− // user `execute` code on the dynamic-worker runtime, but the artifact
248− // smoke render is a QuickJS sandbox on every host — without this it fails
249− // open on each create and the check silently does nothing.
250− // Idempotent per isolate.
251− yield* Effect.promise(() => preloadQuickJs());
252− const { executor, engine } = yield* makeExecutionStack(
253− sessionMeta.userId,
254− sessionMeta.organizationId,
255− sessionMeta.organizationName,
256− { mcpResource: sessionMeta.resource },
257− ).pipe(
258− // The metered stack tracks each execution to Autumn. It requires
259− // `AutumnService | DbService`; `AutumnService.Default` is provided here
260− // (it only reads `env`, no further deps), and `DbService` flows from the
261− // outer `makeSessionServices`. When `AUTUMN_SECRET_KEY` is unset the
262− // billing service degrades to a no-op tracker, so this stays inert in
263− // cloud dev/preview environments that run without a billing backend.
264− Effect.provide(CloudMeteredExecutionStackLayer.pipe(Layer.provide(AutumnService.Default))),
265− Effect.withSpan("McpSessionDOSqlite.makeExecutionStack"),
266− );
267− // Build the description here so `executor.connections.list()` stays under
268− // the DO startup span and the MCP SDK receives a concrete string instead
269− // of invoking `engine.getDescription` across its async boundary.
270− const description = yield* buildExecuteDescription(executor).pipe(
271− Effect.withSpan("mcp.execute.description.build"),
272− );
362+ const runtime = yield* makeCloudExecutionRuntime(sessionMeta, dbHandle);
363+ const { executor, engine, description } = runtime;
364+ const modernRuntime = makeCloudModernRuntime(sessionMeta, runtime, {
365+ pausedExecutionHooks: self.modernPausedExecutionHooks,
366+ resumeFallback: self.modernModelResumeFallback,
367+ parentSpan: () => self.currentParentSpan(),
368+ });
273369 const sessionElicitationMode = sessionMeta.elicitationMode ?? "model";
274370 const mcpServer = yield* createExecutorMcpServer({
275371 engine,
@@ -310,15 +406,37 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD · 34 unmodified lines
310406 }
311407 : { mode: sessionElicitationMode },
312408 }).pipe(Effect.withSpan("McpSessionDOSqlite.createExecutorMcpServer"));
313− return { mcpServer, engine } satisfies BuiltMcpServer;
409+ return { mcpServer, engine, modernRuntime } satisfies BuiltMcpServer;
314410 }).pipe(
315411 Effect.withSpan("McpSessionDOSqlite.buildMcpServer"),
316− Effect.provide(makeSessionServices(dbHandle)),
317412 // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: runtime-build failures surface as the base's tapCause/cleanup defect
318413 Effect.orDie,
319414 );
320415 }
321416
417+ protected override buildModernMcpRuntime(
418+ sessionMeta: SessionMeta,
419+ dbHandle: CloudSessionDbHandle,
420+ ): Effect.Effect<BuiltModernMcpRuntime> {
421+ const self = this;
422+ return makeCloudExecutionRuntime(sessionMeta, dbHandle).pipe(
423+ Effect.map((runtime) =>
424+ makeCloudModernRuntime(sessionMeta, runtime, {
425+ pausedExecutionHooks: self.modernPausedExecutionHooks,
426+ resumeFallback: self.modernModelResumeFallback,
427+ parentSpan: () => self.currentParentSpan(),
428+ }),
429+ ),
430+ Effect.withSpan("McpSessionDOSqlite.buildModernMcpRuntime"),
431+ // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: runtime-build failures surface through the base RPC cleanup path
432+ Effect.orDie,
433+ );
434+ }
435+
436+ protected override modernRequestStateSigningKey(): string {
437+ return requireMcpRequestStateKey(env.MCP_REQUEST_STATE_KEY);
438+ }
439+
322440 protected override withTelemetry<A, E>(
323441 effect: Effect.Effect<A, E>,
324442 incoming?: IncomingTraceHeaders,
apps/cloud/src/mcp/telemetry-modern.test.ts+77−0
@@ -0,0 +1,77 @@
1+import { describe, expect, it } from "@effect/vitest";
2+import { Effect } from "effect";
3+import type * as Tracer from "effect/Tracer";
4+
5+import { annotateMcpRequest } from "./telemetry";
6+
7+const makeRecordingTracer = (): {
8+ readonly tracer: Tracer.Tracer;
9+ readonly requestAttributes: () => ReadonlyMap<string, unknown> | undefined;
10+} => {
11+ const recorded: Array<{
12+ readonly name: string;
13+ readonly attributes: Map<string, unknown>;
14+ }> = [];
15+ const tracer: Tracer.Tracer = {
16+ span: (options) => {
17+ const attributes = new Map<string, unknown>();
18+ recorded.push({ name: options.name, attributes });
19+ let status: Tracer.SpanStatus = { _tag: "Started", startTime: options.startTime };
20+ return {
21+ _tag: "Span",
22+ name: options.name,
23+ spanId: `span-${recorded.length}`,
24+ traceId: "trace-modern",
25+ parent: options.parent,
26+ annotations: options.annotations,
27+ get status() {
28+ return status;
29+ },
30+ attributes,
31+ links: options.links,
32+ sampled: options.sampled,
33+ kind: options.kind,
34+ end: (endTime, exit) => {
35+ status = { _tag: "Ended", startTime: options.startTime, endTime, exit };
36+ },
37+ attribute: (key, value) => {
38+ attributes.set(key, value);
39+ },
40+ event: () => undefined,
41+ addLinks: () => undefined,
42+ };
43+ },
44+ };
45+ return {
46+ tracer,
47+ requestAttributes: () => recorded.find(({ name }) => name === "mcp.request")?.attributes,
48+ };
49+};
50+
51+describe("annotateMcpRequest modern envelope", () => {
52+ it.effect("records the 2026 protocol version outside initialize", () => {
53+ const { tracer, requestAttributes } = makeRecordingTracer();
54+ const request = new Request("https://executor.sh/mcp", {
55+ method: "POST",
56+ headers: { "content-type": "application/json" },
57+ body: JSON.stringify({
58+ jsonrpc: "2.0",
59+ id: 1,
60+ method: "tools/list",
61+ params: {
62+ _meta: {
63+ "io.modelcontextprotocol/protocolVersion": "2026-07-28",
64+ "io.modelcontextprotocol/clientCapabilities": {},
65+ },
66+ },
67+ }),
68+ });
69+
70+ return Effect.gen(function* () {
71+ yield* annotateMcpRequest(request, { token: null, parseBody: true });
72+ const attributes = requestAttributes();
73+ expect(attributes?.get("mcp.rpc.method")).toBe("tools/list");
74+ expect(attributes?.get("mcp.client.protocol_version")).toBe("2026-07-28");
75+ }).pipe(Effect.withSpan("mcp.request"), Effect.withTracer(tracer));
76+ });
77+});
apps/cloud/src/mcp/telemetry.ts+18−1
@@ -102,6 +102,14 @@ const InitializeParams = Schema.Struct({ · 101 unmodified lines
102102 capabilities: Schema.optional(UnknownRecord),
103103 });
104104
105+const ModernEnvelopeParams = Schema.Struct({
106+ _meta: Schema.optional(
107+ Schema.Struct({
108+ "io.modelcontextprotocol/protocolVersion": Schema.optional(Schema.String),
109+ }),
110+ ),
111+});
112+
105113 const NamedParams = Schema.Struct({ name: Schema.optional(Schema.String) });
106114 const UriParams = Schema.Struct({ uri: Schema.optional(Schema.String) });
107115
@@ -119,6 +127,7 @@ const decodeJsonRpcEnvelopeString = Schema.decodeUnknownOption( · 11 unmodified lines
119127 Schema.fromJsonString(JsonRpcEnvelope),
120128 );
121129 const decodeInitializeParams = Schema.decodeUnknownOption(InitializeParams);
130+const decodeModernEnvelopeParams = Schema.decodeUnknownOption(ModernEnvelopeParams);
122131 const decodeNamedParams = Schema.decodeUnknownOption(NamedParams);
123132 const decodeUriParams = Schema.decodeUnknownOption(UriParams);
124133 const decodeCancelledParams = Schema.decodeUnknownOption(CancelledParams);
@@ -136,7 +145,14 @@ const readJsonRpcEnvelope = (request: Request): Effect.Effect<Option.Option<Json · 11 unmodified lines
136145
137146 const methodAttrs = (envelope: JsonRpcEnvelope): Record<string, unknown> => {
138147 const params = envelope.params ?? {};
139− return Match.value(envelope.method).pipe(
148+ const protocolAttrs = Option.match(decodeModernEnvelopeParams(params), {
149+ onNone: () => ({}),
150+ onSome: (modern) => {
151+ const protocolVersion = modern._meta?.["io.modelcontextprotocol/protocolVersion"];
152+ return protocolVersion ? { "mcp.client.protocol_version": protocolVersion } : {};
153+ },
154+ });
155+ const methodSpecific = Match.value(envelope.method).pipe(
140156 Match.when("initialize", () =>
141157 Option.match(decodeInitializeParams(params), {
142158 onNone: () => ({}) as Record<string, unknown>,
@@ -181,6 +197,7 @@ const methodAttrs = (envelope: JsonRpcEnvelope): Record<string, unknown> => { · 38 unmodified lines
181197 Match.option,
182198 Option.getOrElse(() => ({}) as Record<string, unknown>),
183199 );
200+ return { ...protocolAttrs, ...methodSpecific };
184201 };
185202
186203 const replyAttrs = (envelope: JsonRpcEnvelope): Record<string, unknown> => {
apps/cloud/wrangler.jsonc+4−0
@@ -96,6 +96,10 @@ · 95 unmodified lines
9696 "binding": "LOADER",
9797 },
9898 ],
99+ // DEPLOYMENT PREREQUISITE: MCP 2026-07-28 requestState is shared between
100+ // stateless Worker isolates and session DOs. Configure the same 32+ byte
101+ // secret for both with `wrangler secret put MCP_REQUEST_STATE_KEY`.
102+ // It must never be placed in `vars` or generated independently per isolate.
99103 "vars": {
100104 "VITE_PUBLIC_SITE_URL": "https://executor.sh",
101105 "VITE_PUBLIC_POSTHOG_KEY": "phc_nNLrNMALpRsfrEkZovUkfMxYbcJvHnsJHeoSPavprgLL",
apps/host-cloudflare/src/config.ts+2−0
@@ -47,6 +47,8 @@ export interface CloudflareEnv { · 46 unmodified lines
4747 readonly SELF_HOSTED_ORG_SLUG?: string;
4848 /** At-rest secret-encryption key (a `wrangler secret`, NOT a var). */
4949 readonly EXECUTOR_SECRET_KEY?: string;
50+ /** HMAC key for MCP 2026-07-28 continuation state (32+ byte secret). */
51+ readonly MCP_REQUEST_STATE_KEY?: string;
5052 readonly ALLOW_LOCAL_NETWORK?: string;
5153 readonly VITE_PUBLIC_SITE_URL?: string;
5254 /**
apps/host-cloudflare/src/mcp/agent-handler.ts+39−14
@@ -7,6 +7,7 @@ import { · 6 unmodified lines
77 type AuthOutcome,
88 type Principal,
99 } from "@executor-js/host-mcp";
10+import { requestBodyFromRequest } from "@executor-js/host-mcp/tool-server-v2";
1011 import {
1112 currentPropagationHeaders,
1213 readArtifactsEnabled,
@@ -14,23 +15,18 @@ import { · 1 unmodified line
1415 withVerifiedIdentityHeaders,
1516 } from "@executor-js/cloudflare/mcp/do-headers";
1617 import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";
18+import {
19+ classifyMcpProtocolEra,
20+ makeMcpModernRequestRouter,
21+ mcpCorsPreflightResponse,
22+ requireMcpRequestStateKey,
23+} from "@executor-js/cloudflare/mcp/modern-request-router";
24+import { mcpExecutionOwnerDirectoryFromNamespace } from "@executor-js/cloudflare/mcp/execution-owner-directory";
1725 import { mcpSessionStub } from "@executor-js/cloudflare/mcp/session-stub";
1826
1927 import type { CloudflareConfig, CloudflareEnv } from "../config";
2028 import { cloudflareAccessMcpAuth } from "./auth";
21−import { McpSessionDO } from "./session-durable-object";
22−
23−const corsPreflightResponse = (): Response =>
24− new Response(null, {
25− status: 204,
26− headers: {
27− "access-control-allow-origin": "*",
28− "access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
29− "access-control-allow-headers":
30− "content-type, authorization, mcp-session-id, accept, mcp-protocol-version",
31− "access-control-expose-headers": "mcp-session-id, WWW-Authenticate",
32− },
33− });
29+import { McpSessionDO, makeCloudflareModernMcpServerBuilder } from "./session-durable-object";
3430
3531 const jsonRpcResponse = (
3632 status: number,
@@ -91,13 +87,16 @@ const propsForPrincipal = ( · 54 unmodified lines
9187 });
9288
9389 export const makeCloudflareMcpAgentHandler = (config: CloudflareConfig) => {
90+ const modern = makeMcpModernRequestRouter();
9491 const serve = McpSessionDO.serve("/mcp", {
9592 binding: "MCP_SESSION",
9693 transport: "streamable-http",
9794 });
9895
9996 return async (request: Request, env: CloudflareEnv, ctx: ExecutionContext): Promise<Response> => {
100− if (request.method === "OPTIONS") return corsPreflightResponse();
97+ if (request.method === "OPTIONS") {
98+ return mcpCorsPreflightResponse(request.headers.get("access-control-request-headers"));
99+ }
101100 const sessionId = request.headers.get("mcp-session-id");
102101
103102 const { auth, outcome } = await Effect.runPromise(authenticate(request, config));
@@ -114,6 +113,32 @@ export const makeCloudflareMcpAgentHandler = (config: CloudflareConfig) => { · 10 unmodified lines
114113 return renderAuthError(auth, request, outcome);
115114 }
116115
116+ const parsedBody = await Effect.runPromise(requestBodyFromRequest(request));
117+ const era = await classifyMcpProtocolEra(request, parsedBody);
118+ if (era === "modern") {
119+ const props = await Effect.runPromise(propsForPrincipal(request, outcome.principal));
120+ (ctx as ExecutionContext & { props?: McpSessionProps }).props = props;
121+ const forwarded = withVerifiedIdentityHeaders(
122+ request,
123+ {
124+ accountId: outcome.principal.accountId,
125+ organizationId: outcome.principal.organizationId,
126+ },
127+ defaultMcpResource,
128+ );
129+ return modern.fetch({
130+ request: forwarded,
131+ parsedBody,
132+ principal: outcome.principal,
133+ resource: defaultMcpResource,
134+ props,
135+ requestStateSigningKey: requireMcpRequestStateKey(env.MCP_REQUEST_STATE_KEY),
136+ builder: makeCloudflareModernMcpServerBuilder(env, config, props.session),
137+ sessions: env.MCP_SESSION,
138+ executionOwners: mcpExecutionOwnerDirectoryFromNamespace(env.MCP_EXECUTION_OWNER),
139+ });
140+ }
141+
117142 if (!sessionId && request.method === "DELETE") {
118143 return new Response(null, { status: 204, headers: { "access-control-allow-origin": "*" } });
119144 }
apps/host-cloudflare/src/mcp/session-durable-object.ts+163−13
@@ -3,7 +3,11 @@ import { Data, Effect } from "effect"; · 2 unmodified lines
33 import {
44 PAUSED_APPROVAL_TIMEOUT_MS,
55 createExecutorMcpServer,
6+ type PausedExecutionHooks,
7+ type ResumeFallbackOutcome,
68 } from "@executor-js/host-mcp/tool-server";
9+import { buildMcpServerV2 } from "@executor-js/host-mcp/tool-server-v2";
10+import type { McpModernServerBuilder, Principal } from "@executor-js/host-mcp";
711 import { buildResumeApprovalUrl } from "@executor-js/host-mcp/browser-approval";
812 import { artifactUrlFor } from "@executor-js/host-mcp/create-artifact";
913 import { makeAssetsShellHtmlLoader } from "@executor-js/mcp-apps-shell/worker";
@@ -12,18 +16,20 @@ import type { ExecutorDbHandle } from "@executor-js/api/server"; · 2 unmodified lines
1216 import {
1317 McpAgentSessionDOBase,
1418 type BuiltMcpServer,
19+ type BuiltModernMcpRuntime,
1520 type McpApprovalOwner,
1621 type McpSessionModelResumeResult,
1722 type McpSessionInit,
1823 type SessionMeta,
1924 } from "@executor-js/cloudflare/mcp/agent-durable-object";
25+import { requireMcpRequestStateKey } from "@executor-js/cloudflare/mcp/modern-request-router";
2026 import {
2127 mcpExecutionOwnerDirectoryFromNamespace,
2228 type McpExecutionOwnerDirectory,
2329 type McpExecutionOwnerRoute,
2430 } from "@executor-js/cloudflare/mcp/execution-owner-directory";
25−import { mcpSessionStub } from "@executor-js/cloudflare/mcp/session-stub";
26−import type { ResumeResponse } from "@executor-js/execution";
31+import { mcpSessionStubForOwner } from "@executor-js/cloudflare/mcp/session-stub";
32+import { buildExecuteDescription, type ResumeResponse } from "@executor-js/execution";
2733
2834 import { loadConfig, type CloudflareConfig, type CloudflareEnv } from "../config";
2935 import { createD1ExecutorDb } from "../db/d1";
@@ -54,6 +60,124 @@ class McpModelResumeForwardError extends Data.TaggedError("McpModelResumeForward · 24 unmodified lines
5460 readonly cause: unknown;
5561 }> {}
5662
63+class CloudflareModernMcpBuildError extends Data.TaggedError("CloudflareModernMcpBuildError")<{
64+ readonly cause: unknown;
65+}> {}
66+
67+const makeCloudflareExecutionRuntime = (
68+ sessionMeta: SessionMeta,
69+ dbHandle: CfSessionDbHandle,
70+ config: CloudflareConfig,
71+) =>
72+ Effect.gen(function* () {
73+ yield* Effect.promise(() => preloadQuickJs());
74+ const { engine, executor } = yield* makeExecutionStack(
75+ sessionMeta.userId,
76+ sessionMeta.organizationId,
77+ sessionMeta.organizationName,
78+ { mcpResource: sessionMeta.resource },
79+ ).pipe(Effect.provide(makeCloudflareExecutionStackLayer(config, dbHandle)));
80+ const description = yield* buildExecuteDescription(executor);
81+ return { engine, executor, description };
82+ });
83+
84+type CloudflareExecutionRuntime = Effect.Success<ReturnType<typeof makeCloudflareExecutionRuntime>>;
85+
86+type CloudflareModernLifecycle = {
87+ readonly pausedExecutionHooks?: PausedExecutionHooks;
88+ readonly resumeFallback?: (
89+ executionId: string,
90+ response: ResumeResponse,
91+ ) => Effect.Effect<ResumeFallbackOutcome | null, unknown>;
92+};
93+
94+const makeCloudflareModernRuntime = (
95+ sessionMeta: SessionMeta,
96+ runtime: CloudflareExecutionRuntime,
97+ loadAppShellHtml: () => Promise<string>,
98+ config: CloudflareConfig,
99+ lifecycle: CloudflareModernLifecycle = {},
100+): BuiltModernMcpRuntime => {
101+ const artifactOrigin = sessionMeta.webOrigin ?? config.webBaseUrl;
102+ return {
103+ engine: runtime.engine,
104+ buildServer: (options) =>
105+ buildMcpServerV2({
106+ engine: runtime.engine,
107+ description: runtime.description,
108+ artifacts: runtime.executor.artifacts,
109+ connections: runtime.executor.connections,
110+ artifactsEnabled: sessionMeta.artifactsEnabled ?? true,
111+ loadAppShellHtml,
112+ smokeRenderArtifact,
113+ ...(artifactOrigin
114+ ? { artifactUrl: artifactUrlFor(artifactOrigin, sessionMeta.organizationSlug) }
115+ : {}),
116+ elicitationMode: { mode: "native" },
117+ ...(lifecycle.pausedExecutionHooks
118+ ? {
119+ pausedExecutionHooks: lifecycle.pausedExecutionHooks,
120+ pausedExecutionLeaseMs: PAUSED_APPROVAL_TIMEOUT_MS,
121+ }
122+ : {}),
123+ ...(lifecycle.resumeFallback ? { resumeFallback: lifecycle.resumeFallback } : {}),
124+ ...options,
125+ }),
126+ };
127+};
128+
129+const closeModernServerWithDb = <Server extends { close: () => Promise<void> }>(
130+ server: Server,
131+ dbHandle: CfSessionDbHandle,
132+): Server => {
133+ const closeServer = server.close.bind(server);
134+ server.close = () =>
135+ Effect.runPromise(
136+ Effect.promise(closeServer).pipe(Effect.ensuring(Effect.promise(() => dbHandle.end()))),
137+ );
138+ return server;
139+};
140+
141+/** Build the worker-side SDK v2 server over a fresh D1 execution runtime. */
142+export const makeCloudflareModernMcpServerBuilder = (
143+ env: CloudflareEnv,
144+ config: CloudflareConfig,
145+ session: McpSessionInit,
146+): McpModernServerBuilder["Service"] => ({
147+ build: (principal: Principal, options) =>
148+ Effect.promise(async () => {
149+ const handle = await createD1ExecutorDb(env.DB, env.BLOBS);
150+ return { ...handle, end: () => handle.close() } satisfies CfSessionDbHandle;
151+ }).pipe(
152+ Effect.flatMap((dbHandle) => {
153+ const { resource, ...requestOptions } = options;
154+ const sessionMeta: SessionMeta = {
155+ organizationId: principal.organizationId,
156+ organizationName: config.organizationName,
157+ organizationSlug: config.organizationSlug,
158+ userId: principal.accountId,
159+ resource,
160+ elicitationMode: session.elicitationMode,
161+ artifactsEnabled: session.artifactsEnabled,
162+ webOrigin: session.webOrigin,
163+ };
164+ return makeCloudflareExecutionRuntime(sessionMeta, dbHandle, config).pipe(
165+ Effect.flatMap((runtime) =>
166+ makeCloudflareModernRuntime(
167+ sessionMeta,
168+ runtime,
169+ makeAssetsShellHtmlLoader({ assets: env.ASSETS }),
170+ config,
171+ ).buildServer(requestOptions),
172+ ),
173+ Effect.map((server) => closeModernServerWithDb(server, dbHandle)),
174+ Effect.tapCause(() => Effect.promise(() => dbHandle.end())),
175+ Effect.mapError((cause) => new CloudflareModernMcpBuildError({ cause })),
176+ );
177+ }),
178+ ),
179+});
180+
57181 export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSessionDbHandle> {
58182 private readonly cfEnv: CloudflareEnv;
59183 private readonly cfConfig: CloudflareConfig;
@@ -88,7 +212,7 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession · 28 unmodified lines
88212 ): Effect.Effect<McpSessionModelResumeResult, unknown> {
89213 return Effect.tryPromise({
90214 try: () =>
91− mcpSessionStub(this.cfEnv.MCP_SESSION, owner.sessionId).resumeExecutionForModel(
215+ mcpSessionStubForOwner(this.cfEnv.MCP_SESSION, owner).resumeExecutionForModel(
92216 executionId,
93217 identity,
94218 response,
@@ -123,15 +247,18 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession · 28 unmodified lines
123247 const config = this.cfConfig;
124248 const self = this;
125249 return Effect.gen(function* () {
126− // QuickJS-WASM must be loaded before the executor layer builds it (the
127− // default variant can't fetch its .wasm on Workers). Idempotent per isolate.
128− yield* Effect.promise(() => preloadQuickJs());
129− const { engine, executor } = yield* makeExecutionStack(
130− sessionMeta.userId,
131− sessionMeta.organizationId,
132− sessionMeta.organizationName,
133− { mcpResource: sessionMeta.resource },
134− ).pipe(Effect.provide(makeCloudflareExecutionStackLayer(config, dbHandle)));
250+ const runtime = yield* makeCloudflareExecutionRuntime(sessionMeta, dbHandle, config);
251+ const { engine, executor, description } = runtime;
252+ const modernRuntime = makeCloudflareModernRuntime(
253+ sessionMeta,
254+ runtime,
255+ self.loadAppShellHtml,
256+ config,
257+ {
258+ pausedExecutionHooks: self.modernPausedExecutionHooks,
259+ resumeFallback: self.modernModelResumeFallback,
260+ },
261+ );
135262 // Browser elicitation mode (the base owns the approval store + the HTTP
136263 // approval RPCs): a gated execution pauses and returns an approvalUrl into
137264 // the console resume page. The URL origin is the create request's origin
@@ -143,6 +270,7 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession · 5 unmodified lines
143270 const artifactOrigin = sessionMeta.webOrigin ?? config.webBaseUrl;
144271 const mcpServer = yield* createExecutorMcpServer({
145272 engine,
273+ description,
146274 artifacts: executor.artifacts,
147275 connections: executor.connections,
148276 // Artifacts are on by default, opt-out per connection. A session
@@ -188,11 +316,33 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession · 39 unmodified lines
188316 }
189317 : { mode: elicitationMode },
190318 });
191− return { mcpServer, engine } satisfies BuiltMcpServer;
319+ return { mcpServer, engine, modernRuntime } satisfies BuiltMcpServer;
192320 }).pipe(
193321 Effect.withSpan("McpSessionDO.buildMcpServer"),
194322 // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: a runtime-build failure surfaces as the base's tapCause/cleanup defect
195323 Effect.orDie,
196324 );
197325 }
326+
327+ protected override buildModernMcpRuntime(
328+ sessionMeta: SessionMeta,
329+ dbHandle: CfSessionDbHandle,
330+ ): Effect.Effect<BuiltModernMcpRuntime> {
331+ const self = this;
332+ return makeCloudflareExecutionRuntime(sessionMeta, dbHandle, this.cfConfig).pipe(
333+ Effect.map((runtime) =>
334+ makeCloudflareModernRuntime(sessionMeta, runtime, self.loadAppShellHtml, self.cfConfig, {
335+ pausedExecutionHooks: self.modernPausedExecutionHooks,
336+ resumeFallback: self.modernModelResumeFallback,
337+ }),
338+ ),
339+ Effect.withSpan("McpSessionDO.buildModernMcpRuntime"),
340+ // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: runtime-build failures surface through the base RPC cleanup path
341+ Effect.orDie,
342+ );
343+ }
344+
345+ protected override modernRequestStateSigningKey(): string {
346+ return requireMcpRequestStateKey(this.cfEnv.MCP_REQUEST_STATE_KEY);
347+ }
198348 }
apps/host-cloudflare/wrangler.jsonc+5−3
@@ -67,9 +67,11 @@ · 66 unmodified lines
6767 ],
6868 // Cloudflare Access is the entire auth layer. ACCESS_TEAM_DOMAIN, ACCESS_AUD,
6969 // and ADMIN_EMAILS are installation-specific live vars, set after the first
70− // deploy and preserved by keep_vars. EXECUTOR_SECRET_KEY (the at-rest
71− // secret-encryption key) is a SECRET, set it with
72− // `wrangler secret put EXECUTOR_SECRET_KEY`, never in vars.
70+ // deploy and preserved by keep_vars. EXECUTOR_SECRET_KEY (at-rest encryption)
71+ // and MCP_REQUEST_STATE_KEY (MCP 2026-07-28 continuation signing, 32+ bytes)
72+ // are SECRETS, set with `wrangler secret put <NAME>`, never in vars. The
73+ // MCP key is a deployment prerequisite shared by Worker and session DOs;
74+ // never generate it independently per isolate.
7375 "vars": {
7476 "ACCESS_NAME_CLAIM": "name",
7577 "ACCESS_GROUPS_CLAIM": "groups",
bun.lock+1−0
@@ -685,6 +685,7 @@ · 684 unmodified lines
685685 "@executor-js/host-mcp": "workspace:*",
686686 "@executor-js/sdk": "workspace:*",
687687 "@modelcontextprotocol/sdk": "^1.29.0",
688+ "@modelcontextprotocol/server": "2.0.0",
688689 "agents": "^0.17.3",
689690 "effect": "catalog:",
690691 },
packages/hosts/cloudflare/package.json+5−0
@@ -23,6 +23,10 @@ · 22 unmodified lines
2323 "./mcp/session-stub": {
2424 "types": "./src/mcp/session-stub.ts",
2525 "default": "./src/mcp/session-stub.ts"
26+ },
27+ "./mcp/modern-request-router": {
28+ "types": "./src/mcp/modern-request-router.ts",
29+ "default": "./src/mcp/modern-request-router.ts"
2630 }
2731 },
2832 "scripts": {
@@ -36,6 +40,7 @@ · 7 unmodified lines
3640 "@executor-js/host-mcp": "workspace:*",
3741 "@executor-js/sdk": "workspace:*",
3842 "@modelcontextprotocol/sdk": "^1.29.0",
43+ "@modelcontextprotocol/server": "2.0.0",
3944 "agents": "^0.17.3",
4045 "effect": "catalog:"
4146 },
packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts+288−19
@@ -1,8 +1,14 @@
1−import { Cause, Deferred, Effect, Exit, Option, Schema } from "effect";
1+import { Cause, Data, Deferred, Effect, Exit, Option, Schema } from "effect";
22 import type * as Tracer from "effect/Tracer";
33 import type { Connection, ConnectionContext } from "agents";
44 import { McpAgent } from "agents/mcp";
55 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6+import {
7+ createMcpHandler,
8+ type McpHttpHandler,
9+ type McpRequestContext,
10+ type McpServer as ModernMcpServer,
11+} from "@modelcontextprotocol/server";
612
713 import { RequestOrgSlug, RequestWebOrigin } from "@executor-js/api/server";
814 import {
@@ -18,13 +24,28 @@ import { · 9 unmodified lines
1824 type PausedExecutionHooks,
1925 type ResumeFallbackOutcome,
2026 } from "@executor-js/host-mcp/tool-server";
21−import { defaultMcpResource, type McpResource } from "@executor-js/host-mcp";
27+import {
28+ defaultMcpResource,
29+ jsonRpcErrorBody,
30+ mcpResourceKey,
31+ type McpResource,
32+} from "@executor-js/host-mcp";
33+import {
34+ appsEnabledForClientCapabilities,
35+ clientCapabilitiesFromRequestBody,
36+ mcpRequestStatePrincipal,
37+} from "@executor-js/host-mcp/tool-server-v2";
2238
23−import type { IncomingPropagationHeaders, McpElicitationMode } from "./do-headers";
24−import type {
25− McpExecutionOwnerDirectory,
26− McpExecutionOwnerRecord,
27− McpExecutionOwnerRoute,
39+import {
40+ verifiedMcpRequestHeaders,
41+ type IncomingPropagationHeaders,
42+ type McpElicitationMode,
43+} from "./do-headers";
44+import {
45+ modernMcpExecutionOwnerRoute,
46+ type McpExecutionOwnerDirectory,
47+ type McpExecutionOwnerRecord,
48+ type McpExecutionOwnerRoute,
2849 } from "./execution-owner-directory";
2950 import {
3051 MAX_PAUSED_SESSION_IDLE_MS,
@@ -125,6 +146,23 @@ export interface SessionMeta { · 94 unmodified lines
125146 export interface BuiltMcpServer {
126147 readonly mcpServer: McpServer;
127148 readonly engine: ExecutionEngine<Cause.YieldableError>;
149+ /** Modern per-request server factory sharing this legacy runtime's engine. */
150+ readonly modernRuntime?: BuiltModernMcpRuntime;
151+}
152+
153+/** Request-specific inputs added to a DO-local SDK v2 server. */
154+export interface ModernMcpServerRequestOptions {
155+ readonly appsEnabled: boolean;
156+ readonly requestStateSigningKey: Uint8Array | string;
157+ readonly requestStatePrincipal: string;
158+}
159+
160+/** Long-lived DO execution runtime shared by per-request SDK v2 servers. */
161+export interface BuiltModernMcpRuntime {
162+ readonly engine: ExecutionEngine<Cause.YieldableError>;
163+ readonly buildServer: (
164+ options: ModernMcpServerRequestOptions,
165+ ) => Effect.Effect<ModernMcpServer, Cause.YieldableError>;
128166 }
129167
130168 export interface BrowserApprovalStore {
@@ -132,8 +170,15 @@ export interface BrowserApprovalStore { · 1 unmodified line
132170 readonly waitForResponse: (executionId: string) => Effect.Effect<ResumeResponse | null>;
133171 }
134172
173+type ModernRuntimeAccess =
174+ | { readonly status: "ok"; readonly runtime: BuiltModernMcpRuntime }
175+ | { readonly status: "forbidden" };
176+
177+class ModernMcpRuntimeNotConfigured extends Data.TaggedError("ModernMcpRuntimeNotConfigured") {}
178+
135179 const SESSION_META_KEY = "session-meta";
136180 const LAST_ACTIVITY_KEY = "last-activity-ms";
181+const MODERN_SESSION_KEY = "modern-session";
137182 const PARTYSERVER_NAME_KEY = "__ps_name";
138183 /** The agents SDK's durable "condemned" marker (`_cf_scheduleDestroy`). */
139184 const AGENTS_DESTROY_PENDING_KEY = "cf_agents_destroy_pending";
@@ -229,6 +274,12 @@ export abstract class McpAgentSessionDOBase< · 89 unmodified lines
229274 private engine: ExecutionEngine<Cause.YieldableError> | null = null;
230275 private dbHandle: TDbHandle | null = null;
231276 private sessionMeta: SessionMeta | null = null;
277+ private modernRuntime: BuiltModernMcpRuntime | null = null;
278+ private modernRuntimePromise: Promise<ModernRuntimeAccess> | null = null;
279+ private modernHandler: McpHttpHandler | null = null;
280+ private modernRunningRequestCount = 0;
281+ private modernRequestBodies = new WeakMap<Request, unknown>();
282+ private modernRequestPropagation = new WeakMap<Request, IncomingTraceHeaders | undefined>();
232283 private initialized = false;
233284 private onStartPromise: Promise<void> | null = null;
234285 private lastActivityMs = 0;
@@ -245,6 +296,20 @@ export abstract class McpAgentSessionDOBase< · 10 unmodified lines
245296 dbHandle: TDbHandle,
246297 ): Effect.Effect<BuiltMcpServer>;
247298
299+ /** Build the engine and per-request SDK v2 server factory for a modern-only DO. */
300+ protected buildModernMcpRuntime(
301+ _sessionMeta: SessionMeta,
302+ _dbHandle: TDbHandle,
303+ ): Effect.Effect<BuiltModernMcpRuntime, Cause.YieldableError> {
304+ return Effect.fail(new ModernMcpRuntimeNotConfigured());
305+ }
306+
307+ /** Read and validate the deployment-provided modern request-state signing key. */
308+ protected modernRequestStateSigningKey(): string {
309+ // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- composition boundary: subclasses serving modern MCP must provide a shared deployment key
310+ throw new Error("Modern MCP request-state signing is not configured");
311+ }
312+
248313 protected withTelemetry<A, E>(
249314 effect: Effect.Effect<A, E>,
250315 _incoming?: IncomingTraceHeaders,
@@ -293,6 +358,16 @@ export abstract class McpAgentSessionDOBase< · 42 unmodified lines
293358 return { sessionId: this.sessionId };
294359 }
295360
361+ private modernExecutionOwnerRoute(): McpExecutionOwnerRoute {
362+ return this.ctx.id.name
363+ ? this.executionOwnerRoute()
364+ : modernMcpExecutionOwnerRoute(this.ctx.id.toString());
365+ }
366+
367+ private runtimeOwnerId(): string {
368+ return this.ctx.id.name ? this.sessionId : this.modernExecutionOwnerRoute().sessionId;
369+ }
370+
296371 protected sameExecutionOwnerRoute(a: McpExecutionOwnerRoute, b: McpExecutionOwnerRoute): boolean {
297372 return a.sessionId === b.sessionId;
298373 }
@@ -320,6 +395,12 @@ export abstract class McpAgentSessionDOBase< · 21 unmodified lines
320395 ): Effect.Effect<ResumeFallbackOutcome | null> =>
321396 this.resumeFromExecutionOwnerDirectory(executionId, response);
322397
398+ protected readonly modernModelResumeFallback = (
399+ executionId: string,
400+ response: ResumeResponse,
401+ ): Effect.Effect<ResumeFallbackOutcome | null> =>
402+ this.resumeFromExecutionOwnerDirectory(executionId, response, this.modernExecutionOwnerRoute());
403+
323404 protected readonly pausedExecutionHooks: PausedExecutionHooks = {
324405 onExecutionPaused: (executionId, deadline) =>
325406 Effect.sync(() => {
@@ -329,6 +410,17 @@ export abstract class McpAgentSessionDOBase< · 3 unmodified lines
329410 onResumeSettled: (executionId) => this.finishPendingApprovalResume(executionId),
330411 };
331412
413+ /**
414+ * Modern pause hooks await the directory write before the `input_required`
415+ * result leaves the DO, so its signed continuation is immediately routable.
416+ */
417+ protected readonly modernPausedExecutionHooks: PausedExecutionHooks = {
418+ onExecutionPaused: (executionId, deadline) =>
419+ this.startPendingApprovalLease(executionId, deadline, this.modernExecutionOwnerRoute()),
420+ onResumeStarted: (executionId) => this.beginPendingApprovalResume(executionId),
421+ onResumeSettled: (executionId) => this.finishPendingApprovalResume(executionId),
422+ };
423+
332424 override async onConnect(conn: Connection, context: ConnectionContext): Promise<void> {
333425 const requestIds = readActivePostRequestIds(context.request);
334426 if (requestIds.length === 0) {
@@ -428,7 +520,7 @@ export abstract class McpAgentSessionDOBase< · 93 unmodified lines
428520 for (const requestIds of rows.values()) {
429521 if (Array.isArray(requestIds)) count += requestIds.length;
430522 }
431− return count;
523+ return count + this.modernRunningRequestCount;
432524 }
433525
434526 private closeActiveStreams(): void {
@@ -481,7 +573,7 @@ export abstract class McpAgentSessionDOBase< · 46 unmodified lines
481573 console.info(
482574 JSON.stringify({
483575 event: "mcp_session_idle_runtime_dispose",
484− sessionId: this.sessionId,
576+ sessionId: this.runtimeOwnerId(),
485577 idleMs: input.idleMs,
486578 pausedExecutionCount: input.pausedExecutionCount,
487579 }),
@@ -540,7 +632,7 @@ export abstract class McpAgentSessionDOBase< · 52 unmodified lines
540632 event: "mcp_execution_owner_directory_error",
541633 operation: input.operation,
542634 executionId: input.executionId,
543− sessionId: self.sessionId,
635+ sessionId: self.runtimeOwnerId(),
544636 exceptionType: first?.name ?? "Error",
545637 exceptionMessage: first?.message ?? "unknown",
546638 cause: Cause.pretty(input.cause),
@@ -565,7 +657,7 @@ export abstract class McpAgentSessionDOBase< · 18 unmodified lines
565657 JSON.stringify({
566658 event: "mcp_model_resume_forward_error",
567659 executionId: input.executionId,
568− sessionId: self.sessionId,
660+ sessionId: self.runtimeOwnerId(),
569661 ownerSessionId: input.owner.sessionId,
570662 exceptionType: first?.name ?? "Error",
571663 exceptionMessage: first?.message ?? "unknown",
@@ -591,7 +683,7 @@ export abstract class McpAgentSessionDOBase< · 19 unmodified lines
591683 event: "mcp_model_resume_forward_error",
592684 reason: "timeout",
593685 executionId: input.executionId,
594− sessionId: self.sessionId,
686+ sessionId: self.runtimeOwnerId(),
595687 ownerSessionId: input.owner.sessionId,
596688 timeoutMs: input.timeoutMs,
597689 }),
@@ -619,6 +711,120 @@ export abstract class McpAgentSessionDOBase< · 21 unmodified lines
619711 : built;
620712 }
621713
714+ private buildModernRuntime(sessionMeta: SessionMeta, dbHandle: TDbHandle) {
715+ const built = sessionMeta.organizationSlug
716+ ? this.buildModernMcpRuntime(sessionMeta, dbHandle).pipe(
717+ Effect.provideService(RequestOrgSlug, { slug: sessionMeta.organizationSlug }),
718+ )
719+ : this.buildModernMcpRuntime(sessionMeta, dbHandle);
720+ return sessionMeta.webOrigin
721+ ? built.pipe(Effect.provideService(RequestWebOrigin, { origin: sessionMeta.webOrigin }))
722+ : built;
723+ }
724+
725+ private modernPropsOwnSession(sessionMeta: SessionMeta, props: McpSessionProps): boolean {
726+ return (
727+ props.session.userId === sessionMeta.userId &&
728+ props.session.organizationId === sessionMeta.organizationId &&
729+ mcpResourceKey(props.session.resource) === mcpResourceKey(sessionMeta.resource)
730+ );
731+ }
732+
733+ private startModernRuntime(props: McpSessionProps): Promise<ModernRuntimeAccess> {
734+ if (this.modernRuntimePromise) return this.modernRuntimePromise;
735+
736+ const self = this;
737+ const program = Effect.gen(function* () {
738+ yield* self.prepareErrorCaptureScope();
739+ const stored = yield* self.loadSessionMeta();
740+ if (stored && !self.modernPropsOwnSession(stored, props)) {
741+ return { status: "forbidden" as const };
742+ }
743+ const sessionMeta = stored ?? (yield* self.resolveAndStoreSessionMeta(props.session));
744+ if (self.modernRuntime && self.engine) {
745+ yield* Effect.promise(() => self.markActivity());
746+ return { status: "ok" as const, runtime: self.modernRuntime };
747+ }
748+
749+ const dbHandle = self.dbHandle ?? (yield* self.openSessionDbHandle());
750+ self.dbHandle = dbHandle;
751+ const runtime = yield* self.buildModernRuntime(sessionMeta, dbHandle);
752+ self.modernRuntime = runtime;
753+ self.engine = runtime.engine;
754+ yield* Effect.promise(() =>
755+ Promise.all([self.ctx.storage.put(MODERN_SESSION_KEY, true), self.markActivity()]).then(
756+ () => undefined,
757+ ),
758+ );
759+ return { status: "ok" as const, runtime };
760+ }).pipe(
761+ Effect.tapCause((cause) =>
762+ Effect.gen(function* () {
763+ console.error("[mcp-session] modern runtime init failed:", Cause.pretty(cause));
764+ yield* self.captureCauseEffect(cause);
765+ yield* self.recordCauseOnSpan(cause);
766+ yield* self.closeRuntime();
767+ }),
768+ ),
769+ Effect.withSpan("McpSessionDO.startModernRuntime", {
770+ attributes: { "mcp.auth.organization_id": props.session.organizationId },
771+ }),
772+ (effect) => self.withTelemetry(effect, props.propagation),
773+ // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: Durable Object RPC methods can only reject their Promise
774+ Effect.orDie,
775+ (effect) => self.withSpanFlush(effect),
776+ );
777+
778+ const starting = Effect.runPromise(program);
779+ this.modernRuntimePromise = starting;
780+ starting.then(
781+ () => {
782+ if (this.modernRuntimePromise === starting) this.modernRuntimePromise = null;
783+ },
784+ () => {
785+ if (this.modernRuntimePromise === starting) this.modernRuntimePromise = null;
786+ },
787+ );
788+ return starting;
789+ }
790+
791+ private modernHandlerForRuntime(): McpHttpHandler {
792+ if (this.modernHandler) return this.modernHandler;
793+ const self = this;
794+ this.modernHandler = createMcpHandler(
795+ (context: McpRequestContext) => {
796+ const request = context.requestInfo;
797+ const runtime = self.modernRuntime;
798+ const sessionMeta = self.sessionMeta;
799+ if (!request || !runtime || !sessionMeta || !self.modernRequestBodies.has(request)) {
800+ // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: the third-party factory Promise has no typed failure channel; absent DO request context is an SDK defect
801+ return Effect.runPromise(Effect.die("Modern MCP Durable Object has no request runtime"));
802+ }
803+ const parsedBody = self.modernRequestBodies.get(request);
804+ const propagation = self.modernRequestPropagation.get(request);
805+ const capabilities = clientCapabilitiesFromRequestBody(parsedBody);
806+ return Effect.runPromise(
807+ runtime
808+ .buildServer({
809+ appsEnabled: appsEnabledForClientCapabilities(capabilities),
810+ requestStateSigningKey: self.modernRequestStateSigningKey(),
811+ requestStatePrincipal: mcpRequestStatePrincipal({
812+ accountId: sessionMeta.userId,
813+ organizationId: sessionMeta.organizationId,
814+ }),
815+ })
816+ .pipe(
817+ (effect) => self.withTelemetry(effect, propagation),
818+ // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: the third-party factory Promise can only reject
819+ Effect.orDie,
820+ ),
821+ );
822+ },
823+ { legacy: "reject" },
824+ );
825+ return this.modernHandler;
826+ }
827+
622828 private closeRuntime(options: { readonly closeStreams?: boolean } = {}): Effect.Effect<void> {
623829 const self = this;
624830 return Effect.gen(function* () {
@@ -631,8 +837,16 @@ export abstract class McpAgentSessionDOBase< · 6 unmodified lines
631837 delete (self as { server?: McpServer }).server;
632838 yield* Effect.promise(() => server.close()).pipe(Effect.ignore);
633839 }
840+ if (self.modernHandler) {
841+ const handler = self.modernHandler;
842+ self.modernHandler = null;
843+ yield* Effect.promise(() => handler.close()).pipe(Effect.ignore);
844+ }
634845 Reflect.set(self, "_transport", undefined);
635846 self.engine = null;
847+ self.modernRuntime = null;
848+ self.modernRequestBodies = new WeakMap<Request, unknown>();
849+ self.modernRequestPropagation = new WeakMap<Request, IncomingTraceHeaders | undefined>();
636850 if (self.dbHandle) {
637851 const dbHandle = self.dbHandle;
638852 self.dbHandle = null;
@@ -709,10 +923,11 @@ export abstract class McpAgentSessionDOBase< · 70 unmodified lines
709923 yield* self.prepareErrorCaptureScope();
710924 const sessionMeta = yield* self.resolveAndStoreSessionMeta(props.session);
711925 const dbHandle = yield* self.openSessionDbHandle();
712− const { mcpServer, engine } = yield* self.buildRuntime(sessionMeta, dbHandle);
926+ const { mcpServer, engine, modernRuntime } = yield* self.buildRuntime(sessionMeta, dbHandle);
713927 self.dbHandle = dbHandle;
714928 self.server = mcpServer;
715929 self.engine = engine;
930+ self.modernRuntime = modernRuntime ?? null;
716931 self.initialized = true;
717932 yield* Effect.promise(() => self.markActivity()).pipe(
718933 Effect.withSpan("McpSessionDO.markActivity"),
@@ -747,6 +962,50 @@ export abstract class McpAgentSessionDOBase< · 28 unmodified lines
747962 );
748963 }
749964
965+ /**
966+ * Serve one authenticated modern request without entering the legacy
967+ * `McpAgent` streamable-HTTP transport.
968+ */
969+ async serveModernMcp(
970+ request: Request,
971+ props: McpSessionProps,
972+ parsedBody: unknown,
973+ ): Promise<Response> {
974+ this.modernRequestStateSigningKey();
975+ const verified = verifiedMcpRequestHeaders(request);
976+ if (
977+ !verified ||
978+ verified.accountId !== props.session.userId ||
979+ verified.organizationId !== props.session.organizationId ||
980+ verified.resourceKey !== mcpResourceKey(props.session.resource)
981+ ) {
982+ return jsonRpcErrorBody(403, -32003, "Invalid MCP Durable Object identity", {
983+ cors: false,
984+ });
985+ }
986+ const access = await this.startModernRuntime(props);
987+ const sessionMeta = this.sessionMeta;
988+ if (
989+ access.status === "forbidden" ||
990+ !sessionMeta ||
991+ !this.modernPropsOwnSession(sessionMeta, props)
992+ ) {
993+ return jsonRpcErrorBody(403, -32003, "MCP session does not belong to the current bearer", {
994+ cors: false,
995+ });
996+ }
997+
998+ this.modernRequestBodies.set(request, parsedBody);
999+ this.modernRequestPropagation.set(request, props.propagation);
1000+ this.modernRunningRequestCount += 1;
1001+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary: the RPC must decrement its in-memory running lease on both handler resolution and rejection
1002+ try {
1003+ return await this.modernHandlerForRuntime().fetch(request, { parsedBody });
1004+ } finally {
1005+ this.modernRunningRequestCount = Math.max(0, this.modernRunningRequestCount - 1);
1006+ }
1007+ }
1008+
7501009 async validateMcpSessionOwner(
7511010 identity: McpApprovalOwner,
7521011 ): Promise<"ok" | "not_found" | "forbidden" | "terminated"> {
@@ -928,7 +1187,8 @@ export abstract class McpAgentSessionDOBase< · 175 unmodified lines
9281187 }
9291188
9301189 override async alarm(): Promise<void> {
931− if (!(await this.hasPartyServerName())) {
1190+ const isModernSession = (await this.ctx.storage.get<boolean>(MODERN_SESSION_KEY)) === true;
1191+ if (!isModernSession && !(await this.hasPartyServerName())) {
9321192 await this.cleanupUnaddressableSessionAlarm();
9331193 return;
9341194 }
@@ -947,15 +1207,21 @@ export abstract class McpAgentSessionDOBase< · 12 unmodified lines
9471207 });
9481208
9491209 if (decision.kind === "idle_within_timeout") {
1210+ if (isModernSession) {
1211+ await this.ctx.storage.setAlarm(Date.now() + Math.max(1, this.sessionTimeoutMs() - idleMs));
1212+ return;
1213+ }
9501214 await super.alarm();
9511215 return;
9521216 }
9531217
1218+ const ownerId = isModernSession ? this.modernExecutionOwnerRoute().sessionId : this.sessionId;
1219+
9541220 if (decision.kind === "extend_paused_lease") {
9551221 console.info(
9561222 JSON.stringify(
9571223 pausedLeaseExtensionLog({
958− sessionId: this.sessionId,
1224+ sessionId: ownerId,
9591225 pausedExecutionCount,
9601226 idleMs,
9611227 leaseMs: decision.leaseMs,
@@ -970,7 +1236,7 @@ export abstract class McpAgentSessionDOBase< · 8 unmodified lines
9701236 console.info(
9711237 JSON.stringify(
9721238 runningLeaseExtensionLog({
973− sessionId: this.sessionId,
1239+ sessionId: ownerId,
9741240 runningExecutionCount,
9751241 activeStreamCount,
9761242 idleMs,
@@ -1030,6 +1296,7 @@ export abstract class McpAgentSessionDOBase< · 53 unmodified lines
10301296 private writeExecutionOwnerEntry(
10311297 executionId: string,
10321298 deadline: PausedExecutionDeadline | undefined,
1299+ owner: McpExecutionOwnerRoute = this.executionOwnerRoute(),
10331300 ): Effect.Effect<void> {
10341301 const directory = this.executionOwnerDirectory();
10351302 if (!directory || !deadline) return Effect.void;
@@ -1039,7 +1306,7 @@ export abstract class McpAgentSessionDOBase< · 3 unmodified lines
10391306 if (!sessionMeta) return;
10401307 const record: McpExecutionOwnerRecord = {
10411308 executionId,
1042− owner: self.executionOwnerRoute(),
1309+ owner,
10431310 accountId: sessionMeta.userId,
10441311 organizationId: sessionMeta.organizationId,
10451312 expiresAt: deadline.expiresAt,
@@ -1082,6 +1349,7 @@ export abstract class McpAgentSessionDOBase< · 36 unmodified lines
10821349 private resumeFromExecutionOwnerDirectory(
10831350 executionId: string,
10841351 response: ResumeResponse,
1352+ currentOwner: McpExecutionOwnerRoute = this.executionOwnerRoute(),
10851353 ): Effect.Effect<ResumeFallbackOutcome | null> {
10861354 const directory = this.executionOwnerDirectory();
10871355 if (!directory) return Effect.succeed(null);
@@ -1108,7 +1376,7 @@ export abstract class McpAgentSessionDOBase< · 20 unmodified lines
11081376 return { status: "execution_forbidden" } as const;
11091377 }
11101378
1111− if (self.sameExecutionOwnerRoute(record.owner, self.executionOwnerRoute())) {
1379+ if (self.sameExecutionOwnerRoute(record.owner, currentOwner)) {
11121380 yield* self.deleteExecutionOwnerEntry(executionId);
11131381 return { status: "execution_expired", ttlMs: record.ttlMs } as const;
11141382 }
@@ -1155,6 +1423,7 @@ export abstract class McpAgentSessionDOBase< · 40 unmodified lines
11551423 private startPendingApprovalLease(
11561424 executionId: string,
11571425 deadline: PausedExecutionDeadline | undefined,
1426+ owner: McpExecutionOwnerRoute = this.executionOwnerRoute(),
11581427 ): Effect.Effect<void> {
11591428 const self = this;
11601429 return Effect.gen(function* () {
@@ -1177,7 +1446,7 @@ export abstract class McpAgentSessionDOBase< · 16 unmodified lines
11771446 self.queuePendingApprovalLeaseExpiration(executionId);
11781447 }, PAUSED_APPROVAL_TIMEOUT_MS);
11791448 self.pendingApprovalLeases.set(executionId, { disposeKeepAlive, timeout, expiring: false });
1180− yield* self.writeExecutionOwnerEntry(executionId, deadline);
1449+ yield* self.writeExecutionOwnerEntry(executionId, deadline, owner);
11811450 }).pipe(
11821451 Effect.withSpan("McpSessionDO.pending_approval_lease.start", {
11831452 attributes: { "mcp.execution.id": executionId },
packages/hosts/cloudflare/src/mcp/agent-session-modern.test.ts+331−0
@@ -0,0 +1,331 @@
1+import { describe, expect, it } from "@effect/vitest";
2+import { Cause, Effect } from "effect";
3+
4+import type {
5+ ExecutionEngine,
6+ ExecutionResult,
7+ PausedExecution,
8+ ResumeResponse,
9+} from "@executor-js/execution";
10+import { defaultMcpResource } from "@executor-js/host-mcp";
11+import { PAUSED_APPROVAL_TIMEOUT_MS } from "@executor-js/host-mcp/tool-server";
12+import { buildMcpServerV2 } from "@executor-js/host-mcp/tool-server-v2";
13+import { FormElicitation, ToolAddress } from "@executor-js/sdk";
14+
15+import {
16+ McpAgentSessionDOBase,
17+ type BuiltMcpServer,
18+ type BuiltModernMcpRuntime,
19+ type McpSessionInit,
20+ type McpSessionProps,
21+ type ModernMcpServerRequestOptions,
22+ type SessionMeta,
23+} from "./agent-session-durable-object";
24+import {
25+ modernMcpExecutionOwnerRoute,
26+ type McpExecutionOwnerDirectory,
27+ type McpExecutionOwnerRecord,
28+ type McpExecutionOwnerRoute,
29+} from "./execution-owner-directory";
30+
31+const REQUEST_STATE_KEY = "0123456789abcdef0123456789abcdef";
32+const EXECUTION_ID = "exec-modern-pause";
33+
34+class MemoryStorage {
35+ private readonly values = new Map<string, unknown>();
36+ alarm: number | undefined;
37+
38+ async get<T>(key: string): Promise<T | undefined> {
39+ return this.values.get(key) as T | undefined;
40+ }
41+
42+ async put(key: string, value: unknown): Promise<void> {
43+ this.values.set(key, value);
44+ }
45+
46+ async delete(key: string | readonly string[]): Promise<void> {
47+ if (typeof key === "string") {
48+ this.values.delete(key);
49+ return;
50+ }
51+ for (const entry of key) this.values.delete(entry);
52+ }
53+
54+ async list<T>(options: { readonly prefix?: string } = {}): Promise<Map<string, T>> {
55+ return new Map(
56+ Array.from(this.values.entries())
57+ .filter(([key]) => !options.prefix || key.startsWith(options.prefix))
58+ .map(([key, value]) => [key, value as T]),
59+ );
60+ }
61+
62+ async setAlarm(time: number | Date): Promise<void> {
63+ this.alarm = typeof time === "number" ? time : time.getTime();
64+ }
65+
66+ async deleteAlarm(): Promise<void> {
67+ this.alarm = undefined;
68+ }
69+}
70+
71+class MemoryContext {
72+ readonly storage = new MemoryStorage();
73+ readonly id = {
74+ name: undefined,
75+ toString: () => "modern-do-id",
76+ };
77+ readonly waitUntilPromises: Promise<unknown>[] = [];
78+
79+ waitUntil(promise: Promise<unknown>): void {
80+ this.waitUntilPromises.push(promise);
81+ }
82+}
83+
84+class MemoryDirectory implements McpExecutionOwnerDirectory {
85+ readonly records = new Map<string, McpExecutionOwnerRecord>();
86+
87+ put(record: McpExecutionOwnerRecord): Effect.Effect<void> {
88+ return Effect.sync(() => {
89+ this.records.set(record.executionId, record);
90+ });
91+ }
92+
93+ get(executionId: string): Effect.Effect<McpExecutionOwnerRecord | null> {
94+ return Effect.sync(() => this.records.get(executionId) ?? null);
95+ }
96+
97+ delete(executionId: string): Effect.Effect<void> {
98+ return Effect.sync(() => {
99+ this.records.delete(executionId);
100+ });
101+ }
102+}
103+
104+type Harness = {
105+ approvalResponses: Map<string, ResumeResponse>;
106+ approvalWaiters: Map<string, unknown>;
107+ beginPendingApprovalResume: (executionId: string) => Effect.Effect<void>;
108+ buildMcpServer: () => Effect.Effect<BuiltMcpServer>;
109+ buildModernMcpRuntime: () => Effect.Effect<BuiltModernMcpRuntime>;
110+ ctx: MemoryContext;
111+ dbHandle: { readonly end: () => void } | null;
112+ engine: ExecutionEngine<Cause.YieldableError> | null;
113+ executionOwnerDirectory: () => McpExecutionOwnerDirectory;
114+ finishPendingApprovalResume: (executionId: string) => Effect.Effect<void>;
115+ initialized: boolean;
116+ keepAlive: () => Promise<() => void>;
117+ lastActivityMs: number;
118+ modernHandler: null;
119+ modernPausedExecutionHooks: {
120+ readonly onExecutionPaused: (
121+ executionId: string,
122+ deadline: { readonly expiresAt: string; readonly ttlMs: number } | undefined,
123+ ) => Effect.Effect<void>;
124+ readonly onResumeStarted: (executionId: string) => Effect.Effect<void>;
125+ readonly onResumeSettled: (executionId: string) => Effect.Effect<void>;
126+ };
127+ modernRequestBodies: WeakMap<Request, unknown>;
128+ modernRequestPropagation: WeakMap<Request, undefined>;
129+ modernRequestStateSigningKey: () => string;
130+ modernRunningRequestCount: number;
131+ modernRuntime: BuiltModernMcpRuntime | null;
132+ modernRuntimePromise: Promise<unknown> | null;
133+ onStartPromise: Promise<void> | null;
134+ openSessionDb: () => { readonly end: () => void };
135+ pendingApprovalLeases: Map<string, unknown>;
136+ resolveSessionMeta: (token: McpSessionInit) => Effect.Effect<SessionMeta>;
137+ serveModernMcp: (
138+ request: Request,
139+ props: McpSessionProps,
140+ parsedBody: unknown,
141+ ) => Promise<Response>;
142+ server?: never;
143+ sessionMeta: SessionMeta | null;
144+ startPendingApprovalLease: (
145+ executionId: string,
146+ deadline: { readonly expiresAt: string; readonly ttlMs: number } | undefined,
147+ owner: McpExecutionOwnerRoute,
148+ ) => Effect.Effect<void>;
149+};
150+
151+const makeEngine = (): {
152+ readonly engine: ExecutionEngine<Cause.YieldableError>;
153+ readonly resumeCalls: ResumeResponse[];
154+} => {
155+ const paused = new Map<string, PausedExecution>();
156+ const resumeCalls: ResumeResponse[] = [];
157+ const execution: PausedExecution = {
158+ id: EXECUTION_ID,
159+ elicitationContext: {
160+ address: ToolAddress.make("tools.test.org.main.confirm"),
161+ args: {},
162+ request: FormElicitation.make({ message: "Confirm?", requestedSchema: {} }),
163+ },
164+ };
165+ const engine: ExecutionEngine<Cause.YieldableError> = {
166+ execute: () => Effect.succeed({ result: "unused" }),
167+ executeWithPause: () =>
168+ Effect.sync(() => {
169+ paused.set(execution.id, execution);
170+ return { status: "paused" as const, execution };
171+ }),
172+ resume: (executionId, response) =>
173+ Effect.sync((): ExecutionResult | null => {
174+ if (!paused.delete(executionId)) return null;
175+ resumeCalls.push(response);
176+ return { status: "completed", result: { result: response.content?.approved } };
177+ }),
178+ isExecutionSettled: () => Effect.succeed(false),
179+ getPausedExecution: (executionId) => Effect.sync(() => paused.get(executionId) ?? null),
180+ pausedExecutionCount: () => Effect.sync(() => paused.size),
181+ hasPausedExecutions: () => Effect.sync(() => paused.size > 0),
182+ getDescription: Effect.succeed("test engine"),
183+ };
184+ return { engine, resumeCalls };
185+};
186+
187+const makeHarness = () => {
188+ const ctx = new MemoryContext();
189+ const directory = new MemoryDirectory();
190+ const { engine, resumeCalls } = makeEngine();
191+ const session = Object.create(McpAgentSessionDOBase.prototype) as Harness;
192+ session.ctx = ctx;
193+ session.engine = null;
194+ session.dbHandle = null;
195+ session.sessionMeta = null;
196+ session.modernRuntime = null;
197+ session.modernRuntimePromise = null;
198+ session.modernHandler = null;
199+ session.modernRunningRequestCount = 0;
200+ session.modernRequestBodies = new WeakMap();
201+ session.modernRequestPropagation = new WeakMap();
202+ session.initialized = false;
203+ session.onStartPromise = null;
204+ session.lastActivityMs = 0;
205+ session.approvalResponses = new Map();
206+ session.approvalWaiters = new Map();
207+ session.pendingApprovalLeases = new Map();
208+ session.openSessionDb = () => ({ end: () => undefined });
209+ session.keepAlive = () => Promise.resolve(() => undefined);
210+ session.executionOwnerDirectory = () => directory;
211+ session.modernRequestStateSigningKey = () => REQUEST_STATE_KEY;
212+ session.resolveSessionMeta = (token) =>
213+ Effect.succeed({
214+ organizationId: token.organizationId,
215+ organizationName: "Test Org",
216+ userId: token.userId,
217+ resource: token.resource,
218+ elicitationMode: token.elicitationMode,
219+ artifactsEnabled: token.artifactsEnabled,
220+ webOrigin: token.webOrigin,
221+ });
222+ session.buildMcpServer = () => Effect.die("legacy build is not used by this harness");
223+ session.modernPausedExecutionHooks = {
224+ onExecutionPaused: (executionId, deadline) =>
225+ session.startPendingApprovalLease(
226+ executionId,
227+ deadline,
228+ modernMcpExecutionOwnerRoute(ctx.id.toString()),
229+ ),
230+ onResumeStarted: (executionId) => session.beginPendingApprovalResume(executionId),
231+ onResumeSettled: (executionId) => session.finishPendingApprovalResume(executionId),
232+ };
233+ session.buildModernMcpRuntime = () =>
234+ Effect.succeed({
235+ engine,
236+ buildServer: (options: ModernMcpServerRequestOptions) =>
237+ buildMcpServerV2({
238+ engine,
239+ elicitationMode: { mode: "native" },
240+ pausedExecutionHooks: session.modernPausedExecutionHooks,
241+ pausedExecutionLeaseMs: PAUSED_APPROVAL_TIMEOUT_MS,
242+ ...options,
243+ }),
244+ });
245+ return { session, directory, resumeCalls };
246+};
247+
248+const requestBody = (input?: { readonly requestState?: string }) => ({
249+ jsonrpc: "2.0",
250+ id: input?.requestState ? 2 : 1,
251+ method: "tools/call",
252+ params: {
253+ name: "execute",
254+ arguments: { code: "await tools.test.confirm()" },
255+ ...(input?.requestState
256+ ? {
257+ requestState: input.requestState,
258+ inputResponses: {
259+ elicitation: { action: "accept", content: { approved: true } },
260+ },
261+ }
262+ : {}),
263+ _meta: {
264+ "io.modelcontextprotocol/protocolVersion": "2026-07-28",
265+ "io.modelcontextprotocol/clientCapabilities": { elicitation: { form: {} } },
266+ },
267+ },
268+});
269+
270+const requestFor = (body: ReturnType<typeof requestBody>): Request =>
271+ new Request("https://executor.test/mcp", {
272+ method: "POST",
273+ headers: {
274+ "content-type": "application/json",
275+ "mcp-protocol-version": "2026-07-28",
276+ "mcp-method": "tools/call",
277+ "mcp-name": "execute",
278+ "x-executor-mcp-account-id": "acct_1",
279+ "x-executor-mcp-organization-id": "org_1",
280+ "x-executor-mcp-resource-key": "default",
281+ },
282+ body: JSON.stringify(body),
283+ });
284+
285+const isRecord = (value: unknown): value is Record<string, unknown> =>
286+ typeof value === "object" && value !== null && !Array.isArray(value);
287+
288+const requestStateFromResponse = (value: unknown): string | null => {
289+ if (!isRecord(value) || !isRecord(value.result)) return null;
290+ return typeof value.result.requestState === "string" ? value.result.requestState : null;
291+};
292+
293+describe("McpAgentSessionDOBase modern entry", () => {
294+ it("serves a pause, registers modern ownership, and resumes in the same DO", async () => {
295+ const { session, directory, resumeCalls } = makeHarness();
296+ const props: McpSessionProps = {
297+ session: {
298+ organizationId: "org_1",
299+ userId: "acct_1",
300+ elicitationMode: "native",
301+ resource: defaultMcpResource,
302+ webOrigin: "https://executor.test",
303+ },
304+ };
305+
306+ const firstBody = requestBody();
307+ const first = await session.serveModernMcp(requestFor(firstBody), props, firstBody);
308+ const firstPayload: unknown = await first.json();
309+ const requestState = requestStateFromResponse(firstPayload);
310+
311+ expect(first.status).toBe(200);
312+ expect(requestState).not.toBeNull();
313+ expect(directory.records.get(EXECUTION_ID)).toMatchObject({
314+ executionId: EXECUTION_ID,
315+ owner: { sessionId: "modern:modern-do-id" },
316+ accountId: "acct_1",
317+ organizationId: "org_1",
318+ ttlMs: PAUSED_APPROVAL_TIMEOUT_MS,
319+ });
320+ if (!requestState) return;
321+
322+ const secondBody = requestBody({ requestState });
323+ const second = await session.serveModernMcp(requestFor(secondBody), props, secondBody);
324+ const secondText = JSON.stringify(await second.json());
325+
326+ expect(second.status).toBe(200);
327+ expect(secondText).toContain("true");
328+ expect(resumeCalls).toEqual([{ action: "accept", content: { approved: true } }]);
329+ expect(directory.records.has(EXECUTION_ID)).toBe(false);
330+ });
331+});
packages/hosts/cloudflare/src/mcp/do-headers.ts+16−1
@@ -25,6 +25,21 @@ export type VerifiedTokenHeaders = { · 24 unmodified lines
2525 readonly organizationId: string;
2626 };
2727
28+/** Parsed worker-stamped identity and resource received by a session DO. */
29+export type VerifiedMcpRequestHeaders = VerifiedTokenHeaders & {
30+ readonly resourceKey: string;
31+};
32+
33+/** Parse the complete worker-stamped modern identity header set. */
34+export const verifiedMcpRequestHeaders = (request: Request): VerifiedMcpRequestHeaders | null => {
35+ const accountId = request.headers.get(INTERNAL_ACCOUNT_ID_HEADER);
36+ const organizationId = request.headers.get(INTERNAL_ORGANIZATION_ID_HEADER);
37+ const resourceKey = request.headers.get(INTERNAL_RESOURCE_KEY_HEADER);
38+ return accountId && organizationId && resourceKey
39+ ? { accountId, organizationId, resourceKey }
40+ : null;
41+};
42+
2843 // Worker and DO run in separate isolates with independent WebSdk tracer
2944 // providers. Neither one can see the other's OTEL context, so the DO used
3045 // to emit a brand-new root trace on every stub call. Ferry the worker span
@@ -89,7 +104,7 @@ export const withVerifiedIdentityHeaders = ( · 58 unmodified lines
89104 export const withMcpResponseHeaders = (response: Response): Response => {
90105 const headers = new Headers(response.headers);
91106 headers.set("access-control-allow-origin", "*");
92− headers.set("access-control-expose-headers", "mcp-session-id");
107+ headers.set("access-control-expose-headers", "mcp-session-id, mcp-protocol-version");
93108 return new Response(response.body, {
94109 status: response.status,
95110 statusText: response.statusText,
packages/hosts/cloudflare/src/mcp/execution-owner-directory.ts+15−0
@@ -5,6 +5,21 @@ export type McpExecutionOwnerRoute = { · 4 unmodified lines
55 readonly sessionId: string;
66 };
77
8+/** Prefix distinguishing a modern unique DO id from a legacy Agent session id. */
9+export const MODERN_MCP_EXECUTION_OWNER_PREFIX = "modern:";
10+
11+/** Encode a unique modern session DO id in the existing owner route slot. */
12+export const modernMcpExecutionOwnerRoute = (durableObjectId: string): McpExecutionOwnerRoute => ({
13+ sessionId: `${MODERN_MCP_EXECUTION_OWNER_PREFIX}${durableObjectId}`,
14+});
15+
16+/** Decode the unique DO id from a modern owner route, or return null for legacy owners. */
17+export const modernMcpDurableObjectId = (route: McpExecutionOwnerRoute): string | null => {
18+ if (!route.sessionId.startsWith(MODERN_MCP_EXECUTION_OWNER_PREFIX)) return null;
19+ const id = route.sessionId.slice(MODERN_MCP_EXECUTION_OWNER_PREFIX.length);
20+ return id.length > 0 ? id : null;
21+};
22+
823 export type McpExecutionOwnerRecord = {
924 readonly executionId: string;
1025 readonly owner: McpExecutionOwnerRoute;
packages/hosts/cloudflare/src/mcp/modern-request-router.test.ts+381−0
@@ -0,0 +1,381 @@
1+import { describe, expect, it } from "@effect/vitest";
2+import { createRequestStateCodec } from "@modelcontextprotocol/server";
3+import { Effect } from "effect";
4+
5+import type { ExecutionEngine } from "@executor-js/execution";
6+import {
7+ defaultMcpResource,
8+ type McpModernServerBuilder,
9+ type Principal,
10+} from "@executor-js/host-mcp";
11+import { buildMcpServerV2, mcpRequestStatePrincipal } from "@executor-js/host-mcp/tool-server-v2";
12+
13+import type { McpSessionProps } from "./agent-session-durable-object";
14+import {
15+ modernMcpExecutionOwnerRoute,
16+ type McpExecutionOwnerDirectory,
17+ type McpExecutionOwnerRecord,
18+} from "./execution-owner-directory";
19+import {
20+ classifyMcpProtocolEra,
21+ makeMcpModernRequestRouter,
22+ mcpCorsPreflightResponse,
23+ requireMcpRequestStateKey,
24+ type McpModernSessionNamespace,
25+ type McpModernSessionStub,
26+} from "./modern-request-router";
27+
28+const REQUEST_STATE_KEY = "0123456789abcdef0123456789abcdef";
29+
30+const principal: Principal = {
31+ accountId: "acct_1",
32+ organizationId: "org_1",
33+ organizationName: "Org 1",
34+ email: "user@example.test",
35+ name: "Test User",
36+ avatarUrl: null,
37+ roles: [],
38+};
39+
40+const props: McpSessionProps = {
41+ session: {
42+ organizationId: principal.organizationId,
43+ userId: principal.accountId,
44+ elicitationMode: "native",
45+ resource: defaultMcpResource,
46+ webOrigin: "https://executor.test",
47+ },
48+};
49+
50+const engine: ExecutionEngine = {
51+ execute: (code) => Effect.succeed({ result: code }),
52+ executeWithPause: (code) =>
53+ Effect.succeed({ status: "completed" as const, result: { result: code } }),
54+ resume: () => Effect.succeed(null),
55+ isExecutionSettled: () => Effect.succeed(false),
56+ getPausedExecution: () => Effect.succeed(null),
57+ pausedExecutionCount: () => Effect.succeed(0),
58+ hasPausedExecutions: () => Effect.succeed(false),
59+ getDescription: Effect.succeed("test engine"),
60+};
61+
62+const modernBody = (input: {
63+ readonly method: string;
64+ readonly name?: string;
65+ readonly arguments?: Record<string, unknown>;
66+ readonly requestState?: string;
67+}) => ({
68+ jsonrpc: "2.0",
69+ id: 1,
70+ method: input.method,
71+ params: {
72+ ...(input.name ? { name: input.name } : {}),
73+ ...(input.arguments ? { arguments: input.arguments } : {}),
74+ ...(input.requestState ? { requestState: input.requestState } : {}),
75+ _meta: {
76+ "io.modelcontextprotocol/protocolVersion": "2026-07-28",
77+ "io.modelcontextprotocol/clientCapabilities": {},
78+ },
79+ },
80+});
81+
82+const modernRequest = (body: ReturnType<typeof modernBody>): Request =>
83+ new Request("https://executor.test/mcp", {
84+ method: "POST",
85+ headers: {
86+ "content-type": "application/json",
87+ "mcp-protocol-version": "2026-07-28",
88+ "mcp-method": body.method,
89+ ...(typeof body.params.name === "string" ? { "mcp-name": body.params.name } : {}),
90+ },
91+ body: JSON.stringify(body),
92+ });
93+
94+class MemoryDirectory implements McpExecutionOwnerDirectory {
95+ readonly records = new Map<string, McpExecutionOwnerRecord>();
96+
97+ put(record: McpExecutionOwnerRecord): Effect.Effect<void> {
98+ return Effect.sync(() => {
99+ this.records.set(record.executionId, record);
100+ });
101+ }
102+
103+ get(executionId: string): Effect.Effect<McpExecutionOwnerRecord | null> {
104+ return Effect.sync(() => this.records.get(executionId) ?? null);
105+ }
106+
107+ delete(executionId: string): Effect.Effect<void> {
108+ return Effect.sync(() => {
109+ this.records.delete(executionId);
110+ });
111+ }
112+}
113+
114+type ForwardedRequest = {
115+ readonly id: string;
116+ readonly body: unknown;
117+};
118+
119+class MemorySessions implements McpModernSessionNamespace<string> {
120+ readonly forwarded: ForwardedRequest[] = [];
121+ uniqueIds = 0;
122+
123+ newUniqueId(): string {
124+ this.uniqueIds += 1;
125+ return `unique-${this.uniqueIds}`;
126+ }
127+
128+ idFromName(name: string): string {
129+ return `name:${name}`;
130+ }
131+
132+ idFromString(id: string): string {
133+ return `id:${id}`;
134+ }
135+
136+ get(id: string): McpModernSessionStub {
137+ return {
138+ serveModernMcp: async (_request, _props, parsedBody) => {
139+ this.forwarded.push({ id, body: parsedBody });
140+ return new Response(JSON.stringify({ id }), {
141+ headers: { "content-type": "application/json" },
142+ });
143+ },
144+ };
145+ }
146+}
147+
148+const makeBuilder = (builds: { count: number }): McpModernServerBuilder["Service"] => ({
149+ build: (_principal, options) => {
150+ builds.count += 1;
151+ const { resource: _resource, ...requestOptions } = options;
152+ return buildMcpServerV2({
153+ engine,
154+ elicitationMode: { mode: "native" },
155+ ...requestOptions,
156+ });
157+ },
158+});
159+
160+const dispatch = async (input: {
161+ readonly body: ReturnType<typeof modernBody>;
162+ readonly sessions: MemorySessions;
163+ readonly directory: MemoryDirectory;
164+ readonly builder: McpModernServerBuilder["Service"];
165+}) => {
166+ const request = modernRequest(input.body);
167+ return makeMcpModernRequestRouter().fetch({
168+ request,
169+ parsedBody: input.body,
170+ principal,
171+ resource: defaultMcpResource,
172+ props,
173+ requestStateSigningKey: REQUEST_STATE_KEY,
174+ builder: input.builder,
175+ sessions: input.sessions,
176+ executionOwners: input.directory,
177+ });
178+};
179+
180+const mintRequestState = async (executionId: string, ttlSeconds = 60): Promise<string> => {
181+ const binding = `tools/call\u0000${mcpRequestStatePrincipal(principal)}`;
182+ const codec = createRequestStateCodec<{ readonly executionId: string }>({
183+ key: REQUEST_STATE_KEY,
184+ ttlSeconds,
185+ bind: () => binding,
186+ });
187+ const encoded: unknown = await Reflect.apply(codec.mint, codec, [{ executionId }, {}]);
188+ return typeof encoded === "string" ? encoded : "";
189+};
190+
191+describe("modern Cloudflare MCP worker routing", () => {
192+ it("echoes dynamic preflight headers with a static modern fallback", () => {
193+ const requested = "content-type, authorization, mcp-param-search";
194+ expect(mcpCorsPreflightResponse(requested).headers.get("access-control-allow-headers")).toBe(
195+ requested,
196+ );
197+ expect(mcpCorsPreflightResponse().headers.get("access-control-allow-headers")).toContain(
198+ "mcp-method",
199+ );
200+ });
201+
202+ it("fails clearly when the shared modern signing secret is missing or short", () => {
203+ expect(() => requireMcpRequestStateKey(undefined)).toThrow("MCP_REQUEST_STATE_KEY");
204+ expect(() => requireMcpRequestStateKey("too-short")).toThrow("at least 32 bytes");
205+ expect(requireMcpRequestStateKey(REQUEST_STATE_KEY)).toBe(REQUEST_STATE_KEY);
206+ });
207+
208+ it("keeps the canonical legacy classification on the existing transport branch", async () => {
209+ const legacyBody = { jsonrpc: "2.0", id: 1, method: "tools/list", params: {} };
210+ const legacy = new Request("https://executor.test/mcp", {
211+ method: "POST",
212+ headers: { "content-type": "application/json" },
213+ body: JSON.stringify(legacyBody),
214+ });
215+ const modern = modernRequest(modernBody({ method: "tools/list" }));
216+
217+ await expect(classifyMcpProtocolEra(legacy, legacyBody)).resolves.toBe("legacy");
218+ await expect(
219+ classifyMcpProtocolEra(modern, modernBody({ method: "tools/list" })),
220+ ).resolves.toBe("modern");
221+ });
222+
223+ it("serves modern non-tools/call methods worker-side without touching a DO", async () => {
224+ const sessions = new MemorySessions();
225+ const builds = { count: 0 };
226+ const response = await dispatch({
227+ body: modernBody({ method: "tools/list" }),
228+ sessions,
229+ directory: new MemoryDirectory(),
230+ builder: makeBuilder(builds),
231+ });
232+
233+ expect(response.status).toBe(200);
234+ expect(builds.count).toBe(1);
235+ expect(sessions.uniqueIds).toBe(0);
236+ expect(sessions.forwarded).toEqual([]);
237+ });
238+
239+ it("forwards a fresh modern execute call to a new unique DO", async () => {
240+ const sessions = new MemorySessions();
241+ const builds = { count: 0 };
242+ const response = await dispatch({
243+ body: modernBody({
244+ method: "tools/call",
245+ name: "execute",
246+ arguments: { code: "1 + 1" },
247+ }),
248+ sessions,
249+ directory: new MemoryDirectory(),
250+ builder: makeBuilder(builds),
251+ });
252+
253+ expect(await response.json()).toEqual({ id: "unique-1" });
254+ expect(builds.count).toBe(0);
255+ expect(sessions.forwarded.map(({ id }) => id)).toEqual(["unique-1"]);
256+ });
257+
258+ it("forwards malformed modern tools/call requests to a new unique DO", async () => {
259+ const sessions = new MemorySessions();
260+ const builds = { count: 0 };
261+
262+ await dispatch({
263+ body: modernBody({ method: "tools/call" }),
264+ sessions,
265+ directory: new MemoryDirectory(),
266+ builder: makeBuilder(builds),
267+ });
268+
269+ expect(builds.count).toBe(0);
270+ expect(sessions.forwarded.map(({ id }) => id)).toEqual(["unique-1"]);
271+ });
272+
273+ it("verifies continuation state and forwards to its modern owner DO", async () => {
274+ const executionId = "exec-owned";
275+ const state = await mintRequestState(executionId);
276+ const directory = new MemoryDirectory();
277+ directory.records.set(executionId, {
278+ executionId,
279+ owner: modernMcpExecutionOwnerRoute("owner-do-id"),
280+ accountId: principal.accountId,
281+ organizationId: principal.organizationId,
282+ expiresAt: new Date(Date.now() + 60_000).toISOString(),
283+ ttlMs: 60_000,
284+ });
285+ const sessions = new MemorySessions();
286+ const builds = { count: 0 };
287+
288+ await dispatch({
289+ body: modernBody({
290+ method: "tools/call",
291+ name: "execute",
292+ arguments: { code: "1 + 1" },
293+ requestState: state,
294+ }),
295+ sessions,
296+ directory,
297+ builder: makeBuilder(builds),
298+ });
299+
300+ expect(builds.count).toBe(0);
301+ expect(sessions.uniqueIds).toBe(0);
302+ expect(sessions.forwarded.map(({ id }) => id)).toEqual(["id:owner-do-id"]);
303+ });
304+
305+ it("uses a fresh worker server for an unknown continuation owner", async () => {
306+ const state = await mintRequestState("exec-missing");
307+ const sessions = new MemorySessions();
308+ const response = await dispatch({
309+ body: modernBody({
310+ method: "tools/call",
311+ name: "execute",
312+ arguments: { code: "1 + 1" },
313+ requestState: state,
314+ }),
315+ sessions,
316+ directory: new MemoryDirectory(),
317+ builder: makeBuilder({ count: 0 }),
318+ });
319+ const body = await response.json();
320+
321+ expect(body).toMatchObject({
322+ result: { structuredContent: { status: "execution_not_found" } },
323+ });
324+ expect(sessions.uniqueIds).toBe(0);
325+ expect(sessions.forwarded).toEqual([]);
326+ });
327+
328+ it("routes modern resume calls to an existing legacy owner when recorded", async () => {
329+ const executionId = "exec-legacy";
330+ const directory = new MemoryDirectory();
331+ directory.records.set(executionId, {
332+ executionId,
333+ owner: { sessionId: "legacy-session" },
334+ accountId: principal.accountId,
335+ organizationId: principal.organizationId,
336+ expiresAt: new Date(Date.now() + 60_000).toISOString(),
337+ ttlMs: 60_000,
338+ });
339+ const sessions = new MemorySessions();
340+
341+ await dispatch({
342+ body: modernBody({
343+ method: "tools/call",
344+ name: "resume",
345+ arguments: { executionId, action: "accept" },
346+ }),
347+ sessions,
348+ directory,
349+ builder: makeBuilder({ count: 0 }),
350+ });
351+
352+ expect(sessions.forwarded.map(({ id }) => id)).toEqual(["name:streamable-http:legacy-session"]);
353+ });
354+
355+ it("rejects tampered and expired continuation state without touching a DO", async () => {
356+ const valid = await mintRequestState("exec-invalid");
357+ const middle = Math.floor(valid.length / 2);
358+ const tampered = `${valid.slice(0, middle)}${valid[middle] === "A" ? "B" : "A"}${valid.slice(middle + 1)}`;
359+ const expired = await mintRequestState("exec-expired", -1);
360+
361+ for (const requestState of [tampered, expired]) {
362+ const sessions = new MemorySessions();
363+ const response = await dispatch({
364+ body: modernBody({
365+ method: "tools/call",
366+ name: "execute",
367+ arguments: { code: "1 + 1" },
368+ requestState,
369+ }),
370+ sessions,
371+ directory: new MemoryDirectory(),
372+ builder: makeBuilder({ count: 0 }),
373+ });
374+
375+ const body = await response.json();
376+ expect(body).toMatchObject({ error: { code: -32602 } });
377+ expect(sessions.uniqueIds).toBe(0);
378+ expect(sessions.forwarded).toEqual([]);
379+ }
380+ });
381+});
packages/hosts/cloudflare/src/mcp/modern-request-router.ts+250−0
@@ -0,0 +1,250 @@
1+import { Effect, Exit, Option, Schema } from "effect";
2+import {
3+ createMcpHandler,
4+ isLegacyRequest,
5+ type McpHttpHandler,
6+ type McpRequestContext,
7+} from "@modelcontextprotocol/server";
8+
9+import {
10+ jsonRpcErrorBody,
11+ mcpResourceKey,
12+ type McpModernServerBuilder,
13+ type McpResource,
14+ type Principal,
15+} from "@executor-js/host-mcp";
16+import {
17+ appsEnabledForClientCapabilities,
18+ clientCapabilitiesFromRequestBody,
19+ mcpRequestStatePrincipal,
20+ verifyNativeRequestState,
21+} from "@executor-js/host-mcp/tool-server-v2";
22+
23+import type { McpSessionProps } from "./agent-session-durable-object";
24+import type { McpExecutionOwnerDirectory } from "./execution-owner-directory";
25+import { mcpSessionStubForOwner } from "./session-stub";
26+
27+const MCP_CORS_EXPOSED_HEADERS = "mcp-session-id, mcp-protocol-version, WWW-Authenticate";
28+const MCP_CORS_ALLOWED_HEADERS =
29+ "content-type, authorization, mcp-session-id, accept, mcp-protocol-version, mcp-method, mcp-name";
30+
31+const UnknownRecord = Schema.Record(Schema.String, Schema.Unknown);
32+const ModernToolsCallMethod = Schema.Struct({ method: Schema.Literal("tools/call") });
33+const ModernToolCall = Schema.Struct({
34+ method: Schema.Literal("tools/call"),
35+ params: Schema.Struct({
36+ name: Schema.String,
37+ arguments: Schema.optional(UnknownRecord),
38+ requestState: Schema.optional(Schema.String),
39+ }),
40+});
41+type ModernToolCall = typeof ModernToolCall.Type;
42+const decodeModernToolsCallMethod = Schema.decodeUnknownOption(ModernToolsCallMethod);
43+const decodeModernToolCall = Schema.decodeUnknownOption(ModernToolCall);
44+
45+interface ModernRequestInputs {
46+ readonly builder: McpModernServerBuilder["Service"];
47+ readonly parsedBody: unknown;
48+ readonly principal: Principal;
49+ readonly requestStateSigningKey: string;
50+}
51+
52+/** Durable Object namespace surface required by modern execution routing. */
53+export interface McpModernSessionNamespace<Id> {
54+ readonly newUniqueId: () => Id;
55+ readonly idFromName: (name: string) => Id;
56+ readonly idFromString: (id: string) => Id;
57+ readonly get: (id: Id) => unknown;
58+}
59+
60+/** Worker-callable modern RPC exposed by the MCP session Durable Object. */
61+export interface McpModernSessionStub {
62+ readonly serveModernMcp: (
63+ request: Request,
64+ props: McpSessionProps,
65+ parsedBody: unknown,
66+ ) => Promise<Response>;
67+}
68+
69+/** Inputs needed to dispatch one authenticated modern MCP request. */
70+export interface McpModernRequestDispatch<Id> {
71+ readonly request: Request;
72+ readonly parsedBody: unknown;
73+ readonly principal: Principal;
74+ readonly resource: McpResource;
75+ readonly props: McpSessionProps;
76+ readonly requestStateSigningKey: string;
77+ readonly builder: McpModernServerBuilder["Service"];
78+ readonly sessions: McpModernSessionNamespace<Id>;
79+ readonly executionOwners: McpExecutionOwnerDirectory | null;
80+}
81+
82+/** Resource-cached worker router for authenticated 2026-07-28 requests. */
83+export interface McpModernRequestRouter {
84+ readonly fetch: <Id>(input: McpModernRequestDispatch<Id>) => Promise<Response>;
85+ readonly close: () => Promise<void>;
86+}
87+
88+/** Validate the shared request-state secret at the first modern request boundary. */
89+export const requireMcpRequestStateKey = (value: string | undefined): string => {
90+ if (value !== undefined && new TextEncoder().encode(value).byteLength >= 32) return value;
91+ // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- composition boundary: modern MCP cannot safely serve or route continuation state without a deployment-provided HMAC key
92+ throw new Error(
93+ "MCP_REQUEST_STATE_KEY must be set to a secret of at least 32 bytes before serving MCP 2026-07-28 requests",
94+ );
95+};
96+
97+/** Build the MCP preflight response, echoing dynamic modern header names. */
98+export const mcpCorsPreflightResponse = (requestedHeaders?: string | null): Response =>
99+ new Response(null, {
100+ status: 204,
101+ headers: {
102+ "access-control-allow-origin": "*",
103+ "access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
104+ "access-control-allow-headers":
105+ requestedHeaders && requestedHeaders.trim() !== ""
106+ ? requestedHeaders
107+ : MCP_CORS_ALLOWED_HEADERS,
108+ "access-control-expose-headers": MCP_CORS_EXPOSED_HEADERS,
109+ },
110+ });
111+
112+/** Classify an already-parsed request with the SDK's canonical era predicate. */
113+export const classifyMcpProtocolEra = (
114+ request: Request,
115+ parsedBody: unknown,
116+): Promise<"legacy" | "modern"> =>
117+ isLegacyRequest(request, parsedBody).then((legacy) => (legacy ? "legacy" : "modern"));
118+
119+const withModernMcpCors = (response: Response): Response => {
120+ const headers = new Headers(response.headers);
121+ headers.set("access-control-allow-origin", "*");
122+ headers.set("access-control-expose-headers", MCP_CORS_EXPOSED_HEADERS);
123+ return new Response(response.body, {
124+ status: response.status,
125+ statusText: response.statusText,
126+ headers,
127+ });
128+};
129+
130+const toModernSessionStub = (stub: unknown): McpModernSessionStub =>
131+ // oxlint-disable-next-line executor/no-double-cast -- boundary: Workers generates the RPC surface from the bound Durable Object class, while the portable namespace type exposes unknown.
132+ stub as unknown as McpModernSessionStub;
133+
134+const stubForOwner = <Id>(
135+ sessions: McpModernSessionNamespace<Id>,
136+ owner: { readonly sessionId: string },
137+): McpModernSessionStub => toModernSessionStub(mcpSessionStubForOwner(sessions, owner));
138+
139+const freshStub = <Id>(sessions: McpModernSessionNamespace<Id>): McpModernSessionStub =>
140+ toModernSessionStub(sessions.get(sessions.newUniqueId()));
141+
142+const resumeExecutionId = (call: ModernToolCall): string | null => {
143+ if (call.params.name !== "resume") return null;
144+ const executionId = call.params.arguments?.executionId;
145+ return typeof executionId === "string" && executionId.length > 0 ? executionId : null;
146+};
147+
148+/** Build the shared worker-side modern handler and DO-affinity router. */
149+export const makeMcpModernRequestRouter = (): McpModernRequestRouter => {
150+ const handlers = new Map<string, McpHttpHandler>();
151+ const requestInputs = new WeakMap<Request, ModernRequestInputs>();
152+
153+ const handlerFor = (resource: McpResource): McpHttpHandler => {
154+ const resourceKey = mcpResourceKey(resource);
155+ const cached = handlers.get(resourceKey);
156+ if (cached) return cached;
157+
158+ const handler = createMcpHandler(
159+ (context: McpRequestContext) => {
160+ const request = context.requestInfo;
161+ const inputs = request ? requestInputs.get(request) : undefined;
162+ if (!request || !inputs) {
163+ // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: the third-party factory Promise has no typed failure channel; absent request context is an SDK defect
164+ return Effect.runPromise(Effect.die("Modern MCP request has no authenticated context"));
165+ }
166+ const capabilities = clientCapabilitiesFromRequestBody(inputs.parsedBody);
167+ return Effect.runPromise(
168+ inputs.builder.build(inputs.principal, {
169+ resource,
170+ appsEnabled: appsEnabledForClientCapabilities(capabilities),
171+ requestStateSigningKey: inputs.requestStateSigningKey,
172+ requestStatePrincipal: mcpRequestStatePrincipal(inputs.principal),
173+ }),
174+ );
175+ },
176+ { legacy: "reject" },
177+ );
178+ handlers.set(resourceKey, handler);
179+ return handler;
180+ };
181+
182+ const serveWorker = async <Id>(input: McpModernRequestDispatch<Id>): Promise<Response> => {
183+ requestInputs.set(input.request, {
184+ builder: input.builder,
185+ parsedBody: input.parsedBody,
186+ principal: input.principal,
187+ requestStateSigningKey: input.requestStateSigningKey,
188+ });
189+ return handlerFor(input.resource).fetch(input.request, { parsedBody: input.parsedBody });
190+ };
191+
192+ const serveDo = <Id>(
193+ stub: McpModernSessionStub,
194+ input: McpModernRequestDispatch<Id>,
195+ ): Promise<Response> => stub.serveModernMcp(input.request, input.props, input.parsedBody);
196+
197+ return {
198+ fetch: async (input) => {
199+ if (Option.isNone(decodeModernToolsCallMethod(input.parsedBody))) {
200+ return withModernMcpCors(await serveWorker(input));
201+ }
202+
203+ const decoded = decodeModernToolCall(input.parsedBody);
204+ if (Option.isNone(decoded)) {
205+ return withModernMcpCors(await serveDo(freshStub(input.sessions), input));
206+ }
207+
208+ const call = decoded.value;
209+ let executionId = resumeExecutionId(call);
210+ if (call.params.name === "execute" && call.params.requestState !== undefined) {
211+ const verified = await Effect.runPromiseExit(
212+ verifyNativeRequestState({
213+ state: call.params.requestState,
214+ method: call.method,
215+ requestStateSigningKey: input.requestStateSigningKey,
216+ requestStatePrincipal: mcpRequestStatePrincipal(input.principal),
217+ }),
218+ );
219+ if (Exit.isFailure(verified)) {
220+ return withModernMcpCors(await serveWorker(input));
221+ }
222+ executionId = verified.value.executionId;
223+ }
224+
225+ if (executionId === null) {
226+ return withModernMcpCors(await serveDo(freshStub(input.sessions), input));
227+ }
228+
229+ const owner = input.executionOwners
230+ ? await Effect.runPromise(input.executionOwners.get(executionId))
231+ : null;
232+ if (!owner) {
233+ return withModernMcpCors(await serveWorker(input));
234+ }
235+ if (
236+ owner.accountId !== input.principal.accountId ||
237+ owner.organizationId !== input.principal.organizationId
238+ ) {
239+ return withModernMcpCors(
240+ jsonRpcErrorBody(403, -32003, "MCP execution does not belong to the current bearer"),
241+ );
242+ }
243+ return withModernMcpCors(await serveDo(stubForOwner(input.sessions, owner.owner), input));
244+ },
245+ close: () =>
246+ Promise.all(Array.from(handlers.values(), (handler) => handler.close())).then(
247+ () => undefined,
248+ ),
249+ };
250+};
packages/hosts/cloudflare/src/mcp/session-stub.ts+23−1
@@ -7,13 +7,22 @@ import type { · 6 unmodified lines
77 McpSessionModelResumeResult,
88 McpSessionResumeApprovalResult,
99 } from "./agent-session-durable-object";
10−import { mcpSessionDurableObjectName } from "./execution-owner-directory";
10+import {
11+ modernMcpDurableObjectId,
12+ mcpSessionDurableObjectName,
13+ type McpExecutionOwnerRoute,
14+} from "./execution-owner-directory";
1115
1216 export interface McpSessionNamespace<Id> {
1317 readonly idFromName: (name: string) => Id;
1418 readonly get: (id: Id) => unknown;
1519 }
1620
21+/** Session namespace surface that can address both named legacy and unique modern DOs. */
22+export interface McpOwnerSessionNamespace<Id> extends McpSessionNamespace<Id> {
23+ readonly idFromString: (id: string) => Id;
24+}
25+
1726 export interface McpSessionStub {
1827 readonly validateMcpSessionOwner: (
1928 identity: McpApprovalOwner,
@@ -46,3 +55,16 @@ export const mcpSessionStub = <Id>( · 26 unmodified lines
4655 namespace.get(
4756 namespace.idFromName(mcpSessionDurableObjectName(sessionId)),
4857 ) as unknown as McpSessionStub;
58+
59+/** Resolve an execution owner route to its legacy named or modern unique DO. */
60+export const mcpSessionStubForOwner = <Id>(
61+ namespace: McpOwnerSessionNamespace<Id>,
62+ owner: McpExecutionOwnerRoute,
63+): McpSessionStub => {
64+ const modernId = modernMcpDurableObjectId(owner);
65+ const id = modernId
66+ ? namespace.idFromString(modernId)
67+ : namespace.idFromName(mcpSessionDurableObjectName(owner.sessionId));
68+ // oxlint-disable-next-line executor/no-double-cast -- boundary: Workers generates this RPC surface from the bound DO class.
69+ return namespace.get(id) as unknown as McpSessionStub;
70+};
packages/hosts/mcp/src/envelope.ts+2−4
@@ -22,6 +22,7 @@ import { · 21 unmodified lines
2222 import {
2323 appsEnabledForClientCapabilities,
2424 clientCapabilitiesFromRequest,
25+ mcpRequestStatePrincipal,
2526 requestBodyFromRequest,
2627 } from "./tool-server-v2";
2728
@@ -263,9 +264,6 @@ interface ModernMcpRouter { · 235 unmodified lines
263264 ) => Promise<Response>;
264265 }
265266
266−const requestStatePrincipal = (principal: Principal): string =>
267− `${principal.accountId}\u0000${principal.organizationId}`;
268−
269267 /** Build the resource-keyed, process-lifetime modern handler cache. */
270268 const makeModernMcpRouter = (): ModernMcpRouter => {
271269 const handlers = new Map<string, McpHttpHandler>();
@@ -295,7 +293,7 @@ const makeModernMcpRouter = (): ModernMcpRouter => { · 23 unmodified lines
295293 resource,
296294 appsEnabled: appsEnabledForClientCapabilities(clientCapabilities),
297295 requestStateSigningKey: getSigningKey(),
298− requestStatePrincipal: requestStatePrincipal(inputs.principal),
296+ requestStatePrincipal: mcpRequestStatePrincipal(inputs.principal),
299297 });
300298 }),
301299 );
packages/hosts/mcp/src/tool-server-v2.ts+52−8
@@ -8,7 +8,7 @@ · 7 unmodified lines
88 * {@link appsEnabledForClientCapabilities} decision. Legacy routing remains a
99 * separate host path.
1010 */
11−import { Effect, Match, Option, Schema } from "effect";
11+import { Data, Effect, Match, Option, Schema } from "effect";
1212 import * as Cause from "effect/Cause";
1313 import {
1414 acceptedContent,
@@ -48,9 +48,10 @@ import { · 33 unmodified lines
4848
4949 const NATIVE_ELICITATION_RESPONSE_KEY = "elicitation";
5050
51−const NativeRequestState = Schema.Struct({ executionId: Schema.String });
52−type NativeRequestState = typeof NativeRequestState.Type;
53−const decodeNativeRequestState = Schema.decodeUnknownOption(NativeRequestState);
51+const NativeRequestStateSchema = Schema.Struct({ executionId: Schema.String });
52+/** Verified payload carried by a modern native-elicitation continuation. */
53+export type NativeRequestState = typeof NativeRequestStateSchema.Type;
54+const decodeNativeRequestState = Schema.decodeUnknownOption(NativeRequestStateSchema);
5455
5556 type V2RequestContext = McpRequestJoinKeys & {
5657 readonly serverContext: ServerContext;
@@ -80,10 +81,53 @@ export const appsEnabledForClientCapabilities = ( · 23 unmodified lines
8081 clientCapabilities: McpAppsClientCapabilities | null | undefined,
8182 ): boolean => Boolean(getUiCapability(clientCapabilities)?.mimeTypes?.includes(RESOURCE_MIME_TYPE));
8283
84+/** Bind modern continuation state to the ownership identity used by MCP hosts. */
85+export const mcpRequestStatePrincipal = (principal: {
86+ readonly accountId: string;
87+ readonly organizationId: string;
88+}): string => `${principal.accountId}\u0000${principal.organizationId}`;
89+
90+const requestStateBinding = (method: string, principal: string): string =>
91+ `${method}\u0000${principal}`;
92+
93+/** Route-level failure verifying untrusted modern continuation state. */
94+export class McpRequestStateVerificationError extends Data.TaggedError(
95+ "McpRequestStateVerificationError",
96+)<{ readonly cause: unknown }> {}
97+
98+/**
99+ * Verify and parse a modern continuation before a stateless worker uses its
100+ * execution id for Durable Object routing.
101+ */
102+export const verifyNativeRequestState = (input: {
103+ readonly state: string;
104+ readonly method: string;
105+ readonly requestStateSigningKey: Uint8Array | string;
106+ readonly requestStatePrincipal: string;
107+}): Effect.Effect<NativeRequestState, McpRequestStateVerificationError> => {
108+ const codec = createRequestStateCodec<NativeRequestState>({
109+ key: input.requestStateSigningKey,
110+ bind: () => requestStateBinding(input.method, input.requestStatePrincipal),
111+ });
112+ return Effect.tryPromise({
113+ // The route-level verifier has no handler context. Its codec binding is a
114+ // closed value derived from the already-parsed method and principal, so the
115+ // SDK callback never observes this inert placeholder.
116+ try: async () => {
117+ const decoded: unknown = await Reflect.apply(codec.verify, codec, [input.state, null]);
118+ return Effect.runPromise(Schema.decodeUnknownEffect(NativeRequestStateSchema)(decoded));
119+ },
120+ catch: (cause) => new McpRequestStateVerificationError({ cause }),
121+ });
122+};
123+
83124 const isRecord = (value: unknown): value is Record<string, unknown> =>
84125 typeof value === "object" && value !== null && !Array.isArray(value);
85126
86−const clientCapabilitiesFromUnknown = (body: unknown): McpAppsClientCapabilities | null => {
127+/** Parse the MCP Apps capability subset from an already-decoded modern body. */
128+export const clientCapabilitiesFromRequestBody = (
129+ body: unknown,
130+): McpAppsClientCapabilities | null => {
87131 if (!isRecord(body)) return null;
88132 const params = body.params;
89133 if (!isRecord(params)) return null;
@@ -122,7 +166,7 @@ export const requestBodyFromRequest = (request: Request): Effect.Effect<unknown> · 32 unmodified lines
122166 export const clientCapabilitiesFromRequest = (
123167 request: Request,
124168 ): Effect.Effect<McpAppsClientCapabilities | null> =>
125− requestBodyFromRequest(request).pipe(Effect.map(clientCapabilitiesFromUnknown));
169+ requestBodyFromRequest(request).pipe(Effect.map(clientCapabilitiesFromRequestBody));
126170
127171 const requestJoinKeys = (context: ServerContext): V2RequestContext => ({
128172 requestId: context.mcpReq.id,
@@ -208,11 +252,11 @@ const createV2Assembly = <E extends Cause.YieldableError>( · 79 unmodified lines
208252 ...(config.requestStateTtlSeconds === undefined
209253 ? {}
210254 : { ttlSeconds: config.requestStateTtlSeconds }),
211− bind: (context) => `${context.mcpReq.method}\u0000${config.requestStatePrincipal}`,
255+ bind: (context) => requestStateBinding(context.mcpReq.method, config.requestStatePrincipal),
212256 });
213257 const verifyRequestState = async (state: string, context: ServerContext) => {
214258 const decoded = await requestStateCodec.verify(state, context);
215− return Effect.runPromise(Schema.decodeUnknownEffect(NativeRequestState)(decoded));
259+ return Effect.runPromise(Schema.decodeUnknownEffect(NativeRequestStateSchema)(decoded));
216260 };
217261 const server = new McpServer(
218262 { name: "executor", version: "1.0.0" },