Skip to content

feat(bridge): let every devup-mcp on the machine share the plugin - #78

Merged
owjs3901 merged 2 commits into
mainfrom
owjs3901/B1-bridge-multi-client
Sep 26, 2026
Merged

owjs3901 merged 2 commits into
mainfrom
owjs3901/B1-bridge-multi-client

Conversation

@owjs3901

Copy link
Copy Markdown
Contributor

One devup-mcp runs per MCP client session, and only one of them can hold the port the plugin's manifest allows (ws://localhost:1993). Measured on one machine: eight devup-mcp processes, the plugin attached to one of them, and a session that needed Figma - a different process - reported listening: false and could read only through the metered direct path. Its only remedy was to kill another session's process.

What changes

One host, the others relay. The process holding the port is the host; the others connect to its new /relay endpoint and send their reads through it. Request ids are assigned by the host, so answers reach only the process that asked, and attachedFiles and the selection read the same in every process.

The port passes on, and so does a collection. When the host exits, a remaining process takes the port over (the OS gives it to exactly one) and the plugin re-attaches on its own two-second retry. A read in flight at that moment is sent again once the plugin is back - reads do not change the document - so the collection finishes instead of failing. It fails only if the plugin is not back within 10 s, and a read whose plugin window closed fails at once instead of after 90 s.

Plugin: sessionId and devup-cancel. The plugin names its window with a sessionId, so a plugin that cannot report its file key (Dev Mode) keeps its routing key across reconnects and handovers. A read nobody waits for any more - the process that asked went away, or it timed out - is withdrawn with devup-cancel: dropped from the plugin's queue, or its answer withheld if already running. Older plugin builds ignore both and keep working.

Who holds the port. devup_figma_auth status reports paths.bridge.role (host, relay, connecting, unavailable), the host's pid/version/buildId, handoverFrom during a handover, and for an unusable port an issue (legacy-host, foreign-program, incompatible-protocol, authentication-failed, ...) with the step that fixes it. A holder that does not identify itself is named by pid, process name and executable path as the OS reports them (netstat/tasklist/PowerShell, lsof/ps, /proc) - added after the first answer, so status never waits on the lookup.

Same user only. Both sides prove knowledge of a per-user secret with HMAC over fresh nonces; the secret never crosses the wire. It is %USERPROFILE%\AppData\Local\devup-mcp\bridge-relay.key on Windows and /tmp/devup-mcp-<uid>/bridge-relay.key on macOS and Linux - by user id, not HOME, because clients hand their servers different HOMEs. The directory must be the user's own with mode 0700; an open one is closed and its secret replaced, and one owned by someone else, or a link, is refused. /relay refuses any Origin; /plugin admits only the plugin's null origin and figma.com.

Pacing. Reads the bridge serves are no longer held to the metered pace (DEVUP_FIGMA_CALLS_PER_MINUTE, eight a minute), which made a second export on the same server wait most of a minute for an allowance it was not spending.

No port for self-checks. --self-check and servers built in-process for tests no longer open the bridge.

Compatibility

  • A single process behaves and answers as before; DEVUP_FIGMA_BRIDGE_PORT keeps its meaning.
  • A devup-mcp from before sharing that holds the port is reported as legacy-host, and a new process takes the port over when it exits.
  • The changepack is keyed to crates/devup-mcp/Cargo.toml (Minor, 0.12.0 -> 0.13.0), which the release job tags and builds from.

Verification

  • Windows: cargo test --workspace 120 suites, 1247 passed, 0 failed, 2 ignored; cargo clippy --workspace --all-targets --all-features -D warnings, cargo fmt --check, cargo insta test --check and the node tests (17) clean.
  • Linux (WSL, Ubuntu 22.04): clippy on the whole workspace clean; the figma crate's unit tests (secret, /proc lookup), bridge_relay, bridge_transport, and devup-mcp's bridge_handover, bridge_relay, bridge_first pass. With the previous HOME-based secret location, both handover tests fail.
  • Real binaries on random ports: port bound again 0.5 ms after its holder was killed, the plugin re-attached after 2.02 s, and an export whose read was in flight finished 2.03 s after the kill. With the released 0.12.0 holding the port, the first status answered legacy-host in 4 ms and named the holder (pid, executable) 0.87 s later. /plugin: https://example.com and http://localhost:3000 -> 403; null, https://www.figma.com and no Origin -> 101.
  • plugin/tests/withdraw.test.mjs (now in CI) passes against the new bundle; against the previous one, 2 of its 3 cases fail.

Known limits

  • Origin: null is also what sandboxed iframes and file:// pages send, so those are not kept off /plugin.
  • On Unix, another user who creates /tmp/devup-mcp-<uid> first blocks relaying for this user (secret-unavailable) rather than obtaining the secret.
  • The real Figma plugin was not driven end to end here - port 1993 on the test machine belongs to running sessions; a stand-in plugin speaking the same protocol was.

The plugin can only reach the one port its manifest allows, and one devup-mcp
per MCP client session is normal, so every process but the first reported
`listening: false` and could read Figma only through the metered direct path -
with the plugin attached and serving, the only remedy was to kill another
session's process.

The process holding the port is now the host; the others relay through its new
`/relay` WebSocket. The host assigns every plugin `requestId`, so reads from
several processes never collide and each answer returns only on the connection
that asked; the attached-file list (page and selection included) is pushed to
every relay, so `attachedFiles` reads the same everywhere. When the host exits,
a remaining process binds the port at once (the OS gives it to exactly one) and
the plugin re-attaches on its own 2-second retry. Reads in flight fail at once
instead of after 90 s, and a host drops the reads of a relay that went away.

Relaying is limited to the same user's devup-mcp: both sides prove, by HMAC
over fresh nonces, that they hold a secret kept in a user-only file; the secret
never crosses the wire, a relay handshake carrying `Origin` (a browser page) is
refused before the upgrade, and nothing listens beyond 127.0.0.1. The handshake
carries a protocol version and an incompatible peer is refused, not trusted.

`devup_figma_auth` status/doctor report `paths.bridge.role` (host, relay,
connecting, unavailable), the holder's pid/version/buildId, `handoverFrom`
while the port changes hands, and for a port that cannot be read through an
`issue` - `legacy-host` for a devup-mcp from before sharing, `foreign-program`,
`incompatible-protocol`, `authentication-failed` - with the step that fixes it,
within seconds. `available` is true only when a read can be sent now; a relay
that reaches the plugin never suggests logging in.

Tests that spawn the binary now turn the bridge off, so they never touch the
machine's real port 1993.
… sharing gaps

- A read in flight when the port's holder leaves is sent again once the
  plugin is back, so the collection finishes; a read whose plugin window
  closed fails at once instead of after 90 s.
- The plugin names its window with a sessionId, so a keyless (Dev Mode)
  plugin keeps its routing key across reconnects, and it honours
  devup-cancel: a read nobody waits for is dropped from its queue, or its
  answer withheld if already running. Older builds ignore both.
- A holder that does not identify itself (a devup-mcp from before sharing,
  another program) is named by pid, name and executable path from the OS,
  after status has answered.
- The Unix relay secret lives in /tmp/devup-mcp-<uid>/, chosen by user id
  rather than HOME, which clients set differently; the directory must be
  the user's own and 0700.
- /plugin admits only the plugin's null origin and figma.com.
- Reads the bridge serves are not held to the metered pace.
- --self-check and in-process servers no longer open the bridge.
- The changepack is keyed to crates/devup-mcp/Cargo.toml, which the release
  job tags and builds from.
@owjs3901
owjs3901 merged commit b4fff26 into main Sep 26, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant