Skip to content

Commit 3828da4

Browse files
committed
docs: per-device machine header and identity copy paths
Document X-Mcpmux-Machine-Id resolver behavior, client setup examples, and viewer modal / Settings surfaces for copying machine UUIDs and MCP headers. Signed-off-by: Joe Sangiorgio <jsangio1@gmail.com>
1 parent 7c60234 commit 3828da4

7 files changed

Lines changed: 179 additions & 20 deletions

File tree

docs/guide/clients.mdx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,16 @@ The tools an app sees are decided by the **folder it reports** (its MCP workspac
4545

4646
- Open your **backend repo** in any IDE and it sees your database and deploy tools; open a **docs folder** and it sees only search and filesystem.
4747
- Two different apps (say Cursor and VS Code) opening the **same folder** get the **same tools** — routing follows the folder, not the app's identity.
48-
- A session with **no reported folder** falls through to the **active Space's** FeatureSet.
48+
- A session with **no reported folder** and **no binding** gets **no backend tools** until you map the folder (deny-by-default).
4949

5050
To control an app's tools, map its folder in the **Workspaces** tab (or let the AI do it with [Tool Optimization](/docs/tool-optimization/)) rather than configuring the app itself.
5151

52+
### Multiple devices, one tunneled gateway
53+
54+
If Cursor on your laptop and Cursor on your desktop both hit the **same** remote gateway (e.g. `https://mcp.example.com/mcp`), add `X-Mcpmux-Machine-Id` to **each** device's MCP config with that device's machine UUID from **Settings → Machine Identity**. Without it, machine-scoped bindings may resolve as the gateway host, not the device you're actually on.
55+
56+
See [Remote Access](/docs/remote-access/) for the full headers example.
57+
5258
![A connected app — its toolset is decided by the Workspace binding for the folder it reports](https://mcpmux.com/screenshots/client-detail.png)
5359

5460
Open any app to see how it's currently routed and exactly which tools, prompts, and resources resolve for it.

docs/guide/gateway.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ ChatGPT ─────────┘ ├──→ PostgreS
1919
The gateway receives MCP JSON-RPC requests from clients and:
2020

2121
1. **Authenticates** the client using its access key
22-
2. **Resolves** the session's folder (its workspace root) to a Space + FeatureSet via the matching [Workspace](/docs/workspaces/) binding — falling back to the active Space when no folder is reported
22+
2. **Resolves** the session's folder (its workspace root) to a Space + FeatureSet via the matching [Workspace](/docs/workspaces/) binding — unmapped folders get no backend tools (deny-by-default)
2323
3. **Filters** the available tools, resources, and prompts to that resolved FeatureSet
2424
4. **Routes** each request to the correct backend MCP server
2525
5. **Returns** the response to the client
@@ -114,7 +114,7 @@ The Tauri app resolves `apps/desktop/dist` when the admin server starts. MCP cli
114114

115115
Remote MCP and tunneled web admin are **optional**. The gateway stays on loopback; you expose it with Cloudflare Tunnel (or similar) and Cloudflare Access at the edge.
116116

117-
For a high-level capability map — two-hostname layout, auth layers, Settings knobs, and client config shape — see [Remote Access](/docs/remote-access/).
117+
For a high-level capability map — two-hostname layout, auth layers, Settings knobs, client config shape, and **per-device machine headers** for multi-device tunnel setups — see [Remote Access](/docs/remote-access/).
118118

119119
## Next Steps
120120

docs/guide/remote-access.mdx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,28 @@ Point the client at your public MCP URL instead of localhost:
5959
"url": "https://mcp.example.com/mcp",
6060
"headers": {
6161
"CF-Access-Client-Id": "<service-token-id>",
62-
"CF-Access-Client-Secret": "<service-token-secret>"
62+
"CF-Access-Client-Secret": "<service-token-secret>",
63+
"X-Mcpmux-Machine-Id": "<machine-uuid-from-mcpmux-settings>"
6364
}
6465
}
6566
}
6667
}
6768
```
6869

70+
When multiple physical devices (e.g. a Mac Studio and a laptop) share one tunneled gateway, add `X-Mcpmux-Machine-Id` on **each** device's config so machine-scoped workspace bindings resolve correctly. Copy the UUID from **Settings → Machine Identity** in McpMux.
71+
72+
### Per-device identity (shared tunnel)
73+
74+
One gateway install can serve MCP for several physical machines over a tunnel. The gateway host always knows its own identity (`gateway.local_machine_id`), but it cannot tell which laptop made a given request unless the client says so.
75+
76+
| Situation | What to do |
77+
| --------- | ---------- |
78+
| MCP on `localhost:45818` on the same box as McpMux | No extra header — `local_machine_id` is enough |
79+
| Cursor on a **different** machine hitting a tunneled URL | Add `X-Mcpmux-Machine-Id: <that-machine-uuid>` to that device's MCP config |
80+
| Binding scoped to machine A only | Requests with machine B's header (or no header when B is remote) stay **unbound** until you create a binding for B |
81+
82+
In **Settings → Machine Identity → Manage all machines**, use **Copy MCP header** to get a ready-made JSON snippet for Cursor or other HTTP MCP clients.
83+
6984
Service tokens are created in **Zero Trust → Access → Service auth**. The MCP and admin Access applications each need a policy that accepts your token (or your SSO identity for browser admin use).
7085

7186
After the first connection, approve OAuth in the McpMux consent UI (desktop or web admin, depending on your setup).

docs/guide/workspaces.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ Open the **Workspaces** tab and click **Add mapping**:
4040

4141
Unmapped folders receive no tools until you map them — an explicit, fail-closed default so a new project never silently inherits another's access.
4242

43+
## Machine-scoped bindings
44+
45+
Bindings can be scoped to a **specific machine** (your Mac Studio, laptop, cloud agent) instead of applying globally. A path bound only on **Gondor** does not grant tools when the session is treated as **Rohan** — you get the bind prompt instead.
46+
47+
**Local MCP** (`localhost:45818`): the gateway knows which install it is via `gateway.local_machine_id`; machine-scoped bindings resolve automatically.
48+
49+
**Shared tunnel** (one gateway, many devices): each device's MCP config must send its machine identity so the gateway can tell devices apart:
50+
51+
```json
52+
{
53+
"headers": {
54+
"X-Mcpmux-Machine-Id": "<machine-uuid>"
55+
}
56+
}
57+
```
58+
59+
Copy the snippet from **Settings → Machine Identity → Manage all machines → Copy MCP header**. See [Remote Access](/docs/remote-access/) for the full tunneled client example.
60+
4361
The FeatureSet you pick is the exact toolset the folder resolves to — choose which tools, prompts, and resources from each server it's allowed to use:
4462

4563
![The FeatureSet a folder resolves to — pick exactly which tools each server contributes](https://mcpmux.com/screenshots/featureset-detail.png)
@@ -58,3 +76,4 @@ The pin is a Workspace mapping — it sticks for every future session from that
5876
- [FeatureSets](/docs/feature-sets/) — the curated toolsets a Workspace grants
5977
- [Tool Optimization](/docs/tool-optimization/) — let the AI map folders for itself
6078
- [Clients](/docs/clients/) — how connected apps report the folder that drives routing
79+
- [Remote Access](/docs/remote-access/) — per-device machine header when using a shared tunneled gateway

docs/planning/deny-by-default-bindable-callers.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Three coupled problems, all introduced or sharpened by PR #175 (`7fc50a0`).
3232
| 2 | Source of truth | The **`WorkspaceBinding` is canonical**; reported root, client identity, and machine are *ranked match signals*, none mandatory. Absence of all → `Unbound`. | Decouples the feature from `roots` without ripping out roots support. The deprecation just removes one signal from the priority list someday — not a rewrite. |
3333
| 3 | Invocation gate | A caller with no matching binding (`Unbound`) gets **zero backend tools**, including via `mcpmux_invoke_tool`. Meta management/discovery/bind tools always work. | "No calls without a representation on the page." The `mcpmux_*` tools that let the LLM/user *create* a representation (search, get_schema, list_servers, set_workspace_root, bind) stay callable so you can always dig out. |
3434
| 4 | Rootless callers | A rootless client with no grant resolves to `Unbound` and surfaces as a **bindable card keyed on client identity** (Option C). Every caller — folder or client — has a card and must be bound. | Anchors trust on mcpmux's existing per-client access keys (reliable even when roots aren't). Unifies the two surfaces under one rule. Matches the industry virtual-key pattern. |
35-
| 5 | Bound-elsewhere | A path bound only on another machine resolves to `Unbound` on this machine (`deny_here`). | Consistent with deny-by-default. The user adds a machine-scoped (or adopts via `workspace-binding-project-adopt`) binding for this machine to enable tools. |
35+
| Bound-elsewhere | A path bound only on another machine resolves to `Unbound` on this machine (`deny_here`). | Consistent with deny-by-default. The user adds a machine-scoped (or adopts via `workspace-binding-project-adopt`) binding for this machine to enable tools. Tunneled multi-device: set `X-Mcpmux-Machine-Id` on each remote client — see [`per-device-machine-header.md`](./per-device-machine-header.md). |
3636
| 6 | Unmapped card visibility | A detected-but-unbound folder **still appears as a card**, in an explicit "denied / bind to enable tools" state. The card *is* how you create the representation. | The CTA and the representation are the same surface — hiding it would make the deny a dead-end. |
3737
| 7 | Card aggregation | **One card per project path**, per-machine breakdown inside it. Badges shift relative to the viewer; the card's *content* does not change with the machine dropdown. | Matches "used from Gondor but not Rohan." The bottom routing table (`EntryCardRoutingTable`) is the bones for the per-machine *binding* axis. Per-machine *usage* attribution is a data gap (see Architecture). |
3838
| 8 | Starter fate | Keep Starter as an **opt-in default bundle** the user can explicitly bind, surfaced during onboarding — never the silent fallback. | The user should see, at setup, that default = nothing, and be offered the Starter bundle as a deliberate choice. |
@@ -302,6 +302,7 @@ pnpm lint # ESLint + cargo clippy --workspace -- -D warnings
302302

303303
- [`projects-grouped-machine-cards.md`](./projects-grouped-machine-cards.md) — one-card-per-path `Entry.bindings[]`, `EntryCardRoutingTable`, machine rows — the card bones this builds on
304304
- [`workspace-binding-project-adopt.md`](./workspace-binding-project-adopt.md) — cross-machine adopt flow + `live-unbound` badge; the path to enabling a bound-elsewhere folder on this machine
305-
- [`workspace-machine-binding.md`](./workspace-machine-binding.md) — machine CRUD, `machine_id` on bindings, 3-tier resolver lookup
305+
- [`workspace-machine-binding.md`](./workspace-machine-binding.md) — machine CRUD, `machine_id` on bindings, resolver machine lookup
306+
- [`per-device-machine-header.md`](./per-device-machine-header.md)`X-Mcpmux-Machine-Id` for tunneled multi-device routing
306307
- [SEP-2577 — Deprecate Roots, Sampling, Logging](https://modelcontextprotocol.io/seps/2577-deprecate-roots-sampling-and-logging) — why the binding (not roots) must be canonical
307308
- [MCP client feature support matrix](https://modelcontextprotocol.info/docs/clients/) — which clients report roots (Claude Desktop/Code yes; Cursor/VS Code inconsistent; browser clients no)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Per-Device Machine Identity Header
2+
3+
**Last Updated:** Jun 30, 2026
4+
**Status:** Implemented (Jun 30, 2026)
5+
**Branch:** `feat/workspace-machine-binding`
6+
**Depends on:** `workspace-machine-binding.md`, `deny-by-default-bindable-callers.md`
7+
**Unblocks:** Machine-scoped bindings work when multiple physical devices share one tunneled gateway
8+
9+
---
10+
11+
## Problem
12+
13+
When Cursor on Rohan reaches the gateway on Gondor via a shared tunnel (`gateway.public_url`), the resolver cannot tell which physical device made the request. Tier 1 falls back to `inbound_clients.machine_id` (static, set once at OAuth consent) and then `gateway.local_machine_id` (always Gondor). A path bound only on Gondor matches even when the caller is on Rohan.
14+
15+
## Decision
16+
17+
Add optional per-request header `X-Mcpmux-Machine-Id: <machine-uuid>` in each device's MCP client config. Gateway reads it as the highest-priority machine signal in Tier 1 binding lookup.
18+
19+
## Resolver priority (Tier 1 machine scoping)
20+
21+
**Header absent** (unchanged):
22+
23+
```text
24+
1. inbound_clients.machine_id (static OAuth client tag)
25+
2. gateway.local_machine_id (this install)
26+
3. global binding (machine_id IS NULL)
27+
```
28+
29+
**Header present** (`X-Mcpmux-Machine-Id` with valid UUID):
30+
31+
```text
32+
1. Header machine id only
33+
2. global binding (machine_id IS NULL)
34+
```
35+
36+
Client and gateway-local machine tags are skipped when the header is set, so a tunneled Rohan caller is not mistaken for Gondor.
37+
38+
## Files modified
39+
40+
| File | Change |
41+
| ---- | ------ |
42+
| `crates/mcpmux-gateway/src/mcp/context.rs` | `OAuthContext.request_machine_id`; parse header |
43+
| `crates/mcpmux-gateway/src/services/feature_set_resolver.rs` | `resolve(..., request_machine_id)`; header-first lookup |
44+
| `crates/mcpmux-gateway/src/services/authorization.rs` | Forward `request_machine_id` |
45+
| `crates/mcpmux-gateway/src/mcp/handler.rs` | Thread through routing + binding prompts |
46+
| `tests/rust/tests/integration/feature_set_resolver.rs` | Header outranks client/local; deny when only other machine bound |
47+
| `apps/desktop/src/features/settings/SettingsPage.tsx` | Copy MCP header snippet per machine; MachineIdSection on viewer card |
48+
| `apps/desktop/src/components/ViewerIdentity.tsx` | MachineIdSection in viewer modal (status bar → edit) |
49+
| `apps/desktop/src/components/machine-id-section.component.tsx` | Shared machine ID display, dual copy, paste-to-link |
50+
| `apps/desktop/src/lib/machine-id.helpers.ts` | UUID validation, MCP header snippet builder, clipboard helper |
51+
| `apps/desktop/src/hooks/use-viewer-identity.hook.tsx` | `linkMachineById` for paste-to-link existing catalog rows |
52+
| `apps/desktop/src/locales/en/common.json` | Viewer modal machine ID + copy/link strings |
53+
| `apps/desktop/src/locales/en/settings.json` | Copy header + copy UUID toast strings |
54+
| `crates/mcpmux-gateway/src/services/meta_tools/meta_tool_common.rs` | Pass `None` for header (meta tools have no HTTP context) |
55+
| `crates/mcpmux-gateway/src/services/meta_tools/set_workspace_root.rs` | Pass `None` for header |
56+
| `crates/mcpmux-gateway/src/consumers/mcp_notifier.rs` | Pass `None` for header (session fan-out) |
57+
| `docs/guide/remote-access.mdx` | Example config with optional machine header |
58+
59+
## Implementation notes
60+
61+
- Malformed header values are ignored; full client → local → global chain applies.
62+
- When header is present (valid UUID), client and gateway-local machine tags are **not** consulted.
63+
- Tests: `request_machine_header_outranks_client_and_local_machine`, `request_machine_header_enables_deny_when_only_other_machine_bound`.
64+
- Validated Jun 30, 2026: 331 integration tests, clippy clean, desktop typecheck clean.
65+
66+
## Client setup
67+
68+
On each physical device, add to that device's MCP client config (alongside Cloudflare Access headers if used):
69+
70+
```json
71+
{
72+
"headers": {
73+
"X-Mcpmux-Machine-Id": "<machine-uuid-from-settings>"
74+
}
75+
}
76+
```
77+
78+
Copy the snippet from **Settings → Machine Identity** (viewer card or **Manage all machines**) or from the **status bar viewer modal** (click `Viewer: …`**Copy MCP header**). **Copy UUID** is also available on all three surfaces.
79+
80+
## See also
81+
82+
- [Remote Access](/docs/remote-access/) — tunneled MCP client config (CF Access + machine header)
83+
- [Workspaces](/docs/workspaces/) — machine-scoped bindings
84+
- [Clients](/docs/clients/) — multi-device tunnel setup
85+
- [workspace-machine-binding.md](./workspace-machine-binding.md) — machine catalog and binding model
86+
87+
```bash
88+
pnpm test:rust
89+
pnpm typecheck
90+
pnpm lint
91+
```

0 commit comments

Comments
 (0)