feat(bridge): let every devup-mcp on the machine share the plugin - #78
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 - reportedlistening: falseand 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
/relayendpoint and send their reads through it. Request ids are assigned by the host, so answers reach only the process that asked, andattachedFilesand 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:
sessionIdanddevup-cancel. The plugin names its window with asessionId, 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 withdevup-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_authstatus reportspaths.bridge.role(host,relay,connecting,unavailable), the host's pid/version/buildId,handoverFromduring a handover, and for an unusable port anissue(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.keyon Windows and/tmp/devup-mcp-<uid>/bridge-relay.keyon macOS and Linux - by user id, notHOME, because clients hand their servers differentHOMEs. 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./relayrefuses anyOrigin;/pluginadmits only the plugin'snullorigin andfigma.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-checkand servers built in-process for tests no longer open the bridge.Compatibility
DEVUP_FIGMA_BRIDGE_PORTkeeps its meaning.legacy-host, and a new process takes the port over when it exits.crates/devup-mcp/Cargo.toml(Minor, 0.12.0 -> 0.13.0), which the release job tags and builds from.Verification
cargo test --workspace120 suites, 1247 passed, 0 failed, 2 ignored;cargo clippy --workspace --all-targets --all-features -D warnings,cargo fmt --check,cargo insta test --checkand the node tests (17) clean./proclookup),bridge_relay,bridge_transport, and devup-mcp'sbridge_handover,bridge_relay,bridge_firstpass. With the previousHOME-based secret location, both handover tests fail.legacy-hostin 4 ms and named the holder (pid, executable) 0.87 s later./plugin:https://example.comandhttp://localhost:3000-> 403;null,https://www.figma.comand 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: nullis also what sandboxed iframes andfile://pages send, so those are not kept off/plugin./tmp/devup-mcp-<uid>first blocks relaying for this user (secret-unavailable) rather than obtaining the secret.