Skip to content

Commit 1b970f9

Browse files
committed
test(cursor-bridge): Phase 1 — Manual spike results
Autonomous decisions: - GO for Phase 2 with partial automation — machine-naming dialog confirms bridge auth/connect; two-window ${workspaceFolder} test deferred to manual QA Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent cf69d3e commit 1b970f9

1 file changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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

Comments
 (0)