Skip to content

refactor: narrow what devup-mcp asks for and what it sends back - #8

Merged
owjs3901 merged 2 commits into
mainfrom
refactor/narrow-the-surface
Sep 8, 2026
Merged

refactor: narrow what devup-mcp asks for and what it sends back#8
owjs3901 merged 2 commits into
mainfrom
refactor/narrow-the-surface

Conversation

@owjs3901

@owjs3901 owjs3901 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

See the two commit messages for the full reasoning. Summary:

Tools 9 -> 7. devup_figma_to_ui and devup_figma_to_json were devup_figma_export with one entry in outputs. Every client carried three schemas and had to choose between them on each call.

sourcePolicy removed. auto and direct both meant the direct connection; it never branched, and it was part of a cache key it could not affect.

Closed sets published in the schema. action, scope, rootLayout, delivery, match, project-context scope, stack-diff layers - one shared constant read by both the schema and the parser.

Caller mistakes are INVALID_PARAMS. Everything used to arrive as INTERNAL_ERROR, so "you passed a scope this tool does not have" and "Figma stopped answering" were indistinguishable at the protocol level.

Response stops restating itself. Measured: a tsx-only export 2,411 -> 1,405 B; the part carried regardless 1,911 -> 994 B. fidelity/completenessReport are sent when they disagree with a clean grade or when includeDiagnostics asks. deliverable stays - a consumer relies on it.

A lost token is no longer silent. A binding whose variable/style the catalog never named was resolved to a raw value with nothing reported, inside a response graded exact. It now raises DEVUP_CODEGEN_TOKEN_NAME_UNRESOLVED and stops quality.projection reading exact. Generated bytes unchanged; 268 plugin goldens untouched.

Releases stop shipping devup-mcp-visual (harness builds it from source; nothing downloads it), and Cargo.lock is committed at the released version, which stops every release binary reporting its build id as -dirty.

Three things were costing every caller on every call: two tools that were
one tool, a parameter that never branched, and a response that restated
itself. Measured rather than guessed - a tsx-only export was 2,411 bytes
of which 1,911 was carried regardless of what was asked for.

## Nine tools become seven

`devup_figma_to_ui` and `devup_figma_to_json` were `devup_figma_export`
with a single entry in `outputs`. Every client carried three schemas in
its context and had to choose between them on each call, and the export
tool's own description spent a sentence saying which to prefer. Use
`outputs: ["tsx"]` and `outputs: ["devupJson"]`.

Removing them also deletes ~190 lines from `complete_operation`, which
had grown to just over a thousand.

## `sourcePolicy` is gone

`auto` and `direct` both meant the direct connection; the parameter
never branched. It offered a caller nothing but a chance to get it
wrong, and it was part of the artifact cache key it could not affect.

## Closed sets are published in the schema

`action`, `scope`, `rootLayout`, `delivery`, `match`, the project
context `scope` and the stack diff `layers` all arrived as bare strings,
so a caller learned the accepted values one rejection at a time.
`outputs` and the asset `format` already published theirs, and the
constant behind `outputs` says in its own doc comment that this is the
point. Each set is now one constant that both the schema and the parser
read, so the two cannot drift.

## A caller mistake is INVALID_PARAMS

Every error reached the caller as `INTERNAL_ERROR`, so "you passed a
scope this tool does not have" and "Figma stopped answering" were
indistinguishable at the protocol level. For an agent that is the
difference between fixing its arguments and giving up. A mistake in the
call - an unknown scope, a node that is not in the file, an expired
`artifactId` - is now `INVALID_PARAMS`; everything behind the call stays
`INTERNAL_ERROR`. `data` carries the same `code` and `retryable` as before.

Argument validation that had been reported as `DEVUP_THEME_CONFLICT` or
`DEVUP_SNAPSHOT_UNSUPPORTED` is `DEVUP_INVALID_INPUT`, so a real theme
conflict is no longer confused with a typo.

## The response stops restating itself

A tsx-only export goes from 2,411 to 1,405 bytes, and the part carried
regardless from 1,911 to 994.

`fidelity` and `completenessReport` are the drill-down beneath `quality`;
on a clean result they report 100% across six axes and six empty arrays,
which `quality` already said. They are sent when the result is not exact
or complete, and whenever `includeDiagnostics` asks for them. On a
Section export that was per screen.

`imports`, `usedTokens`, `componentImports`, `responsiveImports` and
`responsiveComponents` restated the tsx's own first line and its
`$token`s. `deliverable` existed to stop an agent mistaking a
`needs_figma` handoff step for the answer, and that handoff no longer
exists, so it was restating `status` in prose on every response.

`completeness` and `themeCompleteness` stay: they grade how far token
resolution reached, which `quality` does not say. `cache` stays whole -
it is the artifact-reuse contract that avoids real Figma calls, and its
fields are pinned by tests.

The server instructions were the larger cost and are the larger fix.
They told every agent to take `tsx`, `rawSnapshot` and `sourceMap`
together; on the same measurement `sourceMap` is about 5x the size of
the tsx it annotates and `rawSnapshot` about 2x, so following the
instruction spent roughly eight bytes for every one of code. They now
say to ask for an output only when it will be read, and what each one
is for.

## Releases stop shipping devup-mcp-visual

The render harness builds it from source with cargo and nothing
downloads it, so three of six release assets were binaries no consumer
had a use for, built on every platform of every release. The crate stays.

## Cargo.lock is committed at the released version

It sat at 0.1.0 while the workspace moved to 0.2.1, so every build
regenerated it, dirtied the tree, and made every release binary report
its build id as `1e8b0b6de9a6-dirty` - which meant `--version` and
`--self-check` could not tell a release from a developer's working tree,
the one thing they exist to do.

## Also

Removes `DevupFigmaHostRequired` and `DevupCompatCorpusDrift`, which no
production code ever constructed. Their only reference was a test
pinning their wire strings, which would have passed forever whether or
not they were reachable; that test is replaced by one that pins the
INVALID_PARAMS split on both sides.
…iverable

Two corrections from consumer feedback on the previous commit.

## A lost token is no longer silent

A fill bound to a variable, or a text carrying a style, is the design
saying "this is a token". The generator writes the token when the
resource catalog carried that variable or style, and the resolved value
when it did not. Writing something is right - the module still has to
compile and render. Writing it silently is not.

Measured on a synthetic frame bound to `VariableID:1:1009` and
`S:ddba35e8…` with neither in the catalog: the code came out with
`#7d7f83` and `fontSize="14px" fontWeight="400" lineHeight="1.6"`, and
`diagnostics` was empty. That value renders identically today and stops
following the theme tomorrow, and the response still graded the
conversion `exact`, so a caller reading only the answer had no way to
know a token was in there. The report a consumer had to reach for
instead was `sourceMap`, which is about five times the size of the tsx
it annotates.

Each such binding now raises `DEVUP_CODEGEN_TOKEN_NAME_UNRESOLVED`
carrying the node, the property and the resource id. Being a
`DEVUP_CODEGEN_` warning it counts as an approximation, so
`quality.projection` stops reading `exact` when a token was lost.

The check is a pass over the collected subtree rather than a step inside
rendering, so it can name the node a binding belongs to without
threading an argument through every render function. The generated bytes
do not change, so all 268 plugin-parity goldens are untouched.

The path that already worked is pinned alongside it: with the catalog
present, the same frame emits `$caption` and `typography="captionSm"`,
no resolved value survives beside the token that names it, and nothing
is reported.

## `deliverable` comes back

It was removed on the reasoning that it only restated `status`, since
the `needs_figma` handoff it originally guarded against no longer
exists. A consumer reported relying on it: `status: "complete"` says the
run went well, and `deliverable` says which of the response's values is
the answer to implement from. That is a different sentence, and 109
bytes is cheap for it.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Changepacks

devup-mcp@0.2.1 → 0.3.0 - crates/devup-mcp/Cargo.toml

