|
| 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