---
date: 2026-05-13
status: implemented-in-worktree
---

# MCP-first v2 migration on a separate namespace

The v2 turn-network protocol substrate is in place (M4 wire path, CommitScopeDO
persistence with delta saves, cost-budget guards). This note captures the
decision and implementation shape for making **MCP the first v2 consumer**,
deployed onto a **fresh Cloudflare namespace** (`woah.inguz.workers.dev`) where
v1 compatibility is not required.

The legacy production namespace stays on v1 unchanged. The new namespace is the
v2 lab: MCP traffic, soak-test load, real CF write-budget validation.

## Why MCP first, not the browser UI

E2E testability is the immediate driver. MCP is JSON-RPC over HTTP-streamable:
a CI test can open an MCP session, drive a tool call, assert the resulting
observation, and assert that the commit went through `CommitScopeDO` — all in
~100ms per round-trip with plain `fetch`. The browser equivalent needs
Playwright, a Vite-built worker bundle, IndexedDB inspection, and render
assertions; an order of magnitude more harness for the same protocol coverage.

The architectural reason it's also correct: MCP agents are conceptually the
same shape as the future M5 infrastructure nodes (narrow-auth consumers that
observe and invoke verbs without holding durable state). The McpGateway
becoming a v2 client is a template for what a GCP infrastructure node will
look like. Building MCP↔v2 is building the pattern that's reused for
cross-infra federation later. Browser-first wouldn't give us that.

Risk profile is also gentler. A v2 bug behind MCP shows up as "some agent
sees stale state" — recoverable, observable in metrics, low blast radius. A v2
bug behind the browser UI is visible breakage to every user.

## Why a separate namespace, not the production one

Two reasons compound:

- v1 paths inside `src/core/` are dormant on a pure-v2 namespace because no v1
  client connects. The production namespace still needs them. Forking the
  namespaces lets v2 evolve at its own pace without v1 regressions.
- The cost-budget guards (`tests/worker/v2-cost-budget.test.ts`) assert
  expected per-turn write counts at unit-test scale. Validating those against
  *real* CF billing requires real traffic; the new namespace is the place to
  generate that traffic without affecting production users.

The namespace is **MCP-only** (or close to it). The browser UI on this
namespace would not work — it consumes v1 frames; the namespace doesn't emit
them under v2-authoritative scopes. Browsers continue to hit the legacy
namespace. The new namespace can omit the SPA bundle entirely or serve a
placeholder/debug page; the wire surface that matters is `/v2/turn-network/ws`,
`/v2/session/mint`, `/mcp`, `/healthz`.

## Hybrid v1+v2 in McpGateway is not built

An earlier plan was a hybrid McpGateway that accepted both v1 `AppliedFrame`
and v2 `ShadowCommitAccepted`, with a per-scope routing flag. Since v1
compatibility is not required on the new namespace, the hybrid is skipped:

- McpGateway becomes a pure v2 consumer on this namespace.
- Single observation source: v2 only.
- All world/object verb tool calls route through `/v2/envelope`; MCP queue and
  focus controls remain gateway-local protocol controls.
- No per-scope feature flag plumbing.

Drops ~3 days of work and avoids carrying a dual-source plumbing layer that
would be deleted later anyway.

## Integration shape

Three pieces inside `src/mcp/gateway.ts` and immediate surroundings:

1. **v2 observation source.** McpGateway opens an internal subscription to
   `CommitScopeDO` for each scope it cares about. Receives accepted frames and
   transcript tails; converts to MCP notifications. Drops the v1 hookpoints
   (`routeAppliedFrame`, `routeLiveEvents`) on this namespace.
2. **Internal v2 token minting.** When an MCP HTTP-streamable session opens,
   the gateway mints a v2 session token scoped to the MCP-authenticated actor.
   The bearer is held by the gateway and attached to subsequent
   internal-to-CommitScopeDO calls. Avoids exposing v2 tokens to MCP clients.
3. **Tool call routing.** World/object verb invocations send
   `woo.turn.exec.request.shadow.v1` envelopes through `/v2/envelope`
   (gateway DO → CommitScopeDO). The MCP client never sees the v2 wire surface
   directly. MCP queue/focus controls (`wait`, `focus`, `unfocus`,
   `focus_list`) stay local because they are session/queue protocol state, not
   durable world commits.

## Live plane goes unexercised by MCP

Catalog live events (chat preview, cursor moves, dubspace knob feedback) are
browser concerns. MCP agents don't typically subscribe to live observations.
So the MCP-first namespace validates the commit/state planes but leaves the
live plane on substrate tests until the browser UI migrates.

This is acceptable: the live-plane substrate tests already exist; the wire
path for live envelopes is transport of an already-validated shape. The first
live-plane production exercise can wait for the browser UI work.

## Browser worker continues running silently

`src/client/v2-browser-worker.ts` and `src/client/v2-browser-cache.ts` remain
in the codebase and continue to bundle into the SPA. On the **legacy**
namespace they keep running silently against the v1 UI — open the v2
WebSocket, persist hello/reply state into IndexedDB, but drive no rendering.

This is fine: it exercises the wire path under real user load without
affecting user-visible behavior. When the time comes to migrate the browser UI
to v2, the worker is already battle-tested for reconnect, idempotency-key
behavior, and E_RESET handling.

## Sequencing

1. **Deploy the namespace** per the deploy-blocker checklist
   ([2026-05-13 namespace deploy notes pending]):
   `[env.woah]` block uncommented, secrets provisioned, CORS allowlist
   updated, `npm run cf:migrations:check` green.
2. **Ship pure-v2 MCP integration**: implemented in `src/mcp/gateway.ts` and
   `src/mcp/host.ts`, with `tests/v2-mcp-e2e.test.ts` covering MCP initialize,
   `woo_call`, v2 commit persistence, and accepted-frame fan-out to `woo_wait`.
3. **Soak the namespace**: run MCP-driven workloads (Claude Code, scripted
   agents). Validate `tests/worker/v2-cost-budget.test.ts` expectations
   against real CF write counts via the dashboard's `rowsWritten` metric.
4. **Pick a small catalog to flip on the legacy namespace** (probably `note`
   or one of the demo catalogs) once MCP-against-v2 has been operating
   cleanly. Hybrid plumbing in the McpGateway *would* be added then if the
   legacy namespace needs MCP+v2 coexistence — but only if a real use case
   appears.
5. **Browser UI migration** follows once one or more catalogs are
   v2-authoritative on the legacy namespace and the wire path is proven.

## What this delays

Not blocking, but consciously deferred:

- **Live-plane production exercise** — until browser UI migration.
- **UI consumption of v2 observations** — until per-catalog UI rewrite.
- **Hybrid McpGateway** — until the legacy namespace needs MCP+v2.
- **Cross-trust federation** (M5) — independent track.

## What this proves

If MCP-first lands cleanly, it validates:

- v2 wire path under real load (not just substrate tests).
- CommitScopeDO row-write budget at production scale.
- The "agent-shaped narrow-auth consumer" pattern that M5 infrastructure
  nodes will reuse.
- Token minting / auth handoff between gateway and CommitScopeDO.
- The substrate's M2 catch-up tail behavior when MCP sessions reconnect.

That's a strong baseline for the bigger migrations that follow.
