Claude Opus 5.5

agents-0-22-0-14-b

Claude Code's read-only tools, web search on, ludion sync on

Question

Where does MCPClientManager get its storage from in the Cloudflare Agents SDK?

Answer key

A correct answer says
`MCPClientManager` gets storage from the Lifecycle it is installed on; the `storage` option was removed, so standalone construction with explicit DurableObjectStorage is not supported.
The stale answer
Construct `MCPClientManager` with a `storage` option pointing at DurableObjectStorage.

Answer

In the installed `agents` 0.27.0, you don't pass storage to `MCPClientManager`. It extends `LifecycleCapability`, and its constructor only takes `(name, version, options?)`, where `options` holds just `env` and `createAuthProvider`. When the Durable Object lifecycle installs it (for example as `agent.mcp`), it gets the host Durable Object's `DurableObjectStorage` through `this.lifecycle.storage` (`LifecycleServices.storage`).

It uses that storage for three things:
- **Server list:** it keeps the `cf_agents_mcp_servers` table through `storage.sql.exec`, plus a schema-version key.
- **OAuth:** it builds its `DurableObjectOAuthClientProvider` on the same storage.
- **Renames:** it moves OAuth keys when a server id changes.

This is from the installed package's type definitions and compiled code (`node_modules/agents/dist/client-1tEQ1Oqz.js:1304-1353`). Older examples that pass storage into the constructor don't match this version.

Answering cost $0.19.

Grade

correct

The judge's reason: It says storage comes from the installing lifecycle (this.lifecycle.storage), that the constructor takes no storage option, and that passing storage to the constructor is outdated.

The judge's log

Session log