Minor

  • Narrow what devup-mcp asks for and what it sends back, and make a refusal say which of the two kinds it is. Nine tools become seven: devup_figma_to_ui and devup_figma_to_json were devup_figma_export with a single entry in outputs, so every client carried three schemas in its context and had to decide between them on every call, and the export tool's own description had to spend a sentence saying which to prefer. Use outputs: [tsx] and outputs: [devupJson] instead. sourcePolicy is gone from all of them: auto and direct both meant the direct connection and the parameter never branched, so it only ever offered a caller something to get wrong, and it was also part of the artifact cache key it could not affect. Every remaining closed-set input now publishes its accepted values in the JSON schema - action, scope, rootLayout, delivery, match, project context scope and stack diff layers - from one shared constant the parser reads too, so the schema cannot drift from what is accepted and a caller stops discovering the set one rejection at a time; outputs and the asset format were the only two that already did this. Errors are no longer all INTERNAL_ERROR. A mistake in the call itself - an unknown scope, a node that is not in the file, an expired artifactId - is now JSON-RPC INVALID_PARAMS, and everything behind the call stays INTERNAL_ERROR, so an agent can tell 'fix the arguments and retry' from 'stop and report' without parsing the message; the exact code and retryable are unchanged in data. Pure argument validation that had been reported as DEVUP_THEME_CONFLICT or DEVUP_SNAPSHOT_UNSUPPORTED is DEVUP_INVALID_INPUT, so a real theme conflict is no longer confused with a typo. The response is lighter for the same content. Measured on one export, a tsx-only call went from 2,411 to 1,405 bytes and the part every response carries regardless from 1,911 to 994. fidelity and completenessReport are the drill-down beneath quality and on a clean result restate it - 100% across six axes, six empty arrays - so they are sent when the result is not exact or complete, or when includeDiagnostics asks for them; on a Section export that was per screen. imports, usedTokens, componentImports, responsiveImports and responsiveComponents restated the tsx's own import line and its $tokens and are gone. deliverable stays: it was removed on the reasoning that the needs_figma handoff it guarded against is gone, and a consumer reported relying on it to know which value is the answer, which settles it. The server instructions were the larger cost and are corrected: they told every agent to take tsx, rawSnapshot and sourceMap together, which on the same measurement is about eight times the bytes of the code, and they now say to ask for an output only when it will be read. completeness and themeCompleteness stay - they grade how far token resolution reached, which quality does not say. Releases stop shipping devup-mcp-visual: the render harness builds it from source with cargo and nothing downloads it, so three of six assets were binaries no consumer used, built on every platform of every release. Cargo.lock is committed at the released version, which stops every release binary reporting its build id as -dirty and losing the ability to tell a release from a developer's working tree. Also removes two ErrorCode variants no production code ever constructed, whose only reference was a test pinning their wire strings - a test that would have passed forever whether or not they were reachable. A binding the resource catalog never named is now reported rather than quietly resolved. A fill bound to a variable, or a text carrying a style, is the design saying this is a token; the generator writes the token when the catalog carried that resource and the resolved value when it did not, and it has to write something because the module still has to compile. It said nothing when it did, so a hardcoded #7d7f83 could sit where the design says - identical today, no longer following the theme tomorrow - inside a response graded exact. Each such binding now raises DEVUP_CODEGEN_TOKEN_NAME_UNRESOLVED carrying the node, the property and the resource id, which also stops quality.projection reading exact. The check runs as a pass over the collected subtree rather than inside rendering, so it can name the node without threading an argument through the render functions, and the 268 plugin-parity goldens are unaffected because the generated bytes do not change.

devup-mcp-devup-ui@0.2.1 → 0.3.0 - crates/devup-mcp-devup-ui/Cargo.toml

Minor

  • Narrow what devup-mcp asks for and what it sends back, and make a refusal say which of the two kinds it is. Nine tools become seven: devup_figma_to_ui and devup_figma_to_json were devup_figma_export with a single entry in outputs, so every client carried three schemas in its context and had to decide between them on every call, and the export tool's own description had to spend a sentence saying which to prefer. Use outputs: [tsx] and outputs: [devupJson] instead. sourcePolicy is gone from all of them: auto and direct both meant the direct connection and the parameter never branched, so it only ever offered a caller something to get wrong, and it was also part of the artifact cache key it could not affect. Every remaining closed-set input now publishes its accepted values in the JSON schema - action, scope, rootLayout, delivery, match, project context scope and stack diff layers - from one shared constant the parser reads too, so the schema cannot drift from what is accepted and a caller stops discovering the set one rejection at a time; outputs and the asset format were the only two that already did this. Errors are no longer all INTERNAL_ERROR. A mistake in the call itself - an unknown scope, a node that is not in the file, an expired artifactId - is now JSON-RPC INVALID_PARAMS, and everything behind the call stays INTERNAL_ERROR, so an agent can tell 'fix the arguments and retry' from 'stop and report' without parsing the message; the exact code and retryable are unchanged in data. Pure argument validation that had been reported as DEVUP_THEME_CONFLICT or DEVUP_SNAPSHOT_UNSUPPORTED is DEVUP_INVALID_INPUT, so a real theme conflict is no longer confused with a typo. The response is lighter for the same content. Measured on one export, a tsx-only call went from 2,411 to 1,405 bytes and the part every response carries regardless from 1,911 to 994. fidelity and completenessReport are the drill-down beneath quality and on a clean result restate it - 100% across six axes, six empty arrays - so they are sent when the result is not exact or complete, or when includeDiagnostics asks for them; on a Section export that was per screen. imports, usedTokens, componentImports, responsiveImports and responsiveComponents restated the tsx's own import line and its $tokens and are gone. deliverable stays: it was removed on the reasoning that the needs_figma handoff it guarded against is gone, and a consumer reported relying on it to know which value is the answer, which settles it. The server instructions were the larger cost and are corrected: they told every agent to take tsx, rawSnapshot and sourceMap together, which on the same measurement is about eight times the bytes of the code, and they now say to ask for an output only when it will be read. completeness and themeCompleteness stay - they grade how far token resolution reached, which quality does not say. Releases stop shipping devup-mcp-visual: the render harness builds it from source with cargo and nothing downloads it, so three of six assets were binaries no consumer used, built on every platform of every release. Cargo.lock is committed at the released version, which stops every release binary reporting its build id as -dirty and losing the ability to tell a release from a developer's working tree. Also removes two ErrorCode variants no production code ever constructed, whose only reference was a test pinning their wire strings - a test that would have passed forever whether or not they were reachable. A binding the resource catalog never named is now reported rather than quietly resolved. A fill bound to a variable, or a text carrying a style, is the design saying this is a token; the generator writes the token when the catalog carried that resource and the resolved value when it did not, and it has to write something because the module still has to compile. It said nothing when it did, so a hardcoded #7d7f83 could sit where the design says - identical today, no longer following the theme tomorrow - inside a response graded exact. Each such binding now raises DEVUP_CODEGEN_TOKEN_NAME_UNRESOLVED carrying the node, the property and the resource id, which also stops quality.projection reading exact. The check runs as a pass over the collected subtree rather than inside rendering, so it can name the node without threading an argument through the render functions, and the 268 plugin-parity goldens are unaffected because the generated bytes do not change.

