feat(cloudflare): hibernatable MCP Durable Object (with old impl removed)#1178
Conversation
Expose pausedExecutionCount and hasPausedExecutions on ExecutionEngine so Cloudflare MCP session DOs can distinguish idle cleanup from paused continuations.
… exist Add shared alarm policy so idle sessions still destroy after 5 minutes, but sessions with in-memory paused executions reschedule a 10-minute lease instead of tearing down the runtime. Preserves the legacy unaddressable-alarm guard.
…ptile) The extend_paused_lease branch rescheduled the alarm unconditionally, so a session whose paused execution was never resumed (browser tab closed, client disconnect) would extend the lease forever and keep the Durable Object alive and billable indefinitely. Bound the lease to a single approval window: once idle exceeds SESSION_TIMEOUT_MS + PAUSED_EXECUTION_LEASE_MS (15 min), the session is torn down regardless of outstanding paused work. By that point the browser approval wait (tool-server, 10 min) has already timed out, so the paused execution is no longer resumable. idleMs grows across extensions because activity is not marked during the wait, so decideSessionAlarm stays pure. Also add the missing coverage: idle_within_timeout wins regardless of paused work, and the new cap forces destroy_idle_session.
Add host-owned paused execution hooks so Cloudflare MCP sessions can hold a bounded pending-approval lease, keep active POST requests alive until their response drains, and release hibernation blockers on resume or timeout. Also bound the approval wait window and resolve OpenAPI-backed annotations with point lookups so large specs do not reset the Durable Object before returning a pause envelope. This is the upstream port of the core runtime pieces from aryasaatvik#56. The execution-history payload compaction from that PR is omitted because this upstream branch does not contain that plugin.
Remove MCP host console logging wrappers and rely on existing debug logging plus span annotations for pause lifecycle state. Keep Cloudflare Durable Object logs focused on exceptional boundary events by dropping happy-path active POST and pending approval lease messages.
Handle malformed active POST metadata without failing the Agent connection. Avoid overwriting a stored browser approval on timeout, and only resume timed-out executions directly when no MCP waiter is already registered.
…igration The hibernatable Agent-based MCP Durable Object now fully replaces the old bespoke session DO. This removes the superseded implementation instead of leaving it alongside the new one: - delete the old McpSessionDOBase + its worker transport and request-id queue (nothing extends it anymore; both hosts' concrete McpSessionDO already extend McpAgentSessionDOBase) - finish the cloud migration: cloud only kept the legacy McpServingRoutes session-store dispatch to publish its OAuth discovery docs. Make mcp.sessions optional and add a discovery-only McpDiscoveryRoutes layer so a host whose /mcp transport lives outside the envelope (the Agent bridge) can serve just the discovery docs. Cloud now wires only the auth seam. - drop the now-dead cloud session-store, seams, response-peek, and reporter wiring plus the stale package exports Net: the old impl and the half-migrated cloud envelope path are gone. Co-authored-by: Saatvik Arya <aryasaatvik@gmail.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | d335d83 | Commit Preview URL Branch Preview URL |
Jun 28 2026, 07:00 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | d335d83 | Jun 28 2026, 07:02 PM |
Brings the branch up to date with main (78 commits). Non-trivial integration: main added toolkit-scoped MCP (`/mcp/toolkits/<slug>`, an `McpResource` threaded through the session flow) while this branch replaced the old session DO with the hibernatable Agent DO. Ported `resource` onto the new `McpAgentSessionDOBase` (McpSessionInit + SessionMeta) and threaded it through both agent handlers: host-cloudflare always serves the default resource (only `/mcp` routes to the bridge), cloud derives the toolkit resource from the request path. Kept the four old-impl files deleted (main had modified them; no new consumers).
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
Cloudflare previewTorn down — the PR is closed. |
Greptile SummaryThis PR replaces the previous bespoke
Confidence Score: 5/5This PR is safe to merge. It replaces a hand-rolled DO transport layer with the platform-native hibernatable McpAgent, results in a net deletion of ~573 lines, and all typechecks and e2e tests are verified clean. The architecture change is well-bounded: the new No files require special attention for merge readiness. The Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant C as MCP Client
participant W as Worker (server.ts / worker.ts)
participant AH as AgentHandler
participant DO as McpSessionDO (McpAgent)
participant B as Browser Approval UI
C->>W: POST /mcp (Bearer token)
W->>AH: "classifyMcpPath -> kind=mcp"
AH->>AH: authenticate(request)
alt Unauthenticated
AH-->>C: 401 + WWW-Authenticate challenge
end
AH->>AH: validateMcpSessionOwner (if session-id present)
AH->>AH: propsForPrincipal (session init props)
AH->>W: set ctx.props, withVerifiedIdentityHeaders
W->>DO: McpAgent.serve().fetch (streamable-http)
DO->>DO: "init() -> resolveSessionMeta -> buildMcpServer -> markActivity"
DO->>DO: execute tool (engine.executeWithPause)
alt Execution paused awaiting approval
DO->>DO: "onExecutionPaused -> startPendingApprovalLease -> keepAlive()"
DO-->>C: MCP paused result (approvalUrl)
B->>W: GET /api/mcp-sessions/:id/executions/:eid
W->>DO: getPausedExecutionForApproval (DO RPC)
DO-->>B: formatted paused execution
B->>W: POST /api/mcp-sessions/:id/executions/:eid/resume
W->>DO: resumeExecutionForApproval (DO RPC)
DO->>DO: "recordApprovalResponse -> Deferred.succeed(waiter)"
DO->>DO: "resumeWithLifecycle -> engine.resume"
DO-->>B: approved/declined
end
alt Idle timeout alarm
DO->>DO: "alarm() -> decideSessionAlarm"
alt paused executions within MAX_PAUSED_SESSION_IDLE_MS
DO->>DO: extend alarm lease
else idle beyond timeout
DO->>DO: "destroy() -> closeRuntime + super.destroy"
end
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant C as MCP Client
participant W as Worker (server.ts / worker.ts)
participant AH as AgentHandler
participant DO as McpSessionDO (McpAgent)
participant B as Browser Approval UI
C->>W: POST /mcp (Bearer token)
W->>AH: "classifyMcpPath -> kind=mcp"
AH->>AH: authenticate(request)
alt Unauthenticated
AH-->>C: 401 + WWW-Authenticate challenge
end
AH->>AH: validateMcpSessionOwner (if session-id present)
AH->>AH: propsForPrincipal (session init props)
AH->>W: set ctx.props, withVerifiedIdentityHeaders
W->>DO: McpAgent.serve().fetch (streamable-http)
DO->>DO: "init() -> resolveSessionMeta -> buildMcpServer -> markActivity"
DO->>DO: execute tool (engine.executeWithPause)
alt Execution paused awaiting approval
DO->>DO: "onExecutionPaused -> startPendingApprovalLease -> keepAlive()"
DO-->>C: MCP paused result (approvalUrl)
B->>W: GET /api/mcp-sessions/:id/executions/:eid
W->>DO: getPausedExecutionForApproval (DO RPC)
DO-->>B: formatted paused execution
B->>W: POST /api/mcp-sessions/:id/executions/:eid/resume
W->>DO: resumeExecutionForApproval (DO RPC)
DO->>DO: "recordApprovalResponse -> Deferred.succeed(waiter)"
DO->>DO: "resumeWithLifecycle -> engine.resume"
DO-->>B: approved/declined
end
alt Idle timeout alarm
DO->>DO: "alarm() -> decideSessionAlarm"
alt paused executions within MAX_PAUSED_SESSION_IDLE_MS
DO->>DO: extend alarm lease
else idle beyond timeout
DO->>DO: "destroy() -> closeRuntime + super.destroy"
end
end
Reviews (2): Last reviewed commit: "fix: reconcile bun.lock against main pin..." | Re-trigger Greptile |
| } | ||
|
|
||
| const corsPreflightResponse = (): Response => | ||
| new Response(null, { | ||
| status: 204, | ||
| headers: { | ||
| "access-control-allow-origin": "*", |
There was a problem hiding this comment.
_cf_scheduleDestroy is not defined in McpAgentSessionDOBase
The McpAgentSessionStub interface declares _cf_scheduleDestroy(), but this method has no implementation in McpAgentSessionDOBase (or its McpAgent parent, as far as the repository's source shows). If the Cloudflare Agents SDK does not expose this method as a callable DO RPC, the best-effort session-cleanup on a Forbidden auth outcome silently becomes a no-op — the Effect.ignore(Effect.tryPromise(...)) wrapper swallows the failure. The same stub is duplicated in apps/host-cloudflare/src/mcp/agent-handler.ts; both handlers share the same risk. At worst the orphaned session is cleaned up only by the inactivity alarm, so this is a latency concern rather than a data-safety issue.
Sync with main (11 commits). Conflicts resolved: - accounts-section.tsx: took main's version (PR #1180 fixed a duplicate "Add connection" button); this branch only reformatted it. - engine.ts / promise.ts auto-merged (paused-execution introspection vs main). bun.lock regenerated; package.json files carry main's content as-is.
Standalone version of #1095. Same hibernatable Agent-based MCP Durable Object, but the superseded implementation is removed in the same PR rather than left alongside the new one, so this is a net reduction.
What changed relative to #1095
The new
McpAgentSessionDOBase(Cloudflare AgentsMcpAgent, hibernation-aware) is unchanged. On top of it:McpSessionDOBase(the previous bespoke session DO) plus itsworker-transportandrequest-id-queue. Nothing extended it anymore: both hosts' concreteMcpSessionDOalready extendMcpAgentSessionDOBase.wrangler.jsoncis untouched (same class name, sameMCP_SESSIONbinding).apps/cloudhad only been half-migrated: it kept the legacyMcpServingRoutessession-store dispatch purely to publish its OAuth discovery docs. Mademcp.sessionsoptional and added a discovery-onlyMcpDiscoveryRouteslayer, so a host whose/mcptransport is served outside the envelope (the Agent bridge intercepts it inserver.ts) can mount just the discovery docs. Cloud now wires only theauthseam.session-store/reporter, cloudflaresession-store/seams/response-peek, and the stale package exports.Diff
Verification
core/api,host-mcp,cloudflare,cloud,host-cloudflare,host-selfhost,localhost-cloudflareworker e2e (real workerd, drives/mcpover HTTP): 8/8host-mcp45/45 (new test coversMcpDiscoveryRoutesserving discovery docs with no session store, and/mcpreturning 404)Notes
/.well-known/oauth-*end to end (host-cloudflare has no discovery, cloud has no workerd e2e). Discovery is covered at the envelope unit level. Can add a cloud discovery e2e if wanted.