What the agent wrote, the tools it called, and what they returned, from docs/bench/2026-10-11-done3/runs/B-project-sync/agents-0-22-0-14-b.jsonl. Thinking and the session's settings are left out. The raw log

  1. Tool call: Grep

    {
      "pattern": "class MCPClientManager|storage",
      "path": "C:\\Users\\haya0\\AppData\\Local\\Temp\\ludion-bench-Lmdynx\\work\\node_modules\\agents\\dist",
      "glob": "*.d.ts",
      "output_mode": "content",
      "head_limit": 40
    }
  2. Tool output

    node_modules\agents\dist\browser-tmkcAAI3.d.ts:137:   * {@link DurableBrowserSessionStore} over the host object's storage, which
    node_modules\agents\dist\browser-tmkcAAI3.d.ts:203: * object's own storage. It schedules nothing: Browser Run reclaims idle
    node_modules\agents\dist\browser-tmkcAAI3.d.ts:210: * Store discipline: locks wrap storage operations only — liveness probes and
    node_modules\agents\dist\capability-runner-O5VbQzqg.d.ts:42:   * Agents SDK) to namespace or wrap internal state storage.
    node_modules\agents\dist\capability-runner-O5VbQzqg.d.ts:167:  /** Stable equality and storage key. */ readonly key: string /** Transport-owned serialized address. */;
    node_modules\agents\dist\capability-runner-O5VbQzqg.d.ts:228:  readonly storage: DurableObjectStorage;
    node_modules\agents\dist\capability-runner-O5VbQzqg.d.ts:347: * Capabilities extending `LifecycleCapability` receive the standard storage,
    node_modules\agents\dist\connector-Cl-VqPmA.d.ts:320:   * all holders using the same key. Held only around storage reads/writes —
    node_modules\agents\dist\connector-Cl-VqPmA.d.ts:336:  private readonly storage;
    node_modules\agents\dist\connector-Cl-VqPmA.d.ts:337:  constructor(storage: DurableObjectStorage);
    node_modules\agents\dist\browser\ai.d.ts:17:   * storage — so the tool must be created from inside a Durable Object
    node_modules\agents\dist\browser\ai.d.ts:64:   * {@link DurableBrowserSessionStore} over `ctx.storage`.
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:478: * Table and column names are storage-frozen — never rename them.
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:663: * Nothing here renames any wire- or storage-visible identifier: the
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:745:   * `ctx.facets.delete` to wipe the target's storage.
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:758:   * own children; storage is preserved.
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:763:   * storage. Transitively deletes the child's own children.
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:933:   * inside the child's isolate, so every storage write and `onStart()`
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:983:   * its storage is preserved. Transitively aborts the child's own
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:991:   * its storage. Transitively deletes the child's own children.
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:999:   * auto-maintained SQLite registry in the parent's storage.
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1054: * Pluggable storage adapter for persisting `WorkerTransport` state across
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1057: * A typical implementation reads/writes a single key on `this.ctx.storage`
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1078:   * Optional storage adapter for persisting transport state across DO
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1082:  storage?: MCPStorageApi;
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1086:  private readonly _storage?;
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1224: * stream from `eventId` without a storage hit.
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1230: * immediately after writing the close frame, so storage growth is
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1255:  /** DO storage caps multi-key delete at 128. */
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1261:  private readonly storage;
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1262:  /** In-memory seq counters per stream, rehydrated lazily from storage. */
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1265:  constructor(storage: DurableObjectStorage);
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1283:   * storage cap) so we never load the entire event log into memory.
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1390:   * {@link DurableObjectEventStore} backed by this agent's storage,
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:1397:   * background cleanup — storage cost is bounded by the in-flight
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:2568://#region src/mcp/client/storage.d.ts
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:2638: * Normalize a caller-supplied MCP server id into a stable, storage- and
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:2645: *  - as a key on the `mcpConnections` map and OAuth provider storage
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:2769:declare class MCPClientManager extends LifecycleCapability {
    node_modules\agents\dist\agent-routing-Bhdbvf7s.d.ts:2829:   *  - OAuth-related storage keys under `/{clientName}/{oldId}/...`
    
    [Showing results with pagination = limit: 40]
  3. Tool call: Read

    {
      "file_path": "C:\\Users\\haya0\\AppData\\Local\\Temp\\ludion-bench-Lmdynx\\work\\node_modules\\agents\\dist\\agent-routing-Bhdbvf7s.d.ts",
      "offset": 2740,
      "limit": 90
    }
  4. Tool output

    2740	  url?: MCPClientElicitationHandler;
    2741	};
    2742	/** Dependencies used by {@link MCPClientManager} across Durable Object wakes. */
    2743	type MCPClientManagerOptions = {
    2744	  /**
    2745	   * Runtime bindings used to restore persisted RPC MCP connections.
    2746	   * Required when the durable catalog contains `rpc://` servers.
    2747	   */
    2748	  readonly env?: Cloudflare.Env /** Construct the OAuth provider used for a persisted HTTP server. */;
    2749	  readonly createAuthProvider?: (callbackUrl: string) => AgentMcpOAuthProvider;
    2750	};
    2751	/**
    2752	 * Filter options for scoping tools, prompts, resources, and resource templates
    2753	 * to a subset of connected MCP servers. All specified criteria are AND'd together.
    2754	 */
    2755	type MCPServerFilter = {
    2756	  /** Include only connections matching this server ID (or IDs). */ serverId?:
    2757	    | string
    2758	    | string[] /** Include only connections whose stored name matches (or is in) this value. */;
    2759	  serverName?:
    2760	    | string
    2761	    | string[] /** Include only connections currently in this state (or states). */;
    2762	  state?: MCPConnectionState | MCPConnectionState[];
    2763	};
    2764	/**
    2765	 * A Durable Object capability that persists and aggregates MCP client
    2766	 * connections. Installing it directly on a Lifecycle (outside `agent.mcp`)
    2767	 * is experimental: that capability surface may change before stabilizing.
    2768	 */
    2769	declare class MCPClientManager extends LifecycleCapability {
    2770	  private readonly _name;
    2771	  private readonly _version;
    2772	  mcpConnections: Record<string, MCPClientConnection>;
    2773	  /** Cache only the current catalog so old schema graphs are not retained. */
    2774	  private readonly _aiToolSchemas;
    2775	  private _didWarnAboutUnstableGetAITools;
    2776	  private _oauthCallbackConfig?;
    2777	  private _connectionDisposables;
    2778	  private readonly _env;
    2779	  private readonly _createAuthProviderFn;
    2780	  private _isRestored;
    2781	  private _pendingConnections;
    2782	  private _elicitationHandlers?;
    2783	  /** @internal Protected for testing purposes. */
    2784	  protected readonly _onObservabilityEvent: Emitter<MCPObservabilityEvent>;
    2785	  readonly onObservabilityEvent: Event<MCPObservabilityEvent>;
    2786	  private readonly _onServerStateChanged;
    2787	  /**
    2788	   * Event that fires whenever any MCP server state changes (registered, connected, removed, etc.)
    2789	   * This is useful for broadcasting server state to clients.
    2790	   */
    2791	  readonly onServerStateChanged: Event<void>;
    2792	  /**
    2793	   * Construct a reusable Durable Object MCP client capability.
    2794	   *
    2795	   * @param _name - MCP client implementation name sent during negotiation.
    2796	   * @param _version - MCP client implementation version sent during negotiation.
    2797	   * @param options - Optional runtime bindings and OAuth configuration.
    2798	   */
    2799	  constructor(
    2800	    _name: string,
    2801	    _version: string,
    2802	    options?: MCPClientManagerOptions
    2803	  );
    2804	  /** Restore persisted HTTP and RPC connections before the host handles work. */
    2805	  onStart(): Promise<void>;
    2806	  /** Intercept a registered MCP OAuth callback request. */
    2807	  onRequest(context: CapabilityRequestContext): Promise<Response | undefined>;
    2808	  private oauthCallbackResponse;
    2809	  /**
    2810	   * Scope the manager-level elicitation handler to a single connection.
    2811	   * Returns undefined when no handler is configured so the connection keeps
    2812	   * its default throwing behavior.
    2813	   */
    2814	  private scopedElicitationHandlers;
    2815	  private sql;
    2816	  private saveServerToStorage;
    2817	  private removeServerFromStorage;
    2818	  /**
    2819	   * Rename a server's id, in-place, across every place the id is used as a
    2820	   * key. Used to JIT-migrate servers that were originally registered under an
    2821	   * auto-generated nanoid to a caller-supplied stable id (see
    2822	   * `Agent.addMcpServer`'s `{ id }` option).
    2823	   *
    2824	   * Migrates:
    2825	   *  - the `cf_agents_mcp_servers` row (primary key)
    2826	   *  - the in-memory `mcpConnections` map key
    2827	   *  - the connection disposables map key
    2828	   *  - the attached `authProvider.serverId`, if any
    2829	   *  - OAuth-related storage keys under `/{clientName}/{oldId}/...`
  5. Tool call: Grep

    {
      "pattern": "LifecycleCapability|storage|sql",
      "path": "C:\\Users\\haya0\\AppData\\Local\\Temp\\ludion-bench-Lmdynx\\work\\node_modules\\agents\\dist\\capability-runner-O5VbQzqg.d.ts",
      "output_mode": "content",
      "-C": 3
    }
  6. Tool output

    39-   *
    40-   * This property is configurable, meaning it can be redefined via
    41-   * `Object.defineProperty` by downstream consumers (e.g. the Cloudflare
    42:   * Agents SDK) to namespace or wrap internal state storage.
    43-   */
    44-  state: ConnectionState<TState>;
    45-  /**
    --
    164-//#region src/lifecycle/capability.d.ts
    165-/** Opaque address understood by a Lifecycle routing transport. */
    166-type LifecycleRouteAddress = {
    167:  /** Stable equality and storage key. */ readonly key: string /** Transport-owned serialized address. */;
    168-  readonly data: string;
    169-};
    170-/** Context supplied with a routed capability message. */
    --
    225-type LifecycleServices = {
    226-  /** The host's Durable Object name; see `Lifecycle.name`. */ readonly name: string /** The host class name, as exported from the Worker. */;
    227-  readonly className: string;
    228:  readonly storage: DurableObjectStorage;
    229-  readonly sockets: LifecycleSockets;
    230-  /**
    231-   * Resolve once Lifecycle startup has finished, starting it if needed, and
    --
    278- *
    279- * @experimental The API surface may change before stabilizing.
    280- */
    281:declare abstract class LifecycleCapability<Props extends object = object> {
    282-  readonly capabilityId: string;
    283-  /** How this capability claims traffic; see {@link DurableObjectCapability.claims}. */
    284-  readonly claims: "selective" | "catch-all";
    --
    344-/**
    345- * A capability installed into a Durable Object lifecycle.
    346- *
    347: * Capabilities extending `LifecycleCapability` receive the standard storage,
    348- * readiness, alarm, event, and routing surface. Host-specific bindings and
    349- * protocol adapters remain explicit constructor dependencies. Hook parameters
    350- * carry only phase data; hooks do not run in ambient host context — a
    --
    486-  MemoryLimitContext as o,
    487-  LifecycleServices as p,
    488-  CapabilityWebSocketUpgradeContext as r,
    489:  LifecycleCapability as s,
    490-  CapabilityRequestContext as t,
    491-  LifecycleRouteAddress as u,
    492-  LifecycleJobOutcome as v,
  7. Tool call: Read

    {
      "file_path": "C:\\Users\\haya0\\AppData\\Local\\Temp\\ludion-bench-Lmdynx\\work\\node_modules\\agents\\dist\\capability-runner-O5VbQzqg.d.ts",
      "offset": 281,
      "limit": 40
    }
  8. Tool output

    281	declare abstract class LifecycleCapability<Props extends object = object> {
    282	  readonly capabilityId: string;
    283	  /** How this capability claims traffic; see {@link DurableObjectCapability.claims}. */
    284	  readonly claims: "selective" | "catch-all";
    285	  protected constructor(capabilityId: string);
    286	  /** Default startup hook; capabilities override when they own startup work. */
    287	  onStart(_context: CapabilityStartContext<Props>): void;
    288	  /** Standard services when installed, or undefined in isolated unit tests. */
    289	  protected get lifecycleServices(): LifecycleServices | undefined;
    290	  /** Standard services supplied when Lifecycle installs this capability. */
    291	  protected get lifecycle(): LifecycleServices;
    292	}
    293	//#endregion
    294	//#region src/lifecycle/capability-runner.d.ts
    295	type MaybePromise<T> = T | Promise<T>;
    296	/** One best-effort event published by a Lifecycle capability. */
    297	type LifecycleEvent = {
    298	  /** Stable capability or subsystem name. */ readonly source: string /** Stable event name within that source. */;
    299	  readonly type: string /** Event-specific data. */;
    300	  readonly payload: unknown;
    301	};
    302	/** Context supplied when durable capabilities start. */
    303	type CapabilityStartContext<Props extends object = object> = {
    304	  /** Properties supplied while resolving the Durable Object. */ readonly props:
    305	    | Props
    306	    | undefined;
    307	};
    308	/** Context supplied when durable capabilities inspect an HTTP request. */
    309	type CapabilityRequestContext = {
    310	  /** The request entering the Durable Object. */ readonly request: Request;
    311	};
    312	/** Context supplied when a capability inspects a WebSocket upgrade. */
    313	type CapabilityWebSocketUpgradeContext = {
    314	  /** The WebSocket upgrade request entering the Durable Object. */ readonly request: Request;
    315	};
    316	/**
    317	 * Context supplied when the alarm memory-limit circuit breaker records a
    318	 * strike (#1825), to capabilities and the host alike.
    319	 */
    320	type MemoryLimitContext = {
  9. Tool call: Grep

    {
      "pattern": "lifecycle\\.storage",
      "path": "C:\\Users\\haya0\\AppData\\Local\\Temp\\ludion-bench-Lmdynx\\work\\node_modules\\agents\\dist",
      "glob": "*.js",
      "output_mode": "content",
      "head_limit": 8
    }
  10. Tool output

    node_modules\agents\dist\client-1tEQ1Oqz.js:1304:		if ((await this.lifecycle.storage.get(MCP_SCHEMA_VERSION_KEY) ?? 0) < CURRENT_MCP_SCHEMA_VERSION) {
    node_modules\agents\dist\client-1tEQ1Oqz.js:1305:			ensureMcpServerTable(this.lifecycle.storage);
    node_modules\agents\dist\client-1tEQ1Oqz.js:1306:			await this.lifecycle.storage.put(MCP_SCHEMA_VERSION_KEY, CURRENT_MCP_SCHEMA_VERSION);
    node_modules\agents\dist\client-1tEQ1Oqz.js:1353:		return [...this.lifecycle.storage.sql.exec(query, ...bindings)];
    node_modules\agents\dist\client-1tEQ1Oqz.js:1398:			const keys = await this.lifecycle.storage.list({ prefix: oldPrefix });
    node_modules\agents\dist\client-1tEQ1Oqz.js:1407:				await this.lifecycle.storage.put(writes);
    node_modules\agents\dist\client-1tEQ1Oqz.js:1408:				await this.lifecycle.storage.delete(deletes);
    node_modules\agents\dist\client-1tEQ1Oqz.js:1585:		const authProvider = new DurableObjectOAuthClientProvider(this.lifecycle.storage, clientName, callbackUrl);
    
    [Showing results with pagination = limit: 8]
  11. Agent

    In the installed `agents` 0.27.0, you don't pass storage to `MCPClientManager`. It extends `LifecycleCapability`, and its constructor only takes `(name, version, options?)`, where `options` holds just `env` and `createAuthProvider`. When the Durable Object lifecycle installs it (for example as `agent.mcp`), it gets the host Durable Object's `DurableObjectStorage` through `this.lifecycle.storage` (`LifecycleServices.storage`).
    
    It uses that storage for three things:
    - **Server list:** it keeps the `cf_agents_mcp_servers` table through `storage.sql.exec`, plus a schema-version key.
    - **OAuth:** it builds its `DurableObjectOAuthClientProvider` on the same storage.
    - **Renames:** it moves OAuth keys when a server id changes.
    
    This is from the installed package's type definitions and compiled code (`node_modules/agents/dist/client-1tEQ1Oqz.js:1304-1353`). Older examples that pass storage into the constructor don't match this version.