Skip to content

Commit 290dfbc

Browse files
committed
Fix cross-session MCP resume routing
1 parent 7e41e04 commit 290dfbc

11 files changed

Lines changed: 583 additions & 19 deletions

File tree

apps/cloud/src/mcp/agent-handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
withVerifiedIdentityHeaders,
1414
} from "@executor-js/cloudflare/mcp/do-headers";
1515
import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";
16+
import { mcpSessionDurableObjectName } from "@executor-js/cloudflare/mcp/execution-owner-directory";
1617

1718
import { wrapMcpSseResponse } from "../observability/memory-metrics";
1819
import { cloudMcpAuth } from "./auth-provider";
@@ -70,7 +71,7 @@ const renderAuthError = (
7071
const sessionStub = (env: Env, sessionId: string): McpAgentSessionStub =>
7172
// oxlint-disable-next-line executor/no-double-cast -- boundary: Workers types expose only DurableObjectStub, but RPC methods are generated from the bound DO class.
7273
env.MCP_SESSION.get(
73-
env.MCP_SESSION.idFromName(`streamable-http:${sessionId}`),
74+
env.MCP_SESSION.idFromName(mcpSessionDurableObjectName(sessionId)),
7475
) as unknown as McpAgentSessionStub;
7576

7677
const authenticate = (request: Request) =>

apps/cloud/src/mcp/session-durable-object.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
type SessionMeta,
3737
} from "@executor-js/cloudflare/mcp/agent-durable-object";
3838
import {
39+
mcpSessionDurableObjectName,
3940
mcpExecutionOwnerDirectoryFromNamespace,
4041
type McpExecutionOwnerDirectory,
4142
type McpExecutionOwnerRoute,
@@ -212,7 +213,7 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD
212213
try: () =>
213214
(
214215
env.MCP_SESSION.get(
215-
env.MCP_SESSION.idFromString(owner.sessionId),
216+
env.MCP_SESSION.idFromName(mcpSessionDurableObjectName(owner.sessionId)),
216217
) as unknown as McpModelResumeStub
217218
).resumeExecutionForModel(executionId, identity, response),
218219
catch: (cause) => new McpModelResumeForwardError({ cause }),

apps/host-cloudflare/src/mcp/agent-handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
withVerifiedIdentityHeaders,
1414
} from "@executor-js/cloudflare/mcp/do-headers";
1515
import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";
16+
import { mcpSessionDurableObjectName } from "@executor-js/cloudflare/mcp/execution-owner-directory";
1617

1718
import type { CloudflareConfig, CloudflareEnv } from "../config";
1819
import { cloudflareAccessMcpAuth } from "./auth";
@@ -70,7 +71,7 @@ const renderAuthError = (
7071
const sessionStub = (env: CloudflareEnv, sessionId: string): McpAgentSessionStub =>
7172
// oxlint-disable-next-line executor/no-double-cast -- boundary: Workers types expose only DurableObjectStub, but RPC methods are generated from the bound DO class.
7273
env.MCP_SESSION.get(
73-
env.MCP_SESSION.idFromName(`streamable-http:${sessionId}`),
74+
env.MCP_SESSION.idFromName(mcpSessionDurableObjectName(sessionId)),
7475
) as unknown as McpAgentSessionStub;
7576

7677
const authenticate = (request: Request, config: CloudflareConfig) =>

apps/host-cloudflare/src/mcp/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
McpSessionApprovalResult,
77
McpSessionResumeApprovalResult,
88
} from "@executor-js/cloudflare/mcp/agent-durable-object";
9+
import { mcpSessionDurableObjectName } from "@executor-js/cloudflare/mcp/execution-owner-directory";
910
import type { ResumeResponse } from "@executor-js/execution";
1011

1112
import type { CloudflareConfig, CloudflareEnv } from "../config";
@@ -41,7 +42,9 @@ export const makeCloudflareApprovalHandler = (
4142
): ((request: Request) => Promise<Response>) => {
4243
const { verify } = makeAccessVerifier(config);
4344
const stubFor = (sessionId: string): McpApprovalStub =>
44-
toApprovalStub(env.MCP_SESSION.get(env.MCP_SESSION.idFromName(`streamable-http:${sessionId}`)));
45+
toApprovalStub(
46+
env.MCP_SESSION.get(env.MCP_SESSION.idFromName(mcpSessionDurableObjectName(sessionId))),
47+
);
4548

4649
return async (request) => {
4750
const principal = await Effect.runPromise(verify(request));

apps/host-cloudflare/src/mcp/session-durable-object.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
type SessionMeta,
1616
} from "@executor-js/cloudflare/mcp/agent-durable-object";
1717
import {
18+
mcpSessionDurableObjectName,
1819
mcpExecutionOwnerDirectoryFromNamespace,
1920
type McpExecutionOwnerDirectory,
2021
type McpExecutionOwnerRoute,
@@ -85,7 +86,7 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession
8586
try: () =>
8687
(
8788
this.cfEnv.MCP_SESSION.get(
88-
this.cfEnv.MCP_SESSION.idFromName(`streamable-http:${owner.sessionId}`),
89+
this.cfEnv.MCP_SESSION.idFromName(mcpSessionDurableObjectName(owner.sessionId)),
8990
) as unknown as McpModelResumeStub
9091
).resumeExecutionForModel(executionId, identity, response),
9192
catch: (cause) => new McpModelResumeForwardError({ cause }),

packages/core/execution/src/engine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export const formatPausedExecution = (
196196
};
197197
};
198198

199-
const formatTtlDuration = (ttlMs: number): string => {
199+
export const formatTtlDuration = (ttlMs: number): string => {
200200
const seconds = Math.max(1, Math.round(ttlMs / 1000));
201201
if (seconds % 60 === 0) {
202202
const minutes = seconds / 60;

packages/core/execution/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export {
22
createExecutionEngine,
33
formatExecuteResult,
44
formatPausedExecution,
5+
formatTtlDuration,
56
type ExecutionEngine,
67
type ExecutionEngineConfig,
78
type ExecutionResult,

packages/hosts/cloudflare/src/mcp/agent-session-durable-object.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const MCP_HTTP_METHOD_HEADER = "cf-mcp-method";
120120
const MCP_MESSAGE_HEADER = "cf-mcp-message";
121121
const ACTIVE_POST_RESPONSE_WAIT_POLL_MS = 25;
122122
const ACTIVE_POST_RESPONSE_WAIT_MAX_MS = PAUSED_APPROVAL_TIMEOUT_MS + 30_000;
123+
const MODEL_RESUME_FORWARD_TIMEOUT_MS = 10_000;
123124
const approvalResponseKey = (executionId: string) => `approval-response:${executionId}`;
124125

125126
type JsonRpcRequestId = string | number;
@@ -889,6 +890,11 @@ export abstract class McpAgentSessionDOBase<
889890
const forwarded = yield* self
890891
.forwardModelResumeToOwner(record.owner, identity, executionId, response)
891892
.pipe(
893+
Effect.timeoutOrElse({
894+
duration: `${MODEL_RESUME_FORWARD_TIMEOUT_MS} millis`,
895+
orElse: () =>
896+
Effect.succeed({ status: "execution_expired" as const, ttlMs: record.ttlMs }),
897+
}),
892898
Effect.catchCause(() =>
893899
Effect.succeed({ status: "execution_expired" as const, ttlMs: record.ttlMs }),
894900
),

0 commit comments

Comments
 (0)