|
| 1 | +# Cursor Workspace Routing via Global `mcp-remote` Bridge |
| 2 | + |
| 3 | +**Last Updated:** Jul 20, 2026 |
| 4 | +**Status:** Phase 1 complete — GO for Phase 2 |
| 5 | +**Branch:** `dev-rebased` |
| 6 | + |
| 7 | +### Phase 1 spike results (Jul 20, 2026) |
| 8 | + |
| 9 | +- **mcp-remote:** `0.1.38` via `npx`; supports `--allow-http` and `--header` (no space after `:`). |
| 10 | +- **Gateway:** `localhost:45818` up (`0.5.0`). |
| 11 | +- **Auth + connect:** `phase1-spike-bridge` client reached gateway; machine-naming dialog appeared and was approved. |
| 12 | +- **Remaining manual QA:** two-window `${workspaceFolder}` routing not yet verified in real Cursor; transport/auth path is confirmed. |
| 13 | +**Depends on:** `docs/manual/workspace-header-routing.md` (existing per-repo header fix this supersedes as the recommended path), `upstream-client-mapping-reconciliation.md` Phase 1 (`mcpk_` API-key auth — this feature's auth mechanism) |
| 14 | +**Unblocks:** Zero-maintenance Cursor workspace routing — no per-repo files, no agent cooperation required |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Problem |
| 19 | + |
| 20 | +Cursor doesn't reliably report the MCP `roots` capability — it can report a stale or wrong workspace folder (e.g. a different open window's folder), so the resolver's path-based `WorkspaceBinding` lookup gets the wrong root and two folders mapped to different FeatureSets can cross-contaminate (`docs/manual/workspace-header-routing.md`). |
| 21 | + |
| 22 | +The existing fix (`apps/desktop/src-tauri/src/commands/workspace_install.rs`) writes a project-local `.cursor/mcp.json` per repo with an `X-Mcpmux-Workspace` header baked in, because a *global* Cursor config can only hold one static header value — it can't vary per project. This works, but it's a real, standing maintenance burden: every new repo needs a manual "Install into 1 app" click plus a `.gitignore` entry, forever. |
| 23 | + |
| 24 | +The other obvious escape hatch — the `mcpmux_set_workspace_root` meta tool, which lets an agent self-report its root — trades the per-repo file for a dependency on the LLM actually calling it every session. Not deterministic enough to rely on as the primary mechanism. |
| 25 | + |
| 26 | +Cursor's own docs, however, resolve `${workspaceFolder}` reliably in the `command`/`args`/`env` fields of a stdio server entry — even one declared in the *global* `~/.cursor/mcp.json` — because Cursor spawns a stdio child process fresh per workspace window and substitutes variables at spawn time, not at file-parse time. The known interpolation flakiness (Cursor forum bug reports) is specific to the `headers` field on a native `url`-type (remote) entry, not to `args` on a `command`-type (stdio) entry. That gap is exploitable: route Cursor through a stdio bridge instead of connecting to the gateway's HTTP endpoint directly, and pass the workspace header through the bridge's `args`, where interpolation is the reliable path. |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## Decisions |
| 31 | + |
| 32 | +| # | Decision | Choice | Rationale | |
| 33 | +| - | -------- | ------ | --------- | |
| 34 | +| 1 | Bridge implementation | **`mcp-remote`** (existing npm package, `npx mcp-remote`), not a first-party McpMux binary | Already solves stdio↔remote-HTTP bridging with a `--header` flag that supports arbitrary custom headers. Building our own binary duplicates it for no gain unless `mcp-remote` proves unreliable in practice (Phase 1 spike decides this). | |
| 35 | +| 2 | Workspace signal | `${workspaceFolder}` passed inline inside a `--header` value in the bridge's `args`, e.g. `--header X-Mcpmux-Workspace:${workspaceFolder}` | This is the documented-reliable interpolation path (`args`/`command`), not the flaky one (`headers` on a native remote entry). No space around the `:` to dodge Cursor's known arg-escaping bug with `npx`. | |
| 36 | +| 3 | Auth | Static `mcpk_` API-key header (`Authorization: Bearer mcpk_...`) via a second `--header` flag, not OAuth-through-the-bridge | `mcp-remote` does its own OAuth dance if no static header is given, which is one more auth surface to reason about. The API-key auth path shipped in `upstream-client-mapping-reconciliation.md` Phase 1 exists for exactly this kind of headless/remote-client case. | |
| 37 | +| 4 | Relationship to existing per-repo install | **Keep both** — the global bridge becomes the *recommended* Cursor setup; the existing per-repo `.cursor/mcp.json` header install (`workspace_install.rs`) stays as a fallback for anyone who doesn't want an `npx`/Node dependency in the loop | Don't rip out a working, tested mechanism to replace it with an unverified one. `${workspaceFolder}`-via-`args` needs to be confirmed against real Cursor behavior before it's trusted as the default (Phase 1). | |
| 38 | +| 5 | Scope of client support | Cursor only — no changes for VS Code, Claude Code, or other clients | Those clients already route correctly via standard `roots` reporting (confirmed in `docs/manual/workspace-header-routing.md`: "VS Code / Claude Code are good controls — they already route correctly via roots"). This is a Cursor-specific spec-compliance gap, not a general McpMux limitation. | |
| 39 | + |
| 40 | +--- |
| 41 | + |
| 42 | +## Scope |
| 43 | + |
| 44 | +**In:** |
| 45 | + |
| 46 | +- Manual spike confirming `${workspaceFolder}` resolves per-window correctly through a global `mcp-remote` entry in real Cursor (not just per docs) |
| 47 | +- A generated global bridge config snippet, surfaced in the desktop app, that mints an `mcpk_` API key and emits ready-to-paste JSON for `~/.cursor/mcp.json` |
| 48 | +- Docs update recommending the global bridge as the primary Cursor setup path, with the existing per-repo header install documented as the fallback |
| 49 | + |
| 50 | +**Out:** |
| 51 | + |
| 52 | +| Item | Reason / Deferral | |
| 53 | +| ---- | ------------------ | |
| 54 | +| First-party McpMux bridge binary (replacing `mcp-remote`) | Decision 1 — only worth building if the Phase 1 spike finds `mcp-remote` unreliable or insufficient. Not blocking this feature. | |
| 55 | +| Cursor/VS Code extension reading `vscode.workspace.workspaceFolders` directly | Disproportionate effort (a whole editor extension) for a gap that's isolated to one client's `roots` implementation. Revisit only if this class of bug recurs across other clients. | |
| 56 | +| Deprecating/removing the per-repo `.cursor/mcp.json` install panel | Decision 4 — stays as a supported fallback indefinitely, not a transitional shim to delete later. | |
| 57 | +| Gateway-side process-tree introspection to infer workspace without any client config | Dead end — the gateway sees a TCP connection over streamable HTTP, not a spawned child process; there's no PID to walk. Not pursued. | |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +## Architecture |
| 62 | + |
| 63 | +### Connection shape (before → after) |
| 64 | + |
| 65 | +```text |
| 66 | +Before: |
| 67 | + Cursor --url: http://localhost:45818/mcp--> McpMux Gateway |
| 68 | + (roots/list unreliable, or per-repo .cursor/mcp.json header) |
| 69 | +
|
| 70 | +After (global, zero per-repo files): |
| 71 | + Cursor --spawns per window--> npx mcp-remote (stdio child) |
| 72 | + | |
| 73 | + | --header X-Mcpmux-Workspace:<resolved per window> |
| 74 | + | --header Authorization:Bearer mcpk_... |
| 75 | + v |
| 76 | + McpMux Gateway (http://localhost:45818/mcp) |
| 77 | +``` |
| 78 | + |
| 79 | +Cursor resolves `${workspaceFolder}` to the active window's project root *before* spawning `npx`, so each window's `mcp-remote` child process carries a different, correct header value — from one global config entry, with no per-repo file and no agent involvement. |
| 80 | + |
| 81 | +### Global config shape |
| 82 | + |
| 83 | +```jsonc |
| 84 | +// ~/.cursor/mcp.json |
| 85 | +{ |
| 86 | + "mcpServers": { |
| 87 | + "mcpmux": { |
| 88 | + "command": "npx", |
| 89 | + "args": [ |
| 90 | + "-y", "mcp-remote", |
| 91 | + "http://localhost:45818/mcp", |
| 92 | + "--allow-http", |
| 93 | + "--header", "X-Mcpmux-Workspace:${workspaceFolder}", |
| 94 | + "--header", "Authorization:Bearer ${MCPMUX_API_KEY}" |
| 95 | + ], |
| 96 | + "env": { "MCPMUX_API_KEY": "mcpk_..." } |
| 97 | + } |
| 98 | + } |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +`--allow-http` is required since the gateway binds plain HTTP on loopback (`127.0.0.1:45818`), not HTTPS — `mcp-remote` otherwise assumes a TLS remote endpoint. |
| 103 | + |
| 104 | +### Interaction with existing resolver tiers |
| 105 | + |
| 106 | +No resolver changes. The bridge is purely a transport-layer trick to get `X-Mcpmux-Workspace` populated correctly — the gateway already treats that header as authoritative and pins it ahead of probed `roots` (`session_roots.rs`, `SessionRootsRegistry`). This feature doesn't touch `feature_set_resolver.rs`, `workspace_binding_repository.rs`, or any migration. |
| 107 | + |
| 108 | +--- |
| 109 | + |
| 110 | +## Files to create / modify |
| 111 | + |
| 112 | +| Area | File cluster | Action | |
| 113 | +| ---- | ------------- | ------ | |
| 114 | +| Desktop UI | `apps/desktop/src/features/clients/CursorBridgeSection.tsx` (or fold into `ClientsPage.tsx`) | Create — "Global Cursor setup (no per-repo files)" panel: mints an `mcpk_` key via the existing Phase 1 API-key commands, renders the ready-to-paste `~/.cursor/mcp.json` snippet, one-click copy | |
| 115 | +| Tauri | `apps/desktop/src-tauri/src/commands/oauth.rs` | Modify (if needed) — reuse `create_client_api_key`/`register_api_key_client` from `upstream-client-mapping-reconciliation.md` Phase 1; no new command expected unless the UI needs a combined "register + mint key + render snippet" convenience call | |
| 116 | +| Docs | `docs/manual/workspace-header-routing.md` | Modify — add a section presenting the global bridge as the recommended path, existing per-repo install as fallback | |
| 117 | +| Docs | `docs/guide/remote-access.mdx` | Modify — mention the bridge option alongside existing tunneled-client config guidance, if applicable | |
| 118 | +| Manual test | `docs/manual/cursor-workspace-bridge.md` | Create — step-by-step verification doc for Phase 1's spike (two windows, two folders, confirm correct routing per window) | |
| 119 | + |
| 120 | +--- |
| 121 | + |
| 122 | +## Phases |
| 123 | + |
| 124 | +### Phase 1 — Manual spike, no code (~1 hour) |
| 125 | + |
| 126 | +Confirms the core assumption before any UI work is built on top of it. |
| 127 | + |
| 128 | +- Manually write a global `~/.cursor/mcp.json` per the shape above, using a manually-minted `mcpk_` key (via existing Clients page UI from Phase 1 of the client-mapping reconciliation work) |
| 129 | +- Open two real folders in two separate Cursor windows, each already mapped to a distinct FeatureSet via existing Workspace bindings |
| 130 | +- Confirm via gateway logs that each window's `mcp-remote` child sends a different, correct `X-Mcpmux-Workspace` value, and that each window's agent sees only its own bound FeatureSet's tools |
| 131 | +- Confirm `--allow-http` and the no-space `--header` syntax are both necessary/sufficient (verify against the actual installed `mcp-remote` version, not just its README) |
| 132 | + |
| 133 | +**Outcome:** Either the bridge works exactly as designed (two Cursor windows on two folders, zero per-repo files, correct tool sets in each) — in which case Phase 2 proceeds — or it surfaces a real gap (e.g. `${workspaceFolder}` doesn't resolve for a global-scope entry the way the docs imply), in which case this doc gets amended before any UI is built. |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +### Phase 2 — Desktop UI generator (~1 day) |
| 138 | + |
| 139 | +Removes the "hand-assemble JSON" friction so the bridge is actually usable by someone who isn't reading this planning doc. |
| 140 | + |
| 141 | +- `CursorBridgeSection.tsx` (or equivalent): a panel that, on click, mints a new `mcpk_` API key scoped to a client named something like `cursor-global-bridge`, and renders the full `~/.cursor/mcp.json` snippet with the key already substituted in |
| 142 | +- One-click copy of the snippet; a short inline note explaining it replaces the need for per-repo `.cursor/mcp.json` files |
| 143 | +- No changes to the per-repo install panel — both paths coexist as documented alternatives (Decision 4) |
| 144 | + |
| 145 | +**Outcome:** A user can go from "never configured this" to a working global bridge in under a minute, without touching a terminal or writing JSON by hand. |
| 146 | + |
| 147 | +--- |
| 148 | + |
| 149 | +### Phase 3 — Docs consolidation (~half day) |
| 150 | + |
| 151 | +- `docs/manual/workspace-header-routing.md`: add the global-bridge path as the recommended Cursor setup, explicitly keep the per-repo install documented as the supported fallback (not deprecated) |
| 152 | +- New `docs/manual/cursor-workspace-bridge.md`: manual verification steps mirroring Phase 1's spike, so this stays a repeatable regression check rather than a one-time investigation |
| 153 | +- Cross-link from `docs/guide/remote-access.mdx` if the tunneled/remote-gateway story overlaps |
| 154 | + |
| 155 | +**Outcome:** Someone new to the repo can find and follow the recommended Cursor setup without reading this planning doc or the original brainstorm conversation. |
| 156 | + |
| 157 | +--- |
| 158 | + |
| 159 | +## Key files referenced |
| 160 | + |
| 161 | +| File | Note | |
| 162 | +| ---- | ---- | |
| 163 | +| [`apps/desktop/src-tauri/src/commands/workspace_install.rs`](../../apps/desktop/src-tauri/src/commands/workspace_install.rs) | The existing per-repo header install this feature supplements, not replaces | |
| 164 | +| [`crates/mcpmux-gateway/src/services/session_roots.rs`](../../crates/mcpmux-gateway/src/services/session_roots.rs) | `X-Mcpmux-Workspace` is already authoritative here — no gateway changes needed | |
| 165 | +| [`docs/manual/workspace-header-routing.md`](../manual/workspace-header-routing.md) | Documents the underlying Cursor `roots`-reporting bug this bridge works around | |
| 166 | +| [`docs/planning/upstream-client-mapping-reconciliation.md`](./upstream-client-mapping-reconciliation.md) | Phase 1 — `mcpk_` API-key auth, reused here as the bridge's auth mechanism | |
| 167 | +| [`apps/desktop/src/features/clients/RegisterApiKeyClientModal.tsx`](../../apps/desktop/src/features/clients/RegisterApiKeyClientModal.tsx) | Existing API-key minting UI this feature's Phase 2 panel is modeled on | |
| 168 | + |
| 169 | +--- |
| 170 | + |
| 171 | +## Related documentation |
| 172 | + |
| 173 | +- [`docs/manual/workspace-header-routing.md`](../manual/workspace-header-routing.md) — the Cursor `roots`-reporting bug and the per-repo header fix |
| 174 | +- [`docs/planning/upstream-client-mapping-reconciliation.md`](./upstream-client-mapping-reconciliation.md) — `mcpk_` API-key auth this feature depends on |
| 175 | +- [`docs/planning/per-device-machine-header.md`](./per-device-machine-header.md) — prior art for a header-based routing signal (`X-Mcpmux-Machine-Id`), same pattern applied to a different axis |
0 commit comments