Skip to content

feat: stop sending agents to log in while the bridge is attached - #76

Merged
owjs3901 merged 10 commits into
mainfrom
feat/bridge-first-connectivity
Sep 25, 2026
Merged

owjs3901 merged 10 commits into
mainfrom
feat/bridge-first-connectivity

Conversation

@owjs3901

Copy link
Copy Markdown
Contributor

An agent asked to "fetch one UI and show devup-ui code" had the Devup Bridge plugin attached on port 1993 and serving, concluded Figma was unreachable, and asked its user to log in. Every step of that came from the server.

The cause

  1. devup_figma_auth {"action":"status"} answered "status":"disconnected" - a word that described only the OAuth path. The bridge state appeared only under doctor, which nothing calls first.
  2. The tool description ("Check, start, or clear Figma Remote MCP OAuth ..."), the server instructions and devup://guide/usage never mentioned the bridge, so "reaching Figma" read as "OAuth".
  3. devup_figma_export without a url failed with Either url or artifactId is required. and no next step, although one attached plugin could have served it. The agent invented /design/bridge/bridge to get past the field.
  4. The plugin could not report its file key (Dev Mode), so it served the invented key because it was the only one attached - and the answer said source.kind: "direct" and echoed the invented key as the file's.

What changes

Status is path-aware. status, login and logout return one report:

{ "connected": true, "status": "connected", "activePath": "bridge",
  "paths": { "bridge": { "attachedFiles": [{ "fileKey": null, "fileName": "Landing",
      "currentPage": { "id": "0:1", "name": "Page 1" },
      "selection": [{ "id": "1:2", "name": "Hero", "type": "FRAME" }], "selectionCount": 1 }] },
    "direct": { "available": false, "tokenState": "absent" } },
  "nextAction": { "tool": "devup_figma_export", "arguments": { "outputs": ["tsx"] } } }

status keeps its key and reads disconnected only when neither path can serve. doctor is the same report plus the reference data. The plugin now reports its current page and selection on attach and on every change.

No url needed with one plugin attached. devup_figma_export, devup_figma_search and devup_figma_explore take no url: they read that plugin's file, and export and explore start from the node selected in Figma. frameIds without url name frames in it - one id is that frame, several are screens of the selected Section. figma-bridge://current[?node-id=] is the official placeholder where a link is still needed, and every link devup-mcp returns for a file whose key is unknown uses it. With no plugin or several, the refusal carries a nextAction: run the plugin, log in and pass a link, or pick one of the attached files.

Provenance is honest. An answer the plugin served says source.kind: "bridge" with bridgePort, fileName, pageName and bridgeReads. source.fileKey is only ever the key the plugin reported - null with fileKeyReason when it reported none - and a key the request carried is reported apart as requestedFileKey. The key a keyless plugin is routed by names its connection, never reaches the direct path, and never appears in an answer.

Login warns rather than refuses while a plugin is attached: "A bridge is attached; login is only needed for file-scope metadata or referencePng."

Everything an agent reads first says it: the devup_figma_auth description, rule 1a "Connection order: bridge -> direct" in the instructions, a full section in devup://guide/usage, the README, and the MCPB manifest, which told hosts that connecting to Figma is a browser OAuth login.

Also fixed on the way

  • Plugins were registered by file key, so two that could not report one collapsed into a single slot: the second replaced the first, and the first one's disconnect erased the second. "Exactly one plugin attached" could not be counted. They are tracked per connection now.
  • --merge-asset-batches required source.fileKey; bridge batches that honestly say null merge by the plugin's file name instead, and a different name is still a different file.

Compatibility

  • status no longer answers {"status": ...} alone - the key stays, with the report beside it.
  • doctor's paths.bridge.attachedFiles entries are objects rather than file-key strings.
  • url is optional in the devup_figma_search and devup_figma_explore input schemas.
  • The instructions ceiling moves from 1,200 to 1,400 bytes for rule 1a.

A dedicated devup_figma_connection tool was considered and not added: another tool is a schema every client carries on every session, and a rename breaks existing callers. The description and the report carry it instead.