devup-mcp-figma@0.2.1 → 0.3.0 - crates/devup-mcp-figma/Cargo.toml

Minor

  • Narrow what devup-mcp asks for and what it sends back, and make a refusal say which of the two kinds it is. Nine tools become seven: devup_figma_to_ui and devup_figma_to_json were devup_figma_export with a single entry in outputs, so every client carried three schemas in its context and had to decide between them on every call, and the export tool's own description had to spend a sentence saying which to prefer. Use outputs: [tsx] and outputs: [devupJson] instead. sourcePolicy is gone from all of them: auto and direct both meant the direct connection and the parameter never branched, so it only ever offered a caller something to get wrong, and it was also part of the artifact cache key it could not affect. Every remaining closed-set input now publishes its accepted values in the JSON schema - action, scope, rootLayout, delivery, match, project context scope and stack diff layers - from one shared constant the parser reads too, so the schema cannot drift from what is accepted and a caller stops discovering the set one rejection at a time; outputs and the asset format were the only two that already did this. Errors are no longer all INTERNAL_ERROR. A mistake in the call itself - an unknown scope, a node that is not in the file, an expired artifactId - is now JSON-RPC INVALID_PARAMS, and everything behind the call stays INTERNAL_ERROR, so an agent can tell 'fix the arguments and retry' from 'stop and report' without parsing the message; the exact code and retryable are unchanged in data. Pure argument validation that had been reported as DEVUP_THEME_CONFLICT or DEVUP_SNAPSHOT_UNSUPPORTED is DEVUP_INVALID_INPUT, so a real theme conflict is no longer confused with a typo. The response is lighter for the same content. Measured on one export, a tsx-only call went from 2,411 to 1,405 bytes and the part every response carries regardless from 1,911 to 994. fidelity and completenessReport are the drill-down beneath quality and on a clean result restate it - 100% across six axes, six empty arrays - so they are sent when the result is not exact or complete, or when includeDiagnostics asks for them; on a Section export that was per screen. imports, usedTokens, componentImports, responsiveImports and responsiveComponents restated the tsx's own import line and its $tokens and are gone. deliverable stays: it was removed on the reasoning that the needs_figma handoff it guarded against is gone, and a consumer reported relying on it to know which value is the answer, which settles it. The server instructions were the larger cost and are corrected: they told every agent to take tsx, rawSnapshot and sourceMap together, which on the same measurement is about eight times the bytes of the code, and they now say to ask for an output only when it will be read. completeness and themeCompleteness stay - they grade how far token resolution reached, which quality does not say. Releases stop shipping devup-mcp-visual: the render harness builds it from source with cargo and nothing downloads it, so three of six assets were binaries no consumer used, built on every platform of every release. Cargo.lock is committed at the released version, which stops every release binary reporting its build id as -dirty and losing the ability to tell a release from a developer's working tree. Also removes two ErrorCode variants no production code ever constructed, whose only reference was a test pinning their wire strings - a test that would have passed forever whether or not they were reachable. A binding the resource catalog never named is now reported rather than quietly resolved. A fill bound to a variable, or a text carrying a style, is the design saying this is a token; the generator writes the token when the catalog carried that resource and the resolved value when it did not, and it has to write something because the module still has to compile. It said nothing when it did, so a hardcoded #7d7f83 could sit where the design says - identical today, no longer following the theme tomorrow - inside a response graded exact. Each such binding now raises DEVUP_CODEGEN_TOKEN_NAME_UNRESOLVED carrying the node, the property and the resource id, which also stops quality.projection reading exact. The check runs as a pass over the collected subtree rather than inside rendering, so it can name the node without threading an argument through the render functions, and the 268 plugin-parity goldens are unaffected because the generated bytes do not change.

