Skip to content

Commit c0e3196

Browse files
committed
feat(gateway): log workspace pin/session signals for Agents Window spike
Surface session_id, X-Mcpmux-Workspace, pin/clobber, and resolved root so we can prove whether Agents Window shares MCP sessions across workspaces. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 58f3852 commit c0e3196

5 files changed

Lines changed: 78 additions & 19 deletions

File tree

crates/mcpmux-gateway/src/mcp/handler.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,14 @@ impl McpMuxGatewayHandler {
109109
.await
110110
{
111111
Ok(resolved) => {
112+
let workspace_root = session_id
113+
.and_then(|sid| services.session_roots.get(sid))
114+
.and_then(|roots| roots.into_iter().next())
115+
.unwrap_or_else(|| "<none>".into());
112116
info!(
113117
%client_id,
114118
session_id = session_id.unwrap_or("<none>"),
119+
workspace_root = %workspace_root,
115120
feature_set_ids = ?resolved.feature_set_ids,
116121
space_id = resolved.space_id.map(|u| u.to_string()).unwrap_or_else(|| "<none>".into()),
117122
source = ?resolved.source,

crates/mcpmux-gateway/src/mcp/oauth_middleware.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ pub async fn mcp_oauth_middleware(
245245
// client can claim any binding (see FeatureSetResolver trust model). Keyed
246246
// by the `mcp-session-id` the client echoes on every post-initialize
247247
// request (the same key the handler stores reported roots under).
248-
let pin = {
248+
let (session_id_header, workspace_header) = {
249249
let headers = request.headers();
250250
let sid = headers
251251
.get("mcp-session-id")
@@ -255,10 +255,20 @@ pub async fn mcp_oauth_middleware(
255255
.get("x-mcpmux-workspace")
256256
.and_then(|v| v.to_str().ok())
257257
.map(str::to_owned);
258-
sid.zip(ws)
258+
(sid, ws)
259259
};
260-
if let Some((sid, ws)) = pin {
261-
services.session_roots.set_pinned(&sid, &ws);
260+
match (&session_id_header, &workspace_header) {
261+
(Some(sid), Some(ws)) => {
262+
services.session_roots.set_pinned(sid, ws);
263+
}
264+
(None, Some(ws)) => {
265+
warn!(
266+
trace_id = %trace_id,
267+
workspace_header = %ws,
268+
"[SessionRoots] X-Mcpmux-Workspace present without mcp-session-id — pin skipped",
269+
);
270+
}
271+
_ => {}
262272
}
263273

264274
// Extract MCP method from body if POST
@@ -276,6 +286,8 @@ pub async fn mcp_oauth_middleware(
276286
trace_id = %trace_id,
277287
client = %&client_id[..client_id.len().min(12)],
278288
space = %&space_id.to_string()[..8],
289+
session_id = session_id_header.as_deref().unwrap_or("<none>"),
290+
workspace_header = workspace_header.as_deref().unwrap_or("<absent>"),
279291
method = method.as_deref().unwrap_or("-"),
280292
"→ MCP"
281293
);

crates/mcpmux-gateway/src/server/logging_middleware.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ fn redact_headers_compact(headers: &axum::http::HeaderMap) -> String {
4646
| "mcp-session-id"
4747
| "mcp-protocol-version"
4848
| "last-event-id"
49+
| "x-mcpmux-workspace"
50+
| "x-mcpmux-machine-id"
4951
)
5052
})
5153
.map(|(name, value)| {

crates/mcpmux-gateway/src/services/session_roots.rs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::time::{Duration, Instant};
1414

1515
use dashmap::DashMap;
1616
use mcpmux_core::normalize_workspace_root;
17-
use tracing::debug;
17+
use tracing::{info, warn};
1818

1919
use super::tool_discovery::ToolIndex;
2020

@@ -216,23 +216,31 @@ impl SessionRootsRegistry {
216216
/// header falls back to the client's reported roots rather than denying.
217217
/// Cheap to call on the request hot path: redundant writes (same
218218
/// normalized value already pinned) are skipped to avoid shard churn.
219+
///
220+
/// Logs at info on first pin, warn when the same session is re-pinned to a
221+
/// different root (Agents Window / shared-session cross-workspace clobber).
219222
pub fn set_pinned(&self, session_id: &str, raw_root: &str) {
220223
let normalized = normalize_workspace_root(raw_root);
221224
if normalized.is_empty() {
222225
return;
223226
}
224-
if self
225-
.pinned
226-
.get(session_id)
227-
.is_some_and(|v| *v == normalized)
228-
{
229-
return;
227+
if let Some(previous) = self.pinned.get(session_id) {
228+
if *previous == normalized {
229+
return;
230+
}
231+
warn!(
232+
%session_id,
233+
previous = %previous.as_str(),
234+
new = %normalized,
235+
"[SessionRoots] X-Mcpmux-Workspace pin clobber — same session, different root",
236+
);
237+
} else {
238+
info!(
239+
%session_id,
240+
workspace_root = %normalized,
241+
"[SessionRoots] pinned explicit workspace root from X-Mcpmux-Workspace header",
242+
);
230243
}
231-
debug!(
232-
%session_id,
233-
workspace_root = %normalized,
234-
"[SessionRoots] pinned explicit workspace root from X-Mcpmux-Workspace header",
235-
);
236244
self.pinned.insert(session_id.to_string(), normalized);
237245
}
238246

docs/planning/cursor-workspace-routing-bridge.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Cursor Workspace Routing via Global `mcp-remote` Bridge
22

3-
**Last Updated:** Jul 20, 2026
4-
**Status:** Complete (Phases 1–3 on `dev-rebased`)
3+
**Last Updated:** Jul 24, 2026
4+
**Status:** Complete (Phases 1–3) — Agents Window multi-workspace spike **pending** (observability logs shipped)
55
**Branch:** `dev-rebased`
66

77
### Phase 1 spike results (Jul 20, 2026)
@@ -10,9 +10,39 @@
1010
- **Gateway:** `localhost:45818` up (`0.5.0`).
1111
- **Auth + connect:** `phase1-spike-bridge` client reached gateway; machine-naming dialog appeared and was approved.
1212
- **Remaining manual QA:** two-window `${workspaceFolder}` routing not yet verified in real Cursor; transport/auth path is confirmed.
13+
1314
**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)
1415
**Unblocks:** Zero-maintenance Cursor workspace routing — no per-repo files, no agent cooperation required
1516

17+
### Agents Window multi-workspace spike (Jul 24, 2026)
18+
19+
**Hypothesis:** Cursor Agents Window groups agents by workspace in the UI, but may share one MCP session / mis-resolve `${workspaceFolder}` across workspaces, so mux cannot pin the correct root→FeatureSet binding. Gondor-local + global bridge config is already the intended Editor path; this spike proves what Agents Window actually sends.
20+
21+
**Observability (shipped):** gateway logs now include:
22+
23+
| Signal | Where | Level |
24+
| ------ | ----- | ----- |
25+
| `session_id` + `workspace_header` on every MCP POST | `oauth_middleware` `→ MCP` | info |
26+
| Workspace header without `mcp-session-id` (pin skipped) | `oauth_middleware` | warn |
27+
| First pin / same-session root clobber | `session_roots.set_pinned` | info / warn |
28+
| `workspace_root` on resolve | `handler` `[FeatureSetResolver] resolved` | info |
29+
| `x-mcpmux-workspace` / `x-mcpmux-machine-id` in DEBUG request headers | `logging_middleware` | debug |
30+
31+
**Repro (Gondor):**
32+
33+
1. Rebuild/restart the desktop gateway so the new logs are live.
34+
2. In Agents Window, start one agent under workspace A and one under workspace B (both already machine-bound on Gondor, e.g. `mcp-mux` vs `sync2hire-platform`).
35+
3. From each agent, call any `mcpmux_*` tool (e.g. `mcpmux_list_servers` or `mcpmux_search_tools`).
36+
4. Grep gateway logs: `SessionRoots`, `workspace_header`, `pin clobber`, `→ MCP`, `[FeatureSetResolver] resolved`.
37+
38+
**Pass:** distinct `session_id` values; each `workspace_header` / resolved `workspace_root` matches that agent's workspace; no `pin clobber` warn.
39+
40+
**Fail:** shared `session_id` with `pin clobber` (previous ≠ new), or `workspace_header=<absent>`, or header present without session id.
41+
42+
**Next if fail:** prefer per-repo static `.cursor/mcp.json` header for Agents Window, and/or treat as Cursor Agents Window MCP binding gap (not a new per-agent identity axis).
43+
44+
**Spike results:** _pending manual run_
45+
1646
---
1747

1848
## Problem
@@ -161,7 +191,9 @@ Removes the "hand-assemble JSON" friction so the bridge is actually usable by so
161191
| File | Note |
162192
| ---- | ---- |
163193
| [`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 |
194+
| [`crates/mcpmux-gateway/src/services/session_roots.rs`](../../crates/mcpmux-gateway/src/services/session_roots.rs) | `X-Mcpmux-Workspace` pin is authoritative; Agents Window spike adds pin/clobber info+warn logs |
195+
| [`crates/mcpmux-gateway/src/mcp/oauth_middleware.rs`](../../crates/mcpmux-gateway/src/mcp/oauth_middleware.rs) | `→ MCP` logs `session_id` + `workspace_header`; warns when pin skipped |
196+
| [`crates/mcpmux-gateway/src/mcp/handler.rs`](../../crates/mcpmux-gateway/src/mcp/handler.rs) | Resolver resolved log includes `workspace_root` |
165197
| [`docs/manual/workspace-header-routing.md`](../manual/workspace-header-routing.md) | Documents the underlying Cursor `roots`-reporting bug this bridge works around |
166198
| [`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 |
167199
| [`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 |

0 commit comments

Comments
 (0)