Specgate
БесплатноНе проверенspecgate is an MCP server that verifies UI design fidelity by comparing a live DOM against Figma specs, reporting specific CSS property deltas so agents know ex
Описание
specgate is an MCP server that verifies UI design fidelity by comparing a live DOM against Figma specs, reporting specific CSS property deltas so agents know exactly what to fix. It also freezes measurement results into lock files for fast offline regression detection.
README
specgate
Design-fidelity verification that tells an agent what to fix — not that something looks off.
Measure the DOM against the Figma spec property by property, report the delta in CSS terms, then freeze the result so regressions are caught offline in ~2 seconds.
Contents
- The problem
- The approach
- Results
- Quick start
- How it works
- Commands
- Using with Claude Code
- Configuration
- Project layout
- Testing and gates
- Design decisions
- Status and roadmap
- Limitations
- Credits
The problem
The intuitive design-fidelity loop — agent writes code → screenshot → compare images → agent fixes — asks a language model to do the thing it is worst at: spatial pixel comparison. A score like 97.3% match does not say which element is wrong, which property is wrong, or by how much. The agent cannot act on it, so it guesses, patches blindly, and breaks something else.
The failure is measurable, not theoretical. We built a testbed screen that matches its Figma frame exactly, then injected 17 known defects and ran the most mature tool in this space as a baseline. Here is one of them:
Defect
d01— a 12×12 checkbox changes from#1660CFto#2F80ED. Fully visible to a human. Baseline verdict:DFS 100 · pixelDiffRatio 0.53% · 0 style diffs · PASS
The defect is real but invisible to the tool: 144 px² of change inside a 378,000 px² frame lands below the 1% pixel threshold, and the style comparison never reaches an element that deep. Five of ten injected defects went undetected the same way, and all three mapping defects passed silently.
Global pixel scores lie about small elements. That is the gap specgate is built to close.
The approach
Measurement is the source of truth; pixels are a supplement.
| Layer | Role | Output |
|---|---|---|
| Measurement gate | The only source of truth | Figma node tree ↔ getComputedStyle() + getBoundingClientRect() → numeric delta + CSS property name |
| Pixel pass | Catches only what measurement cannot see | Per-node crop diff → "the numbers match but it still looks wrong" (wrong icon, wrong asset, missing gradient) |
Two rules follow from this, and they shape everything else:
- No global pixel score is ever produced. If you emit one, the agent optimises for it, and it will lead the agent astray.
- Every finding names a CSS property, a want, a got, and a signed delta with units — because that is the minimum an agent needs to write a one-line fix.
flowchart LR
subgraph inputs[" "]
FIG["Figma REST API<br/><small>files · nodes · images</small>"]
APP["Running app<br/><small>Playwright + Chromium</small>"]
end
FIG --> FN["@ui-perfect/figma<br/><small>version cache · 429 backoff<br/>normalize</small>"]
APP --> PW["@ui-perfect/probe-web<br/><small>freeze kit · WebProbe<br/>per-node crop</small>"]
FN -->|"NormalizedNode<br/>(spec)"| CORE
PW -->|"NormalizedNode<br/>(measured)"| CORE
CORE["<b>@ui-perfect/core</b><br/><small>compare engine · tolerance<br/>offset consensus · ΔE2000<br/>lock schema · report</small>"]
CORE --> REPORT["Report <600 tokens<br/><small>for the agent</small>"]
CORE --> LOCK["Lock file<br/><small>frozen measurements</small>"]
CLI["@ui-perfect/cli<br/><small>verify · accept · guard<br/>audit · impact · mcp</small>"] -.orchestrates.-> FN
CLI -.-> PW
CLI -.-> CORE
style CORE stroke-width:3px
@ui-perfect/core imports neither Figma nor Playwright. It receives two NormalizedNode trees and returns findings — which is why the entire comparison engine is unit-testable with no network and no browser.
Results
Same testbed, same 17 injected defects, same viewport. The baseline column is a mature existing tool measured during Phase 0 to decide whether building this was justified.
| Test set | What it measures | Baseline | specgate |
|---|---|---|---|
| detect (10 real defects) | Recall — does it catch the bug? | 5 / 10 | 9 / 10 ¹ |
| benign (4 no-op refactors) | Noise — false positives on visually identical changes | 4 / 4 clean | 4 / 4 clean |
mapping (3 broken data-fig) |
Does it notice its own mapping is broken? | 0 / 3 (silent) | 3 / 3 reported |
¹ 9 of the 10 d* defects are caught by measurement. d09 (gradient → flat colour) is deliberately not caught here — measurement does not compare gradients by design — and is verified separately by the pixel gate at 19.8% diff. The Phase 2 gate reports these buckets independently rather than merging them into one score.
Every detection in the baseline column came from the global pixel ratio; none came from measurement. specgate's detections all name the node and the property.
The same defect, both tools:
- baseline: DFS 100 · pixelDiffRatio 0.53% · 0 style diffs · PASS
+ specgate: [checkout.summary.checkbox] · background-color: want #1660CF, got #2F80ED (ΔE 12.3)
Gates
Three hard gates, each run against the live system rather than a mock:
| Gate | Criterion | Result |
|---|---|---|
| 1 — Build justified? | Does an existing tool already close the gap? | Recall 5/10, mapping silent → build |
| 2 — Does the loop converge? | Agent fixes from the report alone, cap 5 rounds | 2 rounds on both d04 and d05 |
3 — Is guard trustworthy? |
20 consecutive runs, unchanged code | 20 / 20, zero false positives |
Gate 3 is the important one. A flaky guard is worse than no guard, because it teaches the team to ignore warnings. Its precondition — 20 measurement runs that are byte-identical after quantisation — is verified separately by the jitter test.
Quick start
Prerequisites: Node ≥ 20.19 · pnpm ≥ 9.15 · Docker · a Figma personal access token (create one).
# 1. Install
pnpm install
# 2. Configure — token stays in .env, which is gitignored
cp .env.example .env
$EDITOR .env # FIGMA_TOKEN=figd_...
# PLAYWRIGHT_SERVER_URL=ws://localhost:6003/specgate
# 3. Start the pinned browser (determinism — see Design decisions)
cd docker && docker compose up -d && cd ..
# 4. Start the testbed app
pnpm dev # http://localhost:5173/checkout
# 5. Verify it against the Figma frame
npx tsx packages/cli/src/cli.ts verify checkout mobile
Expected output on a clean testbed:
# specgate: checkout/mobile vs figma teZCNcleh8Oa16hWuqwA5M:1:4960
PASS · 0 high, 0 med, 1 low · coverage 38%
(1 LOW — within rendering tolerance or dynamic data, ignore)
Now break something on purpose and watch the report change:
node apps/testbed/mutations/run.mjs apply d04-fontweight
npx tsx packages/cli/src/cli.ts verify checkout mobile --skip-pixel
node apps/testbed/mutations/run.mjs revert
How it works
The verify loop
The agent never sees an image. It sees a token-budgeted report where the order of lines is itself information — an agent fixes roughly what it reads first, so root causes are printed above symptoms.
sequenceDiagram
participant A as Claude Code
participant M as verify_ui (MCP)
participant F as Figma (cached)
participant B as Chromium (Docker)
A->>A: write / edit UI code
A->>M: verify_ui(screen, viewport)
M->>F: nodes (1 request, pinned per session)
M->>B: measure DOM + screenshot
M->>M: compare · offset consensus · ΔE2000
M-->>A: report < 600 tokens<br/>HIGH → MEDIUM, CSS properties
A->>A: fix the HIGH items
A->>M: verify_ui(...)
M-->>A: PASS (or next round)
Note over A,M: cap 5 rounds — then<br/>"N issues left, needs a human"
A real report, produced by injecting d04 (Grand Total loses its bold) — 136 tokens:
# specgate: checkout/mobile vs figma teZCNcleh8Oa16hWuqwA5M:1:4960
FAIL · 2 high, 1 med, 1 low · coverage 38%
## HIGH (2)
- [checkout.summary.grandTotalLabel] Grand Total · font-weight: want 700, got 400 (-300)
- [checkout.summary.grandTotalValue] $3,439.00 · font-weight: want 700, got 400 (-300)
## MEDIUM (1)
- [checkout.summary.grandTotalValue] $3,439.00 · x (left edge): want 284px, got 286px (+2px)
(1 LOW — within rendering tolerance or dynamic data, ignore)
Fix HIGH first, then run verify_ui again. Don't chase LOW.
Round 1/5.
Root cause before symptoms
A single wrong margin on a container shifts its whole subtree. Naively that is a dozen position findings, none of which is the bug. specgate runs an offset consensus per subtree, subtracts the shift from every child, and charges it to the container instead — one finding at the cause:
FAIL · 1 high, 0 med, 0 low · coverage 83%
⚠ offset dx=0 dy=40 at k.summary — subtracted before position checks
(4 child nodes affected). Fix this container first.
## HIGH (1)
- [k.summary] k.summary · subtree offset: want (0, 0), got (0, 40) (+40px)
← all 4 children shifted by dy=40px — fix this container, not each child
Four position findings collapse into one that names the container. The shift is still a defect, so it fails the gate — suppressing the children is about locating the bug, not forgiving it. A global offset (the whole frame shifted) is treated differently: it is reported as the ⚠ line only, because a uniform frame-wide shift is usually a measurement-origin artefact rather than a UI bug.
Consensus requires agreement, deliberately: at least 4 samples, a median of at least 1px, and ≥60% of nodes within ±1px of it. Without that guard, a median of [0, 0, −8, −8] would "detect" a −4px offset and turn one bug into two wrong ones. The x and y axes are resolved independently, so a subtree that is correct horizontally but shifted vertically is still caught.
The finding pipeline
flowchart TD
START["spec node ↔ measured node<br/><small>paired by logical key</small>"]
START --> ROT{"rotated<br/>in Figma?"}
ROT -->|yes| SKIP["skip geometry<br/><small>bbox of a rotated node lies</small>"]
ROT -->|no| OFF["subtract subtree offset"]
OFF --> CLS["classify against tolerance profile"]
CLS --> T{"|Δ| vs rule"}
T -->|"≤ tol"| DROP["dropped — not reported"]
T -->|"tol .. high"| MED["MEDIUM"]
T -->|"≥ high"| HIGH["HIGH"]
START --> FILL{"fill type"}
FILL -->|solid| DE["ΔE2000 after alpha compositing"]
FILL -->|"gradient / image"| PX["pixel pass<br/><small>per-node crop, text masked</small>"]
FILL -->|"design has fill,<br/>impl transparent"| MISSING["HIGH — missing style,<br/>not a colour delta"]
DE --> CLS
HIGH --> GATE
MED --> GATE
PX --> GATE
GATE["gate: HIGH + MEDIUM only<br/><small>LOW is informational</small>"] --> REP["report<br/><small>severity → key → property</small>"]
style DROP stroke-dasharray: 5 5
style GATE stroke-width:3px
Deliberately not compared: shadow geometry (Figma blur radius and CSS box-shadow blur are not the same quantity — presence only), gradients and image fills (pushed to the pixel pass), text content differences (usually dynamic data → LOW), and line-height: normal against a pinned design value (font-dependent → MEDIUM with a note instead of a fabricated number). Hidden nodes are dropped from the tree entirely — comparing against visible: false layers is the number one source of false "missing element" reports.
Four more exclusions were added after measuring them against the real testbed, each because it produced noise rather than signal:
text-align/text-transform. Categorical, and redundant. A browser reportsstartwhere Figma reportsleft; a<button>reportscenterfrom the UA stylesheet. When alignment genuinely differs the element's box moves, and the geometry check already catches that.- Node type (TEXT vs FRAME). The DOM-side heuristic (
no element children and non-empty text) disagrees with Figma often enough — a label wrapping a checkbox, for instance — that the mismatch says more about the heuristic than about the UI. 1px solid transparent. A standard trick for reserving layout space, and the default in many button resets. Invisible, therefore not a border.- Auto and percentage line-height. Figma still returns a computed
lineHeightPxwhen the designer chose "Auto", so pinning that number would invent a requirement nobody specified. OnlylineHeightUnit: "PIXELS"is treated as a pinned value.
Percentage border-radius is resolved against the element's own box (50% on a 48×48 button is 24px, not 50px), and Figma group opacity is composited down the tree so a child inside a 50%-opacity group is compared as 0.5, not 1.
Mapping: logical keys, not node ids
Markup references a logical key; the config maps that key to a Figma node id per viewport. Figma node ids change when a designer recreates a frame, and the same element has a different id in every breakpoint — markup should not die for either reason.
flowchart LR
subgraph code["Implementation"]
H["<h2 data-fig=<br/>"checkout.title">"]
B["<button data-fig=<br/>"checkout.cta">"]
end
subgraph cfg["screens.config.json"]
K1["checkout.title"]
K2["checkout.cta"]
K3["checkout.sidebar"]
end
subgraph fig["Figma frames"]
M["mobile 1:4960"]
D["desktop 1:2082"]
end
H --- K1
B --- K2
K1 -->|"2:15"| M
K1 -->|"1:2"| D
K2 -->|"2:20"| M
K2 -->|"1:3"| D
K3 -.->|"1:9"| D
K3 -.-x M
style K3 stroke-dasharray: 5 5
checkout.sidebar exists on desktop and is intentionally absent on mobile — expressible with logical keys, impossible with raw node ids. Two metrics keep the mapping honest: coverage (share of significant Figma nodes that are mapped — where "significant" deliberately includes long-thin shapes like a 339×1 divider, which code generators forget and users notice) and staleMappings (keys that fail to resolve on either side, in both directions).
Lock file lifecycle
Once a screen is correct, freezing its measurements turns an expensive question ("does this match the design?") into a cheap one ("did anything move?").
stateDiagram-v2
[*] --> Verifying
Verifying: <b>verify</b><br/>Figma + browser + agent loop
Accepted: <b>accept</b><br/>measurements frozen to lock
Guarding: <b>guard</b><br/>no Figma · no AI · ~2s
Rotted: baseline is stale
Verifying --> Accepted: 0 HIGH + --reason
Accepted --> Guarding: every code change
Guarding --> Guarding: exit 0 — no drift
Guarding --> Verifying: exit 1 — drift found
Guarding --> Accepted: exit 3 — env changed,<br/>re-accept in the new env
Accepted --> Rotted: designer edits Figma
Rotted --> Verifying: <b>audit</b> exit 1<br/>reports what changed
Accepted --> Accepted: <b>audit</b> exit 0 — fresh
Two decisions make this work:
The lock stores measured values, not the spec. If it stored the spec, every guard run would re-report deviations you already consciously accepted — permanent noise. Storing what was measured at acceptance time bakes in tolerated deviations, so guard stays silent until something actually changes.
audit exists because the lock stores measured values. That is the failure mode of the previous decision: if Figma moves ahead and nobody re-verifies, guard passes forever while the design has changed. audit closes it cheaply — it compares the file version first (one small request, ~zero cost when nothing changed), and only when that differs does it fetch nodes and compare a specHash of the mapped subtree. A designer editing an unrelated page changes lastModified but not the hash.
Drift tolerance is roughly 20× tighter than design tolerance (0.05px vs 1px), and that asymmetry is intentional. Comparing Figma to a browser means comparing two different rendering engines; comparing a browser to the same browser on the same frozen page has no such noise. Jitter at 0.05px is a determinism bug to fix, never a tolerance to loosen.
Commands
specgate verify <screen> [viewport] # compare against Figma → report for human or agent
specgate accept <screen> [viewport] # freeze measurements into .specgate/lock/ (--reason required)
specgate guard <screen> [viewport] # compare against lock — no Figma, no AI
specgate audit [screen] # baseline rot check via specHash
specgate impact [--base <rev>] # which screens need guarding, from git diff
specgate mcp # MCP server exposing one tool: verify_ui
| Command | Exit codes | Notes |
|---|---|---|
verify |
0 pass · 1 findings · 2 config/env |
--skip-pixel for a faster measurement-only run |
accept |
0 written · 2 refused |
Refuses while any HIGH finding is unreviewed; checks the Figma version first unless --offline |
guard |
0 clean · 1 drift · 2 no lock/config · 3 env mismatch |
Exit 3 means "not compared" — a changed browser or tolerance profile is not drift |
audit |
0 fresh · 1 rot · 2 no lock |
Lists what the designer changed, property by property |
impact |
0 always when it runs |
Fails open: if the dependency graph errors, guard everything |
mcp |
— | Runs until the client disconnects |
During development, run through tsx: npx tsx packages/cli/src/cli.ts verify checkout mobile. Full reference with real output for every command: docs/CLI.md.
Using with Claude Code
.mcp.json is committed at the project root, so opening this repo in Claude Code exposes the verify_ui tool automatically — no claude mcp add needed:
{
"mcpServers": {
"specgate": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "packages/cli/src/cli.ts", "mcp"]
}
}
}
The token is not in that file. The CLI loads .env from the working directory at startup, and any variable already present in the environment wins.
The tool description carries its own stopping condition, because a loop without one burns tokens chasing sub-pixel antialiasing:
Call after implementing or editing UI, fix the HIGH items, then call again. Stop after 5 calls or when only LOW items remain — LOW items are rendering tolerance, not bugs.
Infrastructure failures are reported as infrastructure failures. A Figma rate limit comes back flagged ⚠ specgate infrastructure error (NOT a UI bug — do not change code because of this), so the agent does not "fix" a network error by editing CSS.
Configuration
.specgate/screens.config.json — one entry per screen, one map per viewport:
{
"$schema": 1,
"figma": { "fileKey": "teZCNcleh8Oa16hWuqwA5M" },
"screens": {
"checkout": {
"sources": ["apps/testbed/src/**"], // for impact analysis
"app": {
"url": "http://localhost:5173/checkout",
"command": "pnpm --filter testbed dev", // how guard boots the app
"fixtureMode": "static"
},
"viewports": {
"mobile": {
"width": 375, "height": 1008, "dpr": 2, "isMobile": true,
"figmaNode": "1:4960", // API form "1:4960", not URL form "1-4960"
"map": {
"checkout.summary.payButton": "I70:7095;70:6703;41:2400",
"checkout.summary.divider": "I70:7095;1:5105"
// … 29 keys total
}
}
}
}
},
"impact": {
// Changes to these bypass dependency analysis entirely and guard every screen —
// a dependency graph cannot see global CSS, tokens, or config
"always": ["**/tokens/**", "apps/testbed/src/styles/**",
"pnpm-lock.yaml", ".specgate/screens.config.json"],
// Where to build the dependency graph. Must be WIDER than any screen's `sources`:
// a screen imports shared code living outside its own glob, and scanning narrowly
// means those imports are never seen. Omit to auto-detect packages/ apps/ src/ lib/
"scan": []
}
}
Tolerance profiles
tol — below this, the difference is dropped entirely, never reported. Between tol and high → MEDIUM. At or above high → HIGH. The gate counts HIGH and MEDIUM only.
| Property | Design tol → high |
Drift tol → high |
|---|---|---|
x y w h |
1 → 8 px | 0.05 → 0.5 px |
fontSize |
0.5 → 2 px | 0.05 → 0.5 px |
lineHeight |
1 → 4 px | 0.05 → 0.5 px |
letterSpacing |
0.15 → 0.8 px | 0.02 → 0.2 px |
fontWeight |
0 → 0.5 — any difference is HIGH | same |
borderWidth |
0.5 → 1.5 px | 0.05 → 0.5 px |
radius |
0.5 → 4 px | 0.05 → 0.5 px |
gap padding |
1 → 8 px | 0.05 → 0.5 px |
opacity |
0.02 → 0.1 | 0.01 → 0.05 |
color |
ΔE 1.2 → 4 | ΔE 0.5 → 2 |
Colour distance is CIEDE2000, never RGB. UI palettes are full of near-neutral darks — #0F172A against #111827 is a large RGB distance and a barely perceptible one to the eye. RGB or CIE76 would send an agent chasing colour bugs that do not exist.
A delta that is not a finite number — NaN from a node that failed to measure, or an infinite difference — is classified HIGH, never dropped. "Within tolerance" has to mean measured and fine; a node nobody could measure must not pass the gate by omission.
The profile version is written into the lock file. When the table changes meaning, guard refuses to compare (exit 3) rather than silently reinterpreting an old baseline.
Project layout
packages/
core/ # ~1,170 lines · no Figma, no Playwright imports
types.ts NormalizedNode — the shape both sides normalise to
tolerance.ts design + drift profiles, classify()
offset.ts per-subtree consensus
color.ts ΔE2000 via culori, alpha compositing
mapping.ts significance rule, coverage, stale mappings
compare.ts the comparison engine
report.ts token-budgeted formatter
lock.ts schema + deterministic serialiser
figma/ # REST client: version cache, session pinning, 429 backoff,
# stale-if-error · node → NormalizedNode
probe-web/ # WebProbe over Playwright · freeze kit · per-node pixel pass
cli/ # specgate binary + MCP server, one shared code path
apps/testbed/ # checkout screen matching a real Figma frame
mutations/ 17 injected defects + 4 gate scripts — the permanent regression suite
.specgate/
screens.config.json
lock/checkout.mobile.json # committed — a baseline you can review in a PR
docker/ # pinned Playwright browser server
docs/ # ARCHITECTURE · CLI · GOTCHAS · phase0-report
PLAN.md, PLAN-02-lockfile-mobile.md # the original design specification
Testing and gates
125 unit tests across 15 files — core 64, probe-web 22, cli 21, figma 18 — plus 8 integration tests in a 16th file that need Docker and Vite running.
pnpm test # unit tests
pnpm --filter @ui-perfect/probe-web test # includes integration (needs Docker + Vite)
Four gate scripts run the whole system against the testbed. They need the Docker browser and the Vite server running, and must be run from the repository root:
npx tsx apps/testbed/mutations/mapping-gate.mjs # Phase 1 — mapping survives refactors, reports breakage
npx tsx apps/testbed/mutations/specgate-gate.mjs # Phase 2 — detect 9/9, d09 defer→pixel, mapping 1/1, benign 4/4
npx tsx apps/testbed/mutations/pixel-gate.mjs # Phase 3 — gradient defect caught by pixels
npx tsx apps/testbed/mutations/jitter-test.mjs 20 # Phase 5 — 20/20 byte-identical
The mutation harness itself is the interesting part. Three sets, three different questions:
- detect (10) — real defects. Measures recall.
- benign (4) — refactors with no visual change: wrapping a
div, swappingaforbutton,14px→0.875rem. Must produce zero findings. This is the set that measures noise, and noise is what kills these tools: past ~40% false positives an agent stops trusting the report. - mapping (3) — deliberately broken
data-figattributes. Must be reported, never silently passed.
Freezing the tolerance table before writing the mutations was deliberate — otherwise the person tuning thresholds is the person choosing the tests, and the numbers mean nothing.
Design decisions
Six decisions carry most of the weight. docs/ARCHITECTURE.md covers the reasoning in full.
| Decision | Why |
|---|---|
| Measurement first, no global score | A percentage is not actionable. A CSS property with a signed delta is. |
| Logical keys instead of Figma node ids | Node ids change when frames are recreated and differ per breakpoint; markup should not. Also makes "intentionally absent on mobile" expressible. |
| Lock stores measured, not spec | Storing the spec re-reports every consciously accepted deviation, forever. |
| Drift tolerance ≈ 20× tighter than design tolerance | Browser-vs-browser has none of the two-renderer noise that browser-vs-Figma has. |
| Text excluded from pixel diff, permanently | Figma and browsers never rasterise glyphs identically. This is not a tolerance problem, so it is not solved with a tolerance. Text is masked out of every crop, including its parents'. |
| Determinism before the first lock file | If the baseline is machine-specific, guard fails for everyone but its author. The browser is pinned to an exact version and runs in Docker; the client version must match exactly. |
Status and roadmap
| Phase | Scope | Outcome |
|---|---|---|
| −1 Bootstrap | Monorepo, pinned Docker browser, Figma sample, testbed | ✅ |
| 0 Validate | 17-mutation harness against a baseline tool | ✅ Gate 1 — build justified |
| 1 Mapping | Logical keys, coverage, stale detection | ✅ 6/6 |
| 2 Engine | Per-node compare, tolerance, offset consensus, ΔE2000 | ✅ detect 9/9 (d09 defer→pixel) + mapping 1/1, benign 4/4 |
| 3 Pixel | Per-node crops, text masking | ✅ gradient defect at 19.8% |
| 4 Report + MCP | 136-token report, verify_ui, 5-round cap |
✅ Gate 2 — converged in 2 rounds |
| 5 Determinism | Jitter test | ✅ 20/20 byte-identical |
| 6 Lock + guard | accept, guard, drift profile, env fingerprint |
✅ Gate 3 — 20 runs, 0 false positives |
| 7 Audit + impact | specHash rot check, dependency analysis with escape hatch |
✅ |
| 8 Mobile | Mobile freeze kit, safe-area injection | ✅ |
Next: desktop viewport mapping · design-token checking via Figma boundVariables (catching "right number, hardcoded instead of using the token") · interaction states (hover, focus, dark mode) · an occasional real-device pass.
Limitations
Stated plainly, because a verification tool that oversells itself is worse than none:
- Gradients and image fills are not measured. By design — they cannot be reduced to one colour. The pixel pass catches them, which means they need the screenshot path, not just measurement.
- Emulation is not a real device. Playwright's mobile emulation answers "is my responsive layout right?", not "is it right on an actual iPhone?". Safe-area insets read as
0and are injected artificially; mobile Safari handles some flex cases differently. Ship-critical mobile work still needs a real-device pass. - Figma Starter plans are rate-limited to roughly 10 requests per minute on the endpoints this tool needs. The client caches per file version, pins that version for a whole verify session, and backs off on 429 — but a cold cache on a busy token will wait.
- Code Connect is not usable here. It requires an Organization or Enterprise plan, which is why mapping is attribute-based.
- The
data-figattributes reach production markup unless stripped at build time. A babel/swc plugin handles this; it is documented, not yet wired up.
Credits
The architecture — measurement-first comparison, logical-key mapping, per-subtree offset consensus, measured-value lock files, the split between verify, guard, and audit — is specified in PLAN.md and PLAN-02-lockfile-mobile.md, written before any code existed. Everything in packages/ is implemented from that specification.
Built on: pixelmatch · culori · pngjs · Playwright · @modelcontextprotocol/sdk · commander · zod · @figma/rest-api-spec · dependency-cruiser.
uimatch is the most mature tool in this space and served as the measured baseline in Phase 0: running it over the same 17 mutations is what quantified the remaining gap and justified building this.
License
MIT
Установка Specgate
У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.
▸ github.com/nv-minh/UI-perfectFAQ
Specgate MCP бесплатный?
Да, Specgate MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Specgate?
Нет, Specgate работает без API-ключей и переменных окружения.
Specgate — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Specgate в Claude Desktop, Claude Code или Cursor?
Открой Specgate на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
LibreOffice Tools
Enables AI agents to read, write, and edit Office documents via LibreOffice with token-efficient design. Supports multiple formats including DOCX, XLSX, PPTX, a
автор: passerbyflutterdannote/figma-use
Full Figma control: create shapes, text, components, set styles, auto-layout, variables, export. 80+ tools.
автор: dannoteLogo.dev
Search and retrieve company logos by brand or domain. Customize size, format, and theme to match your design needs. Accelerate design, prototyping, and content
автор: NOVA-3951Design Inspiration Server
Searches top design platforms like Dribbble and Behance to provide UI inspiration, color palettes, and layout patterns via the Serper API. It allows users to re
автор: YonasValentinCompare Specgate with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории design
