Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/cloud/src/env-augment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ declare global {
MCP_RESOURCE_ORIGIN?: string;
MCP_SESSION_TIMEOUT_MS?: string;
MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS?: string;
/** HMAC key for MCP 2026-07-28 continuation state (32+ byte secret). */
MCP_REQUEST_STATE_KEY?: string;
NODE_ENV?: string;

// Shared with frontend
Expand Down
57 changes: 43 additions & 14 deletions apps/cloud/src/mcp/agent-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,29 @@ import {
type AuthOutcome,
type McpResource,
} from "@executor-js/host-mcp";
import { requestBodyFromRequest } from "@executor-js/host-mcp/tool-server-v2";
import {
currentPropagationHeaders,
readArtifactsEnabled,
readElicitationMode,
withVerifiedIdentityHeaders,
} from "@executor-js/cloudflare/mcp/do-headers";
import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";
import {
classifyMcpProtocolEra,
makeMcpModernRequestRouter,
mcpCorsPreflightResponse,
requireMcpRequestStateKey,
} from "@executor-js/cloudflare/mcp/modern-request-router";
import { mcpExecutionOwnerDirectoryFromNamespace } from "@executor-js/cloudflare/mcp/execution-owner-directory";
import { mcpSessionStub } from "@executor-js/cloudflare/mcp/session-stub";

import { wrapMcpSseResponse } from "../observability/memory-metrics";
import { WorkerTelemetryLive } from "../observability/telemetry";
import { cloudMcpAuth } from "./auth-provider";
import { McpSessionDOSqlite } from "./session-durable-object";
import { McpSessionDOSqlite, makeCloudModernMcpServerBuilder } from "./session-durable-object";
import { parseTraceparent } from "./traceparent";

const corsPreflightResponse = (): Response =>
new Response(null, {
status: 204,
headers: {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET, POST, DELETE, OPTIONS",
"access-control-allow-headers":
"content-type, authorization, mcp-session-id, accept, mcp-protocol-version",
"access-control-expose-headers": "mcp-session-id, WWW-Authenticate",
},
});

const jsonRpcResponse = (
status: number,
code: number,
Expand Down Expand Up @@ -141,6 +137,7 @@ const propsForPrincipal = (
});

export const makeCloudMcpAgentHandler = () => {
const modern = makeMcpModernRequestRouter();
const serveOptions = {
binding: "MCP_SESSION",
transport: "streamable-http",
Expand All @@ -158,7 +155,9 @@ export const makeCloudMcpAgentHandler = () => {
const ALLOWED_METHODS = new Set(["GET", "POST", "DELETE", "OPTIONS"]);

return async (request: Request, env: Env, ctx: ExecutionContext): Promise<Response> => {
if (request.method === "OPTIONS") return corsPreflightResponse();
if (request.method === "OPTIONS") {
return mcpCorsPreflightResponse(request.headers.get("access-control-request-headers"));
}
// The old envelope (packages/hosts/mcp/src/envelope.ts) answered anything
// outside GET/POST/DELETE/OPTIONS with a JSON-RPC 405; the agents SDK
// handler only understands its own transport verbs and falls through to
Expand Down Expand Up @@ -188,6 +187,36 @@ export const makeCloudMcpAgentHandler = () => {
return renderAuthError(auth, request, outcome);
}

const parsedBody = await Effect.runPromise(requestBodyFromRequest(request));
const era = await classifyMcpProtocolEra(request, parsedBody);
if (era === "modern") {
const resource = resourceFromPath(request);
const props = await runTraced(
request,
propsForPrincipal(request, outcome.principal, resource),
);
(ctx as ExecutionContext & { props?: McpSessionProps }).props = props;
const forwarded = withVerifiedIdentityHeaders(
request,
{
accountId: outcome.principal.accountId,
organizationId: outcome.principal.organizationId,
},
resource,
);
return modern.fetch({
request: forwarded,
parsedBody,
principal: outcome.principal,
resource,
props,
requestStateSigningKey: requireMcpRequestStateKey(env.MCP_REQUEST_STATE_KEY),
builder: makeCloudModernMcpServerBuilder(props.session),
sessions: env.MCP_SESSION,
executionOwners: mcpExecutionOwnerDirectoryFromNamespace(env.MCP_EXECUTION_OWNER),
});
}

if (!sessionId && request.method === "DELETE") {
// Matches the old envelope's contract (@modelcontextprotocol/sdk's
// `WebStandardStreamableHTTPServerTransport.handleDeleteRequest`): 200,
Expand Down
Loading
Loading