History

fix(cloud): use a new SQLite DO class name so the binding can move off the deleted KV class

CF rejects deleting McpSessionDO while MCP_SESSION still binds it (code 10061), and won't reuse a deleted class name. Rename the concrete class + all refs to McpSessionDOSqlite, repoint the binding, and have v2 delete the old KV McpSessionDO + create the new SQLite class. Verified the built worker exports McpSessionDOSqlite and nothing named McpSessionDO.

Rhys Sullivan committed 4 months ago

Commit 7fca0961 parent 2a8b1e05 files · +19 −21Browse files

Unified · Split

5 files
  1. apps/cloud/src/app.ts +3 −3
  2. apps/cloud/src/mcp/agent-handler.ts +2 −2
  3. apps/cloud/src/mcp/session-durable-object.ts +5 −5
  4. apps/cloud/src/server.ts +2 −2
  5. apps/cloud/wrangler.jsonc +7 −9
apps/cloud/src/app.ts+3−3
@@ -13,7 +13,7 @@ import { ApiKeyService } from "./auth/api-keys"; · 12 unmodified lines
13import { cloudIdentityFailureStrategy, workosIdentityLayer } from "./auth/workos-auth-provider";13import { cloudIdentityFailureStrategy, workosIdentityLayer } from "./auth/workos-auth-provider";
14import { DbService } from "./db/db";14import { DbService } from "./db/db";
15import { cloudMcpAuth } from "./mcp";15import { cloudMcpAuth } from "./mcp";
16import { McpSessionDO } from "./mcp/session-durable-object";16import { McpSessionDOSqlite } from "./mcp/session-durable-object";
17import { ErrorCaptureLive } from "./observability";17import { ErrorCaptureLive } from "./observability";
18import { AutumnService } from "./extensions/billing/service";18import { AutumnService } from "./extensions/billing/service";
19import {19import {
@@ -112,7 +112,7 @@ const { appLayer, toWebHandler, mcpExport } = ExecutorApp.make({ · 92 unmodified lines
112 failure: cloudIdentityFailureStrategy,112 failure: cloudIdentityFailureStrategy,
113 // The MCP session Durable Object class — a top-level Workers export a Layer113 // The MCP session Durable Object class — a top-level Workers export a Layer
114 // can't return; surfaced so `server.ts` can re-export it.114 // can't return; surfaced so `server.ts` can re-export it.
115 mcpExport: McpSessionDO,115 mcpExport: McpSessionDOSqlite,
116 },116 },
117 // The long-lived (boot-scoped) context provideMerge'd under everything: the117 // The long-lived (boot-scoped) context provideMerge'd under everything: the
118 // WorkOS control plane (the raw `WorkOSClient` + `ApiKeyService` the per-request118 // WorkOS control plane (the raw `WorkOSClient` + `ApiKeyService` the per-request
@@ -135,7 +135,7 @@ const { appLayer, toWebHandler, mcpExport } = ExecutorApp.make({ · 16 unmodified lines
135 requestScoped: RequestScopedServicesLive,135 requestScoped: RequestScopedServicesLive,
136});136});
137 137
138export { McpSessionDO };138export { McpSessionDOSqlite };
139 139
140export const CloudAppLayer = appLayer;140export const CloudAppLayer = appLayer;
141export const cloudMcpExport = mcpExport;141export const cloudMcpExport = mcpExport;
apps/cloud/src/mcp/agent-handler.ts+2−2
@@ -15,7 +15,7 @@ import { · 14 unmodified lines
15import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";15import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";
16 16
17import { cloudMcpAuth } from "./auth-provider";17import { cloudMcpAuth } from "./auth-provider";
18import { McpSessionDO } from "./session-durable-object";18import { McpSessionDOSqlite } from "./session-durable-object";
19 19
20interface McpAgentSessionStub {20interface McpAgentSessionStub {
21 readonly validateMcpSessionOwner: (identity: {21 readonly validateMcpSessionOwner: (identity: {
@@ -111,7 +111,7 @@ const propsForPrincipal = ( · 89 unmodified lines
111 });111 });
112 112
113export const makeCloudMcpAgentHandler = () => {113export const makeCloudMcpAgentHandler = () => {
114 const serve = McpSessionDO.serve("/mcp", {114 const serve = McpSessionDOSqlite.serve("/mcp", {
115 binding: "MCP_SESSION",115 binding: "MCP_SESSION",
116 transport: "streamable-http",116 transport: "streamable-http",
117 });117 });
apps/cloud/src/mcp/session-durable-object.ts+5−5
@@ -157,7 +157,7 @@ const makeSessionServices = (dbHandle: CloudSessionDbHandle) => { · 156 unmodified lines
157// Durable Object157// Durable Object
158// ---------------------------------------------------------------------------158// ---------------------------------------------------------------------------
159 159
160export class McpSessionDO extends McpAgentSessionDOBase<Env, CloudSessionDbHandle> {160export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionDbHandle> {
161 protected override openSessionDb(): CloudSessionDbHandle {161 protected override openSessionDb(): CloudSessionDbHandle {
162 return makeDbHandle({162 return makeDbHandle({
163 idleTimeout: LONG_LIVED_DB_IDLE_TIMEOUT_SECONDS,163 idleTimeout: LONG_LIVED_DB_IDLE_TIMEOUT_SECONDS,
@@ -181,7 +181,7 @@ export class McpSessionDO extends McpAgentSessionDOBase<Env, CloudSessionDbHandl · 17 unmodified lines
181 elicitationMode: token.elicitationMode,181 elicitationMode: token.elicitationMode,
182 } satisfies SessionMeta;182 } satisfies SessionMeta;
183 }).pipe(183 }).pipe(
184 Effect.withSpan("McpSessionDO.resolveSessionMeta"),184 Effect.withSpan("McpSessionDOSqlite.resolveSessionMeta"),
185 Effect.provide(makeSessionServices(dbHandle)),185 Effect.provide(makeSessionServices(dbHandle)),
186 Effect.ensuring(Effect.promise(() => dbHandle.end())),186 Effect.ensuring(Effect.promise(() => dbHandle.end())),
187 // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: a vanished org is a defect; the worker already verified the bearer187 // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: a vanished org is a defect; the worker already verified the bearer
@@ -208,7 +208,7 @@ export class McpSessionDO extends McpAgentSessionDOBase<Env, CloudSessionDbHandl · 20 unmodified lines
208 // billing service degrades to a no-op tracker, so this stays inert in208 // billing service degrades to a no-op tracker, so this stays inert in
209 // cloud dev/preview environments that run without a billing backend.209 // cloud dev/preview environments that run without a billing backend.
210 Effect.provide(CloudMeteredExecutionStackLayer.pipe(Layer.provide(AutumnService.Default))),210 Effect.provide(CloudMeteredExecutionStackLayer.pipe(Layer.provide(AutumnService.Default))),
211 Effect.withSpan("McpSessionDO.makeExecutionStack"),211 Effect.withSpan("McpSessionDOSqlite.makeExecutionStack"),
212 );212 );
213 // Build the description here so `executor.connections.list()` stays under213 // Build the description here so `executor.connections.list()` stays under
214 // the DO startup span and the MCP SDK receives a concrete string instead214 // the DO startup span and the MCP SDK receives a concrete string instead
@@ -235,10 +235,10 @@ export class McpSessionDO extends McpAgentSessionDOBase<Env, CloudSessionDbHandl · 20 unmodified lines
235 }),235 }),
236 }236 }
237 : { mode: sessionElicitationMode },237 : { mode: sessionElicitationMode },
238 }).pipe(Effect.withSpan("McpSessionDO.createExecutorMcpServer"));238 }).pipe(Effect.withSpan("McpSessionDOSqlite.createExecutorMcpServer"));
239 return { mcpServer, engine } satisfies BuiltMcpServer;239 return { mcpServer, engine } satisfies BuiltMcpServer;
240 }).pipe(240 }).pipe(
241 Effect.withSpan("McpSessionDO.buildMcpServer"),241 Effect.withSpan("McpSessionDOSqlite.buildMcpServer"),
242 Effect.provide(makeSessionServices(dbHandle)),242 Effect.provide(makeSessionServices(dbHandle)),
243 // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: runtime-build failures surface as the base's tapCause/cleanup defect243 // oxlint-disable-next-line executor/no-effect-escape-hatch -- boundary: runtime-build failures surface as the base's tapCause/cleanup defect
244 Effect.orDie,244 Effect.orDie,
apps/cloud/src/server.ts+2−2
@@ -12,7 +12,7 @@ import handler from "@tanstack/react-start/server-entry"; · 11 unmodified lines
12import { isAppOwnedPath } from "./app-paths";12import { isAppOwnedPath } from "./app-paths";
13import { makeCloudMcpAgentHandler } from "./mcp/agent-handler";13import { makeCloudMcpAgentHandler } from "./mcp/agent-handler";
14import { classifyMcpPath, prepareMcpOrgScope } from "./mcp/mount";14import { classifyMcpPath, prepareMcpOrgScope } from "./mcp/mount";
15import { McpSessionDO as McpSessionDOBase } from "./mcp/session-durable-object";15import { McpSessionDOSqlite as McpSessionDOBase } from "./mcp/session-durable-object";
16import { browserTracesResponse } from "./observability/browser-traces";16import { browserTracesResponse } from "./observability/browser-traces";
17import { flushTracerProvider, installTracerProvider } from "./observability/telemetry";17import { flushTracerProvider, installTracerProvider } from "./observability/telemetry";
18 18
@@ -41,7 +41,7 @@ const sentryOptions = (env: Env) => ({ · 22 unmodified lines
41// not a global fetch wrapper.41// not a global fetch wrapper.
42// ---------------------------------------------------------------------------42// ---------------------------------------------------------------------------
43 43
44export const McpSessionDO = Sentry.instrumentDurableObjectWithSentry(44export const McpSessionDOSqlite = Sentry.instrumentDurableObjectWithSentry(
45 sentryOptions,45 sentryOptions,
46 McpSessionDOBase,46 McpSessionDOBase,
47);47);
apps/cloud/wrangler.jsonc+7−9
@@ -20,16 +20,17 @@ · 19 unmodified lines
20 "bindings": [20 "bindings": [
21 {21 {
22 "name": "MCP_SESSION",22 "name": "MCP_SESSION",
23 "class_name": "McpSessionDO",23 "class_name": "McpSessionDOSqlite",
24 },24 },
25 ],25 ],
26 },26 },
27 // The MCP session DO moved to the Cloudflare Agents (`McpAgent`) base, which27 // The MCP session DO moved to the Cloudflare Agents (`McpAgent`) base, which
28 // stores state in SQLite. `McpSessionDO` was originally created with the28 // stores state in SQLite. The original `McpSessionDO` was created with the
29 // key-value backend (`new_classes`), and Cloudflare cannot convert a live class29 // key-value backend (`new_classes`), and Cloudflare cannot convert a live class
30 // to SQLite in place. Session state is ephemeral, so v2 deletes the KV class and30 // to SQLite in place, nor delete a class while a binding still references it.
31 // v3 recreates the same name on the SQLite backend. Keeping the name avoids any31 // Session state is ephemeral, so v2 repoints the `MCP_SESSION` binding to a new
32 // binding/export rename.32 // SQLite-backed class (`McpSessionDOSqlite`) and deletes the old KV `McpSessionDO`
33 // (now unreferenced). The worker exports `McpSessionDOSqlite`, not `McpSessionDO`.
33 "migrations": [34 "migrations": [
34 {35 {
35 "tag": "v1",36 "tag": "v1",
@@ -38,10 +39,7 @@ · 2 unmodified lines
38 {39 {
39 "tag": "v2",40 "tag": "v2",
40 "deleted_classes": ["McpSessionDO"],41 "deleted_classes": ["McpSessionDO"],
41 },42 "new_sqlite_classes": ["McpSessionDOSqlite"],
42 {
43 "tag": "v3",
44 "new_sqlite_classes": ["McpSessionDO"],
45 },43 },
46 ],44 ],
47 "services": [45 "services": [