devup-mcp-visual@0.2.1 → 0.2.2 - crates/devup-mcp-visual/Cargo.toml

Patch

  • Narrow what devup-mcp asks for and what it sends back, and make a refusal say which of the two kinds it is. Nine tools become seven: devup_figma_to_ui and devup_figma_to_json were devup_figma_export with a single entry in outputs, so every client carried three schemas in its context and had to decide between them on every call, and the export tool's own description had to spend a sentence saying which to prefer. Use outputs: [tsx] and outputs: [devupJson] instead. sourcePolicy is gone from all of them: auto and direct both meant the direct connection and the parameter never branched, so it only ever offered a caller something to get wrong, and it was also part of the artifact cache key it could not affect. Every remaining closed-set input now publishes its accepted values in the JSON schema - action, scope, rootLayout, delivery, match, project context scope and stack diff layers - from one shared constant the parser reads too, so the schema cannot drift from what is accepted and a caller stops discovering the set one rejection at a time; outputs and the asset format were the only two that already did this. Errors are no longer all INTERNAL_ERROR. A mistake in the call itself - an unknown scope, a node that is not in the file, an expired artifactId - is now JSON-RPC INVALID_PARAMS, and everything behind the call stays INTERNAL_ERROR, so an agent can tell 'fix the arguments and retry' from 'stop and report' without parsing the message; the exact code and retryable are unchanged in data. Pure argument validation that had been reported as DEVUP_THEME_CONFLICT or DEVUP_SNAPSHOT_UNSUPPORTED is DEVUP_INVALID_INPUT, so a real theme conflict is no longer confused with a typo. The response is lighter for the same content. Measured on one export, a tsx-only call went from 2,411 to 1,405 bytes and the part every response carries regardless from 1,911 to 994. fidelity and completenessReport are the drill-down beneath quality and on a clean result restate it - 100% across six axes, six empty arrays - so they are sent when the result is not exact or complete, or when includeDiagnostics asks for them; on a Section export that was per screen. imports, usedTokens, componentImports, responsiveImports and responsiveComponents restated the tsx's own import line and its $tokens and are gone. deliverable stays: it was removed on the reasoning that the needs_figma handoff it guarded against is gone, and a consumer reported relying on it to know which value is the answer, which settles it. The server instructions were the larger cost and are corrected: they told every agent to take tsx, rawSnapshot and sourceMap together, which on the same measurement is about eight times the bytes of the code, and they now say to ask for an output only when it will be read. completeness and themeCompleteness stay - they grade how far token resolution reached, which quality does not say. Releases stop shipping devup-mcp-visual: the render harness builds it from source with cargo and nothing downloads it, so three of six assets were binaries no consumer used, built on every platform of every release. Cargo.lock is committed at the released version, which stops every release binary reporting its build id as -dirty and losing the ability to tell a release from a developer's working tree. Also removes two ErrorCode variants no production code ever constructed, whose only reference was a test pinning their wire strings - a test that would have passed forever whether or not they were reachable. A binding the resource catalog never named is now reported rather than quietly resolved. A fill bound to a variable, or a text carrying a style, is the design saying this is a token; the generator writes the token when the catalog carried that resource and the resolved value when it did not, and it has to write something because the module still has to compile. It said nothing when it did, so a hardcoded #7d7f83 could sit where the design says - identical today, no longer following the theme tomorrow - inside a response graded exact. Each such binding now raises DEVUP_CODEGEN_TOKEN_NAME_UNRESOLVED carrying the node, the property and the resource id, which also stops quality.projection reading exact. The check runs as a pass over the collected subtree rather than inside rendering, so it can name the node without threading an argument through the render functions, and the 268 plugin-parity goldens are unaffected because the generated bytes do not change.

@owjs3901
owjs3901 merged commit fd10f17 into main Sep 8, 2026
8 checks passed
@owjs3901
owjs3901 deleted the refactor/narrow-the-surface branch September 8, 2026 07:29
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