Skip to content

Centralize account-ID resolution; upgrade agents/MCP SDK/zod/ai; migrate to registerTool#384

Merged
mattzcarey merged 11 commits into
mainfrom
feat/centralize-account-id-resolution
Jun 1, 2026
Merged

Centralize account-ID resolution; upgrade agents/MCP SDK/zod/ai; migrate to registerTool#384
mattzcarey merged 11 commits into
mainfrom
feat/centralize-account-id-resolution

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

A sequence of related changes on this branch (each its own commit):

  1. Centralize Cloudflare account-ID resolution and remove the account-management tools.
  2. Remove the now-dormant UserDetails Durable Object (+ DO deletion migration).
  3. Upgrade core deps: agents 0.2.19 → 0.13.3, @modelcontextprotocol/sdk 1.20.2 → 1.29.0, zod 3 → 4, ai 4 → 6.
  4. Migrate tool registration from the deprecated .tool() API to .registerTool().
  5. Migrate the test suite to Vitest 4 + @cloudflare/vitest-pool-workers 0.16.
  6. Get the evals running through a BYOK AI Gateway.
  7. Make the generated worker types current (drop --include-env=false).

Supersedes #316 (per-tool account_id) — this is a superset, so #316 can be closed.

⚠️ This PR requires a manual, ordered deploy — see Deployment below.

1. Account-ID resolution

Removes accounts_list and set_active_account (and the per-app getActiveAccountId/setActiveAccountId). Account scoping is now resolved by a single AccountManager (packages/mcp-common/src/account-manager.ts) via a new server.accountTool() wrapper, in priority order:

  1. Auth-pinned — an account-scoped API token's account, or an OAuth token with exactly one account (no account_id parameter is exposed in this case).
  2. cf-account-id request header — set by the user in their MCP client config (multi-account tokens).
  3. account_id tool argument — auto-appended to account-scoped tools only for multi-account tokens; when omitted (and no header) the tool returns an error listing the available accounts. Multi-account tokens also get their account list injected into the server initialize instructions for discovery.

All tool error responses now set isError: true.

2. Remove the UserDetails Durable Object