Verification

  • cargo test --workspace --all-features, cargo clippy --workspace --all-targets --all-features -- -D warnings, cargo fmt --all -- --check, plugin typecheck and a reproducible npm run build, and mcpb validate on the manifest.
  • New contract tests (crates/devup-mcp/tests/bridge_first.rs) drive the tool surface with a real bridge socket and a simulated plugin across bridge only, direct only and neither: status in each state, a url-less export through the selection, a caller's key the plugin cannot vouch for, the refusals and their nextAction, url-less search and explore, and the login warning.
  • The debug binary was driven over stdio with a simulated keyless plugin on the real socket: status said connected via the bridge, and devup_figma_export {"outputs":["tsx"]} returned TSX with source: {kind: "bridge", fileKey: null, fileKeyReason, fileName, pageName, bridgePort, bridgeReads: 3}.
  • Not yet run inside the Figma desktop app itself.

owjs3901 and others added 10 commits September 25, 2026 22:07
…nk through one helper

A link devup-mcp hands back has to route to the same place. Three identical private canonical_url helpers become FigmaTarget::link, which keeps the Figma URL for a Figma file and answers figma-bridge://current for a file only the bridge can read - never a Figma URL around a key no Figma file has. figma-bridge://current[?node-id=] parses to a bridge-only placeholder the server binds to the attached plugin.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The plugin now sends its current page and selected nodes (the first 20, plus the total) with hello, and again on every selectionchange and currentpagechange. devup-mcp uses them to show what an attached plugin has open, and to resolve a call made without a url to the node selected in Figma. dist/ is rebuilt from this source; the build is reproducible.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… answered

Plugins were registered by file key, so two that could not report one shared the empty key: the second replaced the first, the first one's disconnect erased the second, and 'exactly one plugin attached' could not be counted. They are tracked per connection now, with the page and selection each reports. A plugin that could not report its key is reached through a connection-scoped key that never falls through to the direct path. Every bridge answer carries which plugin gave it, the collector keeps that with the acquisition, and paths.bridge.attachedFiles lists the file name, page and selection of each plugin.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…ng an unneeded login

status used to be the direct path's word alone, so with a plugin attached and serving it still answered disconnected, and an agent asked its user to log in. status, login and logout now return the path-aware report - connected, activePath, both paths, and the next call to make - and keep the status key, which reads disconnected only when neither path can serve. login still runs while a plugin is attached, and says it was probably not needed.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…y the plugin did not report

Every answer said source.kind direct whatever carried it, and repeated the request's key as the file's - including a key invented to get past the url field, which a keyless plugin served because it was the only one attached. An answer the plugin served now says kind bridge with bridgePort, fileName, pageName and bridgeReads; fileKey is the key the plugin reported, or null with fileKeyReason, and a key the request carried is reported apart as requestedFileKey. A reprojected acquisition keeps kind artifact and says origin bridge. sourceMap and the links in recovery and resume arguments follow the same rule.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
--merge-asset-batches established a batch's file from source.fileKey alone, so a batch the bridge served from a plugin that could not report its key - fileKey null rather than a stand-in - would be refused. Such batches are now compared by the file name the plugin gave, kept under source.fileName in the merged summary, and a different name is still a different file.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…when a call gives no url

A call without url was refused as 'url or artifactId is required' while a plugin sat attached and ready, and the agent that met it invented /design/bridge/bridge. With exactly one plugin attached, devup_figma_export, devup_figma_search and devup_figma_explore now take no url: they read that plugin's file, export and explore start from the node selected in Figma, and frameIds without url name frames in it. figma-bridge://current names the same file. With no plugin or several, the refusal carries a nextAction listing the ways forward; a selection that names no single node gets the call that would. The contract tests drive the tool surface with a real bridge socket across bridge-only, direct-only and neither.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…instructions and the guide

Rule 1a says it where every session reads it: call devup_figma_auth status before asking anyone to log in, the bridge needs no login, and with one plugin attached an export takes no url. devup://guide/usage carries the full rule - both paths, url-less calls, figma-bridge://current, the refusals, and how an answer names the bridge. The instructions ceiling moves from 1,200 to 1,400 bytes for it.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… manifest

The bundle manifest told hosts that connecting to Figma is a browser OAuth login; it now names the bridge first. The README documents the path-aware status, url-less calls with figma-bridge://current, the refusals and their nextAction, and the bridge provenance in source.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Minor for crates/devup-mcp/Cargo.toml.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@owjs3901
owjs3901 merged commit f82081c into main Sep 25, 2026
9 checks passed
@owjs3901
owjs3901 deleted the feat/bridge-first-connectivity branch September 25, 2026 13:26
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