Background
#3250 stopped persisting activeOrganizationId to the session row (it was leaking across browser tabs) and switched to a per-request x-org-id header. That fix works for normal MCP fetches via the SDK but exposed a class of bugs every time a callsite forgets the header:
- The OAuth probe in
isConnectionAuthenticated was a hand-rolled fetch without x-org-id → proxy returned 403, supportsOAuth came back false, and the OAuth popup never opened.
- The save path
POST /api/connections/:id/oauth-token had the same gap → token landed on connection.connection_token (fallback) instead of DownstreamTokenStorage, breaking buildCloneInfo ("No GitHub token found").
/api/vm-events and /org/:orgId/watch are SSE — EventSource can't set custom headers at all, so the header convention couldn't carry org context. We worked around it with a GET-only query-param fallback (?x-org-id=...).
Each of these was patched individually. The pattern is: the header convention is invisible from the URL, easy to forget, and structurally incompatible with EventSource.
Proposal
Move org context into the URL path for every org-scoped backend route, removing the need for the x-org-id header (and the SSE query-param workaround).
There's already precedent in the codebase:
/org/:organizationId/watch
/org/:organizationId/events/:type
/api/:org/models/*
After the migration:
/mcp/:connectionId → /:org/mcp/:connectionId
/api/connections/:id/oauth-token → /:org/api/connections/:id/oauth-token
/api/vm-events?... → /:org/api/vm-events?... (no more x-org-id query param)
- ...and the rest of the org-scoped surface for consistency.
authenticateRequest resolves the org from the path segment instead of the header; membership is verified the same way. Multi-tab safety comes from the URL itself — different tabs are different URLs.
Decisions to make
- Scope — narrow (just the routes that currently break or are fragile: proxy
/mcp/*, /api/connections/:id/oauth-token*, the two SSE routes) or sweep (all org-scoped routes: /api/org-sso, /api/files, decopilot, KV, vm-events, etc.). Sweeping gives one consistent shape; narrow keeps the diff small.
- Slug vs id in the path — the frontend URL already uses the slug (
/gimenes-local/...); reusing it reads nicer and matches what users see. authenticateRequest's membership query already supports either.
- Compatibility window — keep the old paths aliased and
x-org-id header working during a deprecation window (matters for API-key consumers of /mcp/:connectionId and /api/connections/...)? Or hard cut?
Touched surface (rough)
Backend (Hono routes): apps/mesh/src/api/app.ts, apps/mesh/src/api/routes/proxy.ts, downstream-token.ts, vm-events.ts, plus the routes mounted under /api/....
SDK + frontend fetch helpers:
packages/mesh-sdk/src/hooks/use-mcp-client.ts — drop the x-org-id header, build org-scoped URL.
packages/mesh-sdk/src/lib/mcp-oauth.ts — isConnectionAuthenticated and checkOAuthTokenStatus.
- All
oauth-token POST/DELETE callsites in apps/mesh/src/web/....
- The three SSE consumers (
vm-events-context.tsx, use-decopilot-events.ts, use-workflow-sse.ts).
Existing tests that assert the 403 cross-tenant guard (apps/mesh/src/api/routes/proxy.test.ts) need to be updated for the new path shape.
Out of scope
The interim fixes already in the codebase (per-callsite x-org-id headers, the GET-only query-param fallback in authenticateRequest) stay until this migration lands.
Background
#3250 stopped persisting
activeOrganizationIdto the session row (it was leaking across browser tabs) and switched to a per-requestx-org-idheader. That fix works for normal MCP fetches via the SDK but exposed a class of bugs every time a callsite forgets the header:isConnectionAuthenticatedwas a hand-rolledfetchwithoutx-org-id→ proxy returned 403,supportsOAuthcame backfalse, and the OAuth popup never opened.POST /api/connections/:id/oauth-tokenhad the same gap → token landed onconnection.connection_token(fallback) instead ofDownstreamTokenStorage, breakingbuildCloneInfo("No GitHub token found")./api/vm-eventsand/org/:orgId/watchare SSE —EventSourcecan't set custom headers at all, so the header convention couldn't carry org context. We worked around it with a GET-only query-param fallback (?x-org-id=...).Each of these was patched individually. The pattern is: the header convention is invisible from the URL, easy to forget, and structurally incompatible with
EventSource.Proposal
Move org context into the URL path for every org-scoped backend route, removing the need for the
x-org-idheader (and the SSE query-param workaround).There's already precedent in the codebase:
/org/:organizationId/watch/org/:organizationId/events/:type/api/:org/models/*After the migration:
/mcp/:connectionId→/:org/mcp/:connectionId/api/connections/:id/oauth-token→/:org/api/connections/:id/oauth-token/api/vm-events?...→/:org/api/vm-events?...(no morex-org-idquery param)authenticateRequestresolves the org from the path segment instead of the header; membership is verified the same way. Multi-tab safety comes from the URL itself — different tabs are different URLs.Decisions to make
/mcp/*,/api/connections/:id/oauth-token*, the two SSE routes) or sweep (all org-scoped routes:/api/org-sso,/api/files, decopilot, KV, vm-events, etc.). Sweeping gives one consistent shape; narrow keeps the diff small./gimenes-local/...); reusing it reads nicer and matches what users see.authenticateRequest's membership query already supports either.x-org-idheader working during a deprecation window (matters for API-key consumers of/mcp/:connectionIdand/api/connections/...)? Or hard cut?Touched surface (rough)
Backend (Hono routes):
apps/mesh/src/api/app.ts,apps/mesh/src/api/routes/proxy.ts,downstream-token.ts,vm-events.ts, plus the routes mounted under/api/....SDK + frontend fetch helpers:
packages/mesh-sdk/src/hooks/use-mcp-client.ts— drop thex-org-idheader, build org-scoped URL.packages/mesh-sdk/src/lib/mcp-oauth.ts—isConnectionAuthenticatedandcheckOAuthTokenStatus.oauth-tokenPOST/DELETE callsites inapps/mesh/src/web/....vm-events-context.tsx,use-decopilot-events.ts,use-workflow-sse.ts).Existing tests that assert the 403 cross-tenant guard (
apps/mesh/src/api/routes/proxy.test.ts) need to be updated for the new path shape.Out of scope
The interim fixes already in the codebase (per-callsite
x-org-idheaders, the GET-only query-param fallback inauthenticateRequest) stay until this migration lands.