A number that only goes up as you get closer.
Deterministic and monotonic — so an agent can hill-climb it: render, compare, fix, repeat.
MCP server · computer vision
A reference goes in, your build goes in. Out comes a score that rises as you get closer — and a punch-list of what to fix.
The idea
Every run returns an overall, five sub-scores, a cv_findings punch-list,
and diagnostic images. The score is built to be actionable and monotonic — not a
single "true" number.
Built for the loop
Point a /loop — or any goal-driven agent run — at
overall. It's the target: the agent renders, compares, fixes the top finding, and
repeats until it clears your threshold. Because the score is monotonic and
deterministic, a rising number is real, repeatable progress — safe to set as the goal and
to gate on.
target = 95
while overall < target: # the goal
render(candidate) # your build step
r = compare_designs(ref, candidate, mode="screen")
overall = r["overall"] # ← the number to climb
fix(r["cv_findings"][0]) # highest-severity miss first
overall ≥ 95, or gate a PR / CI check
on it.cv_findings is already ordered by severity — fix the
top item each pass.Deterministic and monotonic — so an agent can hill-climb it: render, compare, fix, repeat.
Layout, color, content, typography, spacing — scored separately, so you see which axis is off.
Findings name the exact problem — a hex that drifted, an element that's missing, with its box.
compare_motion reads two frame sequences — do they animate with the same energy
and rhythm?
Seeded k-means, fixed pipeline, nothing random. Safe to optimize against, safe to trust when it moves.
Calibrated against human labels — with the correlation numbers and the failure modes published.
What it measures
SSIM on the aligned grayscale. Measures shape — the global shift is registered away first, so a 1-px offset doesn't tank it.
weakness grayscale; plateaus under heavy blur.
A seeded k-means palette in CIELAB, matched one-to-one by ΔE2000. Score =
100·exp(−ΔE/19), each shift named as hex.
weakness flat in a single-theme UI; ignores position.
A contrast-adaptive detector matches the reference's blocks to yours by IoU. Missing or extra blocks become findings.
weakness coarse; region boxes wobble.
Text lines by morphology, compared on amount and scale. Not OCR, not font identity.
weakness misses a wrong font; null when no text.
Outer margins and vertical rhythm from major blocks, each within a tolerance.
weakness coarsest; null on sparse views.
The five combine as a weighted geometric mean — one weak axis drags the whole score down.
overall = exp( Σ wᵢ·ln(scoreᵢ) )
Try it
These run the same formulas the worker does.
One low dimension pulls the overall down far harder than an average would.
Each palette pair scores 100·exp(−ΔE/19), ΔE by CIEDE2000. Pick a
candidate color.
Install & register
Needs Node ≥ 20, Python ≥ 3.10, and —
for capture_frames — a local Chrome.
git clone https://github.com/codenameakshay/design-compare-mcp
cd design-compare-mcp
npm install
npm run build
python3 -m venv worker/.venv
worker/.venv/bin/pip install numpy pillow scikit-image \
opencv-python-headless scikit-learn scipy
npm run smoke # ends with: ALL CHECKS PASSED ✅
claude mcp add design-compare --scope user -- \
node /abs/path/to/design-compare-mcp/dist/index.js
Registers four tools: ping, compare_designs,
compare_motion, capture_frames.
Is it any good?
Scored against 26 components of a dark component library vs. its Flutter port, each rated 0–100 by eye. Correlation is Spearman.
dark-ui preset — the honest working numberA decent layout + typography checker on this set — not a
full oracle. Drop your own labeled pairs into calibration/ and re-run the tuner to
calibrate for your UI.
compare_motion.FAQ
Cloning a UI is a loop: render, compare to the reference, fix the biggest gap, repeat. This replaces the eyeballing with a reproducible number and a specific punch-list.
Two local images — a reference and a candidate. widget mode
for a tight crop, screen mode for a full view. For motion, two frame sequences.
Five dimensions measured independently, combined with a weighted
geometric mean. Anything that doesn't apply returns null and is excluded.
Useful, not authoritative — about +0.44 Spearman with human judgment on the calibration set. A first-pass gate and punch-list, not a replacement for a human's final call.
Yes. Drop (reference, candidate, your 0–100 label) pairs
into calibration/ and run scripts/calibrate.py.
TypeScript speaks MCP and drives headless-Chrome capture; a resident Python worker does the vision, so the heavy imports load once, not per call.
It reads local paths and navigates to URLs you give it. Inputs are
size-capped, metrics isolated, the worker respawns on crash. Set
DESIGN_COMPARE_ALLOWED_ROOTS to sandbox reads.
MIT. Source, design notes, and the calibration workflow are on GitHub.
Give an agent a number to climb.