UserDetails existed only to persist activeAccountId across sessions for the old set_active_account flow (added in #85). Account resolution is now derived per-request, so nothing reads or writes it. This PR:

  • Deletes user_details.do.ts and removes the USER_DETAILS binding, export, and Env type from all 13 account-scoped apps (dev + staging + production).
  • Adds a deleted_classes DO migration (tag v2) to the two owning workers — workers-observability and workers-builds — so the namespace is torn down.
  • The other 11 apps were cross-binders (they referenced observability's UserDetails via script_name in staging/prod); their bindings are removed.
  • Strips dead commented-out tool blocks in r2_bucket.tools.ts / hyperdrive.tools.ts that referenced the removed getActiveAccountId.

3. Dependency upgrade

  • zod 4: z.record(key, value) explicit key; z.string().ip()z.ipv4()/z.ipv6(); dropped removed objectOutputType (→ z.infer<z.ZodObject<Shape>>).
  • agents 0.13: McpAgent env generic constrained to Cloudflare.Env; createApiHandler/handleApiTokenMode infer the env generic (no any); MCPClientManager takes a storage option (eval clients).
  • MCP SDK 1.29: tool annotations must be flat ({ title, readOnlyHint, ... }) — fixes a latent bug where nested annotations: {...} hints were silently ignored.
  • ai 6: eval tooling updated (LanguageModel, inputSchema, stopWhen/stepCountIs, tool-call input).

4. registerTool migration

CloudflareMCPServer wraps both tool() and registerTool() for metrics via one shared helper. accountTool() and all remaining .tool() call sites use .registerTool(name, { description, inputSchema, annotations }, cb). No @ts-ignore / any introduced.

5. Vitest 4 + pool-workers 0.16

  • defineConfig + the cloudflareTest({...}) Vite plugin (replaces defineWorkersConfig / poolOptions.workers); root vitest.config.ts with test.projects (replaces vitest.workspace.ts).
  • Packages set "type": "module" (pool-workers 0.16 main entry is ESM-only).
  • fetchMock was removed from cloudflare:test → migrated those specs to MSW (http / HttpResponse / setupServer).

6. Evals

  • Eval models route through the user's AI Gateway using its stored (BYOK) keys via ai-gateway-provider's createUnified() — no per-provider key in the repo.
  • Subjects: openai/gpt-5.4-mini, openai/gpt-4.1, workers-ai/@cf/moonshotai/kimi-k2.6; judge: openai/gpt-5.4-nano.
  • Eval clients connect over /mcp (token-mode servers don't serve /sse).
  • KV evals pass end-to-end; the hyperdrive evals fail only because the dev token lacks Hyperdrive read permission (not a code issue).

7. Worker types currency

  • Removed --include-env=false from every app's types script (and run-wrangler-types). Under wrangler 4.96 that flag also strips the global interface Env the app/test code relies on (getEnv<Env>, McpAgent<Env>, vitest TestEnv extends Env).
  • Regenerated worker-configuration.d.ts across the repo to the wrangler 4.96 / workerd 1.20260529 runtime types (this also brought @cf/google/embeddinggemma-300m into AiModels, so the docs-vectorize embeddings tool typechecks without @ts-expect-error).
  • Two small source adjustments the new runtime types required: drop public from the ctx: DurableObjectState constructor params in docs-* / sandbox-container (new DurableObjectState<unknown> default conflicted with the McpAgent/DurableObject base's <{}>); assign auth props through a typed mutable view since ExecutionContext.props is now readonly.

Breaking / behavior changes

  • accounts_list and set_active_account are removed.
  • Tool annotation hints (readOnlyHint / destructiveHint) now actually apply (were silently dropped before).
  • Multi-account OAuth users must pass account_id or set a cf-account-id header.

Deployment

This must be deployed manually, in order, because the UserDetails deletion interacts with cross-script bindings:

  1. Deploy the 11 cross-binder apps (ai-gateway, auditlogs, autorag, browser-rendering, cloudflare-one-casb, dex-analysis, dns-analytics, graphql, logpush, radar, workers-bindings) and workers-builds first — their UserDetails bindings are now removed.
  2. Deploy workers-observability last — it owns the shared UserDetails DO that the cross-binders referenced via script_name, and its deleted_classes migration can only run once nothing else binds the class.

Deploying workers-observability before the cross-binders are updated will fail the migration (or break a lagging cross-binder's deploy). Existing UserDetails instances and their stored activeAccountId data are dropped by the migration; nothing reads that data anymore.

Verification

  • check:types, check:lint, check:format, check:deps (syncpack) all green across the workspace; tests pass.
  • eval:ci runs end-to-end against a local server through the AI Gateway (see §6).
  • react / ai peer-dep warnings from agents 0.13 are harmless (this repo doesn't use react).

Changesets

  • centralize-account-id-resolution (minor, 13 account servers).
  • upgrade-agents-mcp-sdk-zod-ai (patch, all 18 apps).

mattzcarey added 11 commits June 1, 2026 20:27
Replace the tool-based account selection (accounts_list + set_active_account,
per-app getActiveAccountId/setActiveAccountId, UserDetails activeAccountId) with a
centralized AccountManager + a CloudflareMCPServer.accountTool() wrapper.

Resolution precedence (per call):
  1. Auth-pinned account — account-scoped token's account, or a single-account OAuth
     token (no account_id param exposed in this case).
  2. cf-account-id request header (user-configured) — multi-account tokens only.
  3. account_id tool argument — auto-appended only for multi-account tokens.

Multi-account credentials get their account list injected into the server initialize
instructions for discovery. All tool error responses now set isError: true.

- New: packages/mcp-common/src/account-manager.ts (3-layer resolver) + account-tool.ts
  (buildAccountTool wrapper core, kept ajv-free for testing) + specs (19 tests).
- CloudflareMCPServer gains accountTool(); collapse dead CloudflareMcpAgentNoAccount layer.
- Migrate all account-scoped shared + per-app tools to accountTool.
- Remove account.tools.ts, account.api.ts, constants.ts, accounts.eval.ts; update READMEs
  and implementation-guides/tools.md.

Supersedes #316.
Bumps across all packages: agents 0.2.19→0.13.3, @modelcontextprotocol/sdk
1.20.2→1.29.0, zod 3.24.2→4.4.3, ai 4.3.10→6.0.193 (+ @ai-sdk/* v3 providers,
ai-gateway-provider 3.1.3).

Migration fixes:
- zod 4: z.record(key, value) explicit key; z.string().ip() -> z.ipv4()/z.ipv6();
  drop removed objectOutputType (use z.infer<z.ZodObject<Shape>>).
- agents 0.13: McpAgent env generic constrained to Cloudflare.Env; api-handler /
  api-token-mode infer the env generic (no `any`).
- MCP SDK 1.29: flatten tool annotations to { title, readOnlyHint, ... } (fixes a
  latent bug where nested annotations were ignored).
- ai 6: eval tooling (LanguageModel, inputSchema, stopWhen/stepCountIs, tool-call input);
  MCPClientManager now takes a storage option.

Typecheck + lint green across all 19 packages. Changeset added for all servers.
The legacy `.tool()` API is deprecated in favour of `.registerTool(name, config, cb)`.

- CloudflareMCPServer now wraps BOTH `tool()` and `registerTool()` for metrics via a shared
  `trackCb` helper, so every registration path is tracked identically.
- accountTool() registers via `registerTool({ description, inputSchema, annotations }, cb)`;
  buildAccountTool's callback is typed as the SDK `ToolCallback<ZodRawShape>`, so the call is
  fully type-checked (removed the `@ts-ignore`).
- Converted all 78 remaining `.tool(...)` call sites across apps + shared tools to
  `.registerTool(...)` with the config-object form (61 in radar, 17 across 8 other files).

Typecheck + lint green across all 19 packages; account-manager + accountTool specs pass.
The two overrides spread `unknown[]` into the bound original methods (`_tool(name, ...rest)`),
which TS rejects with TS2556 (spread into an overloaded signature without a rest param) — the
forwarding is correct at runtime. Type the bound originals as variadic
`(...args: unknown[]) => ReturnType<McpServer['tool' | 'registerTool']>` so the spread is legal.
No suppression, no `any`. Typecheck + lint green.
agents 0.13 needs a newer local workerd than the pinned wrangler provided
(`cloudflare:workers` `exports`), so bump the dev/test toolchain:
- wrangler 4.10.0 → 4.96.0 (recent workerd; fixes `wrangler dev`)
- esbuild override 0.25.1 → 0.27.3 (required by wrangler 4.96)
- @cloudflare/vitest-pool-workers 0.8.14 → 0.12.0 (newest on esbuild 0.27 that
  keeps the vitest-3 `/config` API; avoids the vitest-4 config-rewrite migration)
- vitest 3.0.9 → 3.2.6, @vitest/ui → 3.2.6

All dev dependencies — no change to deployed runtime. Verified `wrangler dev` boots
the workers-observability server and tools resolve end-to-end; tests 116/116, types,
lint, deps, format all green.
Completes the test-toolchain upgrade (vitest 3.2 → 4.1.8, pool-workers 0.12 → 0.16.11),
using pool-workers' official `vitest-v3-to-v4` codemod + recipes.

- Ran the codemod across all vitest configs: `defineWorkersConfig`/`defineWorkersProject`
  → `defineConfig` from 'vitest/config' with the `cloudflareTest({...})` Vite plugin (the
  `poolOptions.workers` block moves into the plugin). Removed dropped options
  (`singleWorker`, `isolatedStorage`).
- Migrated the workspace: `vitest.workspace.ts` → root `vitest.config.ts` with `test.projects`.
- Added `"type": "module"` to the worker packages (pool-workers 0.16 main entry is ESM-only,
  so `.ts` configs must load as ESM).
- pool-workers 0.16 removed `fetchMock` from `cloudflare:test`: migrated the 4 specs that
  mocked api.cloudflare.com to MSW (per the official request-mocking recipe) — shared
  `src/test/msw-server.ts` + `msw-setup.ts`, `server.use(http.<m>(url, () => HttpResponse...))`.
- tsconfig `types`: `@cloudflare/vitest-pool-workers` → `/types` (cloudflare:test module moved).
- eval-tools: import `env` from `cloudflare:workers` (cloudflare:test `env` is deprecated);
  typed eval vars via a local `EvalEnv`; added missing credentials guard in getAnthropicModel.
- Deduped `@types/node` to 22.15.17 (override + declarations) to collapse a duplicate `vite`
  that made plugin types "unrelated".

All gates green: check:types + check:lint (37/37), tests (116), check:deps, check:format.
…r /mcp

- test-models: use ai-gateway-provider createUnified() with the gateway's
  stored (BYOK) keys instead of an empty per-provider apiKey, fixing the
  'Incorrect API key' failures. Judge: gpt-5.4-nano; subjects: gpt-5.4-mini,
  gpt-4.1, and workers-ai/@cf/moonshotai/kimi-k2.6.
- drop now-unused providers (@ai-sdk/openai, @ai-sdk/anthropic, @ai-sdk/google,
  workers-ai-provider) from eval-tools.
- eval clients connect to /mcp (token-mode servers only serve /mcp, not /sse).
- bump ai 6.0.193 -> 6.0.194.
The UserDetails DO existed only to persist activeAccountId across sessions
(added in #85) for the old set_active_account / accounts_list flow. Account
resolution is now derived per-request (auth-pinned -> cf-account-id header ->
account_id arg), so nothing reads or writes it — getUserDetails has no callers.

- delete user_details.do.ts and remove the USER_DETAILS binding, export, and
  Env type from all 13 account-scoped apps (dev + staging + production).
- add a deleted_classes DO migration (tag v2) to the two owning workers
  (workers-observability, workers-builds) so the namespace is torn down.
- strip the dead commented-out tool blocks in r2_bucket.tools.ts and
  hyperdrive.tools.ts (they referenced the removed getActiveAccountId API).
- regenerate worker-configuration.d.ts across apps (drops USER_DETAILS).
CI typecheck broke after regenerating worker-configuration.d.ts to the
wrangler 4.96 / workerd 1.20260529 runtime types. Fixes:

- remove '--include-env=false' from every app's 'types' script (and the
  run-wrangler-types helper). Under the new wrangler that flag also strips the
  global 'interface Env' the app/test code relies on (getEnv<Env>, McpAgent<Env>,
  vitest TestEnv extends Env); without it the global Env is emitted again.
- regenerate packages/mcp-common/worker-configuration.d.ts (was stuck at
  workerd 1.20250409, predating embeddinggemma in AiModels) so the docs-vectorize
  embeddings tool typechecks; drop the now-unnecessary @ts-expect-error directives.
- docs-* and sandbox-container: drop 'public' from the 'ctx: DurableObjectState'
  constructor params — the new DurableObjectState<unknown> default conflicts with
  the McpAgent/DurableObject base's DurableObjectState<{}>; inheriting it avoids
  the redeclaration (matches every other app).
- api-token-mode: ExecutionContext.props is now readonly; assign auth props via a
  typed mutable view (runtime sets props before serve()).
Vestigial leftover from the account-id centralization: 'activeAccountId' was
still declared in each agent's State (and one initialState) but never read or
written — account resolution is now derived per-request via AccountManager.

- drop the field from all 13 account-scoped apps' State (and workers-bindings'
  initialState); workers-builds keeps its still-used activeBuildUUID/activeWorkerId.
- mcp-common CloudflareMCPAgentState is now an empty base (Record<string, unknown>)
  so servers with their own state (workers-builds) stay assignable to CloudflareMcpAgent.
@mattzcarey mattzcarey merged commit f625075 into main Jun 1, 2026
6 checks passed
@mattzcarey mattzcarey deleted the feat/centralize-account-id-resolution branch June 1, 2026 22:50
mattzcarey added a commit that referenced this pull request Jun 2, 2026
#384 removed the UserDetails Durable Object from every server but only added the
delete-class migration to the two servers that own the class (workers-observability,
workers-builds). graphql's deployed worker still depends on UserDetails, so
`wrangler deploy` rejected the new version with code 10064 and aborted the staging
and production deploys; it also blocked workers-observability from applying its own
deleted_classes migration (code 10061).

Adding the delete-class migration lets graphql deploy and releases the binding.
Validated on staging: graphql + observability deploy cleanly and the staging
UserDetails namespace is removed.
@github-actions github-actions Bot mentioned this pull request Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant