import { Data, Effect } from "effect"; import { PAUSED_APPROVAL_TIMEOUT_MS, createExecutorMcpServer,} from "@executor-js/host-mcp/tool-server";import { buildResumeApprovalUrl } from "@executor-js/host-mcp/browser-approval";import type { ExecutorDbHandle } from "@executor-js/api/server";import { McpAgentSessionDOBase, type BuiltMcpServer, type McpApprovalOwner, type McpSessionModelResumeResult, type McpSessionInit, type SessionMeta,} from "@executor-js/cloudflare/mcp/agent-durable-object";import { mcpExecutionOwnerDirectoryFromNamespace, type McpExecutionOwnerDirectory, type McpExecutionOwnerRoute,} from "@executor-js/cloudflare/mcp/execution-owner-directory";import { mcpSessionStub } from "@executor-js/cloudflare/mcp/session-stub";import type { ResumeResponse } from "@executor-js/execution"; import { loadConfig, type CloudflareConfig, type CloudflareEnv } from "../config";import { createD1ExecutorDb } from "../db/d1";import { makeCloudflareExecutionStackLayer, makeExecutionStack } from "../execution";import { preloadQuickJs } from "../quickjs"; // ---------------------------------------------------------------------------// Cloudflare (self-host) MCP Session Durable Object — the host-cloudflare// binding of the shared `McpAgentSessionDOBase` (@executor-js/cloudflare). Identical// base to cloud; the ONLY differences are the injected dependencies:// - openSessionDb → a long-lived D1 `ExecutorDbHandle` (same FumaDB// assembly the HTTP path uses), adapted to the base's// `end` disposal contract.// - resolveSessionMeta → single-tenant: the org is fixed in config, so no// lookup — just stamp the configured org name.// - buildMcpServer → the QuickJS execution stack + the MCP tool server.// host-cf has no OTel/Sentry, so it keeps the base's default no-op telemetry +// error seams. Replacing the prior in-memory store with this DO is what fixes// `tools/list` failing across Worker isolates (a session created on one isolate// was invisible to the next; the DO id == session id routes them all back).// --------------------------------------------------------------------------- // The long-lived D1 handle, adapted to the base's `end` contract. D1 owns its// own lifecycle (the binding is the connection), so `end` is `close` — a no-op.type CfSessionDbHandle = ExecutorDbHandle & { readonly end: () => Promise<void> }; class McpModelResumeForwardError extends Data.TaggedError("McpModelResumeForwardError")<{ readonly cause: unknown;}> {} export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSessionDbHandle> { private readonly cfEnv: CloudflareEnv; private readonly cfConfig: CloudflareConfig; constructor( ctx: ConstructorParameters<typeof McpAgentSessionDOBase<CloudflareEnv, CfSessionDbHandle>>[0], env: CloudflareEnv, ) { super(ctx, env); this.cfEnv = env; this.cfConfig = loadConfig(env); } protected override executionOwnerDirectory(): McpExecutionOwnerDirectory | null { return mcpExecutionOwnerDirectoryFromNamespace(this.cfEnv.MCP_EXECUTION_OWNER); } protected override forwardModelResumeToOwner( owner: McpExecutionOwnerRoute, identity: McpApprovalOwner, executionId: string, response: ResumeResponse, ): Effect.Effect<McpSessionModelResumeResult, unknown> { return Effect.tryPromise({ try: () => mcpSessionStub(this.cfEnv.MCP_SESSION, owner.sessionId).resumeExecutionForModel( executionId, identity, response, ), catch: (cause) => new McpModelResumeForwardError({ cause }), }); } protected override async openSessionDb(): Promise<CfSessionDbHandle> { const handle = await createD1ExecutorDb(this.cfEnv.DB, this.cfEnv.BLOBS); return { ...handle, end: () => handle.close() }; } protected override resolveSessionMeta(token: McpSessionInit): Effect.Effect<SessionMeta> { // Single-tenant: every Access principal belongs to the one configured org, // so there is nothing to resolve — stamp the configured org name. return Effect.succeed({ organizationId: token.organizationId, organizationName: this.cfConfig.organizationName, organizationSlug: this.cfConfig.organizationSlug, userId: token.userId, resource: token.resource, elicitationMode: token.elicitationMode, } satisfies SessionMeta); } protected override buildMcpServer( sessionMeta: SessionMeta, dbHandle: CfSessionDbHandle, ): Effect.Effect<BuiltMcpServer> { const config = this.cfConfig; const self = this; return Effect.gen(function* () { // QuickJS-WASM must be loaded before the executor layer builds it (the // default variant can't fetch its .wasm on Workers). Idempotent per isolate. yield* Effect.promise(() => preloadQuickJs()); const { engine } = yield* makeExecutionStack( sessionMeta.userId, sessionMeta.organizationId, sessionMeta.organizationName, { mcpResource: sessionMeta.resource }, ).pipe(Effect.provide(makeCloudflareExecutionStackLayer(config, dbHandle))); // Browser elicitation mode (the base owns the approval store + the HTTP // approval RPCs): a gated execution pauses and returns an approvalUrl into // the console resume page. The URL origin is the create request's origin // (captured by the base), falling back to the configured site URL. const elicitationMode = sessionMeta.elicitationMode ?? "model"; const mcpServer = yield* createExecutorMcpServer({ engine, browserApprovalStore: self.browserApprovalStore, pausedExecutionHooks: self.pausedExecutionHooks, pausedExecutionLeaseMs: PAUSED_APPROVAL_TIMEOUT_MS, resumeFallback: self.modelResumeFallback, elicitationMode: elicitationMode === "browser" ? { mode: "browser" as const, approvalUrl: (executionId) => { // webOrigin is captured per-request at session create; for a // legacy session cold-restored without it, fall back to the // pinned site URL. If neither exists, the link would be // unreachable — fail VISIBLY (a logged, obviously-invalid host) // instead of silently pointing the human at http://localhost. const origin = sessionMeta.webOrigin ?? config.webBaseUrl; if (!origin) { console.error( "[executor-cloudflare] cannot build MCP approval URL: no session web origin and VITE_PUBLIC_SITE_URL is unset. Set VITE_PUBLIC_SITE_URL so approval links are reachable.", ); } return buildResumeApprovalUrl({ origin: origin ?? "https://unconfigured-origin.invalid", executionId, sessionId: self.sessionId, organizationSlug: sessionMeta.organizationSlug, }); }, } : { mode: elicitationMode }, }); return { mcpServer, engine } satisfies BuiltMcpServer; }).pipe( Effect.withSpan("McpSessionDO.buildMcpServer"), // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: a runtime-build failure surfaces as the base's tapCause/cleanup defect Effect.orDie, ); }}
import { Data, Effect } from "effect";
import {
PAUSED_APPROVAL_TIMEOUT_MS,
createExecutorMcpServer,
} from "@executor-js/host-mcp/tool-server";
import { buildResumeApprovalUrl } from "@executor-js/host-mcp/browser-approval";
import type { ExecutorDbHandle } from "@executor-js/api/server";
import {
McpAgentSessionDOBase,
type BuiltMcpServer,
type McpApprovalOwner,
type McpSessionModelResumeResult,
type McpSessionInit,
type SessionMeta,
} from "@executor-js/cloudflare/mcp/agent-durable-object";
import {
mcpExecutionOwnerDirectoryFromNamespace,
type McpExecutionOwnerDirectory,
type McpExecutionOwnerRoute,
} from "@executor-js/cloudflare/mcp/execution-owner-directory";
import { mcpSessionStub } from "@executor-js/cloudflare/mcp/session-stub";
import type { ResumeResponse } from "@executor-js/execution";
import { loadConfig, type CloudflareConfig, type CloudflareEnv } from "../config";
import { createD1ExecutorDb } from "../db/d1";
import { makeCloudflareExecutionStackLayer, makeExecutionStack } from "../execution";
import { preloadQuickJs } from "../quickjs";
// ---------------------------------------------------------------------------
// Cloudflare (self-host) MCP Session Durable Object — the host-cloudflare
// binding of the shared `McpAgentSessionDOBase` (@executor-js/cloudflare). Identical
// base to cloud; the ONLY differences are the injected dependencies:
// - openSessionDb → a long-lived D1 `ExecutorDbHandle` (same FumaDB
// assembly the HTTP path uses), adapted to the base's
// `end` disposal contract.
// - resolveSessionMeta → single-tenant: the org is fixed in config, so no
// lookup — just stamp the configured org name.
// - buildMcpServer → the QuickJS execution stack + the MCP tool server.
// host-cf has no OTel/Sentry, so it keeps the base's default no-op telemetry +
// error seams. Replacing the prior in-memory store with this DO is what fixes
// `tools/list` failing across Worker isolates (a session created on one isolate
// was invisible to the next; the DO id == session id routes them all back).
// ---------------------------------------------------------------------------
// The long-lived D1 handle, adapted to the base's `end` contract. D1 owns its
// own lifecycle (the binding is the connection), so `end` is `close` — a no-op.
type CfSessionDbHandle = ExecutorDbHandle & { readonly end: () => Promise<void> };
class McpModelResumeForwardError extends Data.TaggedError("McpModelResumeForwardError")<{
readonly cause: unknown;
}> {}
export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSessionDbHandle> {
private readonly cfEnv: CloudflareEnv;
private readonly cfConfig: CloudflareConfig;
constructor(
ctx: ConstructorParameters<typeof McpAgentSessionDOBase<CloudflareEnv, CfSessionDbHandle>>[0],
env: CloudflareEnv,
) {
super(ctx, env);
this.cfEnv = env;
this.cfConfig = loadConfig(env);
}
protected override executionOwnerDirectory(): McpExecutionOwnerDirectory | null {
return mcpExecutionOwnerDirectoryFromNamespace(this.cfEnv.MCP_EXECUTION_OWNER);
}
protected override forwardModelResumeToOwner(
owner: McpExecutionOwnerRoute,
identity: McpApprovalOwner,
executionId: string,
response: ResumeResponse,
): Effect.Effect<McpSessionModelResumeResult, unknown> {
return Effect.tryPromise({
try: () =>
mcpSessionStub(this.cfEnv.MCP_SESSION, owner.sessionId).resumeExecutionForModel(
executionId,
identity,
response,
),
catch: (cause) => new McpModelResumeForwardError({ cause }),
});
}
protected override async openSessionDb(): Promise<CfSessionDbHandle> {
const handle = await createD1ExecutorDb(this.cfEnv.DB, this.cfEnv.BLOBS);
return { ...handle, end: () => handle.close() };
}
protected override resolveSessionMeta(token: McpSessionInit): Effect.Effect<SessionMeta> {
// Single-tenant: every Access principal belongs to the one configured org,
// so there is nothing to resolve — stamp the configured org name.
return Effect.succeed({
organizationId: token.organizationId,
organizationName: this.cfConfig.organizationName,
organizationSlug: this.cfConfig.organizationSlug,
userId: token.userId,
resource: token.resource,
elicitationMode: token.elicitationMode,
} satisfies SessionMeta);
}
protected override buildMcpServer(
sessionMeta: SessionMeta,
dbHandle: CfSessionDbHandle,
): Effect.Effect<BuiltMcpServer> {
const config = this.cfConfig;
const self = this;
return Effect.gen(function* () {
// QuickJS-WASM must be loaded before the executor layer builds it (the
// default variant can't fetch its .wasm on Workers). Idempotent per isolate.
yield* Effect.promise(() => preloadQuickJs());
const { engine } = yield* makeExecutionStack(
sessionMeta.userId,
sessionMeta.organizationId,
sessionMeta.organizationName,
{ mcpResource: sessionMeta.resource },
).pipe(Effect.provide(makeCloudflareExecutionStackLayer(config, dbHandle)));
// Browser elicitation mode (the base owns the approval store + the HTTP
// approval RPCs): a gated execution pauses and returns an approvalUrl into
// the console resume page. The URL origin is the create request's origin
// (captured by the base), falling back to the configured site URL.
const elicitationMode = sessionMeta.elicitationMode ?? "model";
const mcpServer = yield* createExecutorMcpServer({
engine,
browserApprovalStore: self.browserApprovalStore,
pausedExecutionHooks: self.pausedExecutionHooks,
pausedExecutionLeaseMs: PAUSED_APPROVAL_TIMEOUT_MS,
resumeFallback: self.modelResumeFallback,
elicitationMode:
elicitationMode === "browser"
? {
mode: "browser" as const,
approvalUrl: (executionId) => {
// webOrigin is captured per-request at session create; for a
// legacy session cold-restored without it, fall back to the
// pinned site URL. If neither exists, the link would be
// unreachable — fail VISIBLY (a logged, obviously-invalid host)
// instead of silently pointing the human at http://localhost.
const origin = sessionMeta.webOrigin ?? config.webBaseUrl;
if (!origin) {
console.error(
"[executor-cloudflare] cannot build MCP approval URL: no session web origin and VITE_PUBLIC_SITE_URL is unset. Set VITE_PUBLIC_SITE_URL so approval links are reachable.",
);
}
return buildResumeApprovalUrl({
origin: origin ?? "https://unconfigured-origin.invalid",
executionId,
sessionId: self.sessionId,
organizationSlug: sessionMeta.organizationSlug,
});
},
}
: { mode: elicitationMode },
});
return { mcpServer, engine } satisfies BuiltMcpServer;
}).pipe(
Effect.withSpan("McpSessionDO.buildMcpServer"),
// oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: a runtime-build failure surfaces as the base's tapCause/cleanup defect
Effect.orDie,
);
}
}