Extract Design — Dembrandt
Dembrandt runs a headless Chromium browser against any URL, walks up to thousands of DOM elements, reads computed CSS, and returns a structured design system: colors with confidence scoring, typography styles, spacing scale, border radius, borders, shadows, and interactive component styles.
How to Run
MCP Usage (async by default)
To expose Dembrandt as MCP tools, add this server to the agent's MCP config (no install — npx fetches it on first run):
When using the Dembrandt MCP server, all extraction tools return a job_id immediately rather than blocking. Poll get_job_status until status is "completed":
Hand the job_id to the analysis tools instead of passing the extraction back. Every pure tool accepts it, and the queue keeps the whole extraction for an hour, so a job started by a narrow tool such as get_color_palette still feeds export_dtcg. Passing the extraction inline works and wins when you give both, but a real extraction is far too large to travel back through the model as a tool argument. [dembrandt 0.29+]
Pass sync: true to any extraction tool to block and return the result directly (useful on fast networks, risks timeout on slow sites, and proportionally slower when pages is above 1).
Extraction tools: get_design_tokens (everything), get_color_palette, get_typography, get_component_styles, get_surfaces, get_spacing, get_brand_identity, get_motion (durations, easings, named keyframes, hover patterns, and the gradients that travel with them) [dembrandt 0.36+]. All accept slow, mobile (mobile viewport), and cookie (cookie string for authenticated pages); get_design_tokens and get_color_palette also accept darkMode and wcag (contrast analysis). [dembrandt 0.23.1+ for mobile/cookie/wcag]
Every extraction tool also crawls, which is the single biggest lever on token quality: one page gives you one page's tokens. [dembrandt 0.29+]
A page that fails to load is dropped and the merge carries the rest, so a crawl does not fail on one bad URL.
Pure tools (no browser, synchronous; the extraction goes in result, or name a completed job with job_id [dembrandt 0.29+]): compute_drift (0-100 drift score between two extractions; takes baselineJobId and candidateJobId as the job-based form), get_findings (design-system lint: contrast, consistency, duplication), export_dtcg (W3C Design Tokens format), generate_design_md (DESIGN.md brand guide), render_report (self-contained HTML report), export_tailwind (Tailwind v4 @theme block), export_shadcn (shadcn/ui theme, slots left at shadcn's defaults where the page supplied nothing). Job control: get_job_status, list_jobs, cancel_job. [dembrandt 0.23.1+ for get_findings/export_dtcg/generate_design_md/list_jobs, 0.36+ for the two emitters]
Three tools take their own input rather than an extraction [dembrandt 0.36+]: validate_dtcg (check a DTCG document against the 2025.10 spec, including one this server produced, so a hand edit cannot quietly break it), check_contrast (grade colour pairs you name against WCAG 2.1 at the threshold the text size earns, for colours you are about to ship rather than ones already on a page), and check_robots (ask whether robots.txt allows a URL before spending a browser run on it).
Note: npx runs a dembrandt-mcp already on PATH in preference to the version named in --package, so a globally installed dembrandt silently shadows the pinned one. Symptom: options the pinned version supports are rejected as unknown, or a crawl returns a single page. Check with dembrandt --version and upgrade the global install, or point the MCP config at an explicit path.
Note: dembrandt <=0.23.0 fails to start via the npx one-liner above (McpDepsMissingError) — the MCP SDK was an optional peer dependency. Fixed in 0.23.1; require it.
Output Structure
Dembrandt returns a structured object. The key sections:
Working with Extracted Tokens
Seeding a Tailwind theme (dembrandt 0.28+)
Don't hand-map the JSON. --tailwind writes a Tailwind v4 @theme block directly:
Observed values only: no 50–950 shade ramps, no interpolated scale steps, no derived hover or on-colour variants. An invented shade is indistinguishable from a measured one once it is in the file, so the export is a starting point you extend by hand. Colours keep their semantic role name (--color-primary) or the page's own custom property name where one is declared; the rest are numbered --color-brand-N. Spacing collapses to v4's --spacing multiplier when the page has a base-N rhythm, and falls back to named steps otherwise. Tailwind's defaults still apply to anything not listed, so the block extends the theme rather than replacing it.
v4 only. For a v3 tailwind.config.js, map the output by hand — colors.semantic → theme.colors, typography.styles → fontFamily, spacing.commonValues → spacing, borderRadius.values → borderRadius, shadows → boxShadow.
Seeding a shadcn/ui theme
--shadcn (0.34.0+) writes the theme, so do not hand-map the variables:
The file carries the :root block and the @theme inline mapping Tailwind v4
needs, in oklch. A slot is written only where the page supplied a value; the
rest are named in the file header and left to shadcn's own defaults, so an
unobserved slot never arrives as a plausible value that reads as measured.
--dark-mode produces the .dark block, one colour scheme per run.
Do not derive --radius from borderRadius.values[0]: that list is sorted by
length, so the first entry is the smallest radius on the page, not the one its
buttons and inputs use. The flag takes the most-used value.
Reading confidence levels
Dembrandt scores every color by semantic context:
Since 0.28.0 confidence also has a usage floor, as spacing and radii always had: a colour seen once caps at low, twice at medium, and high needs three occurrences whatever its semantic context scores. Hover and focus colours are the exception and keep medium — their single occurrence is provenance, not a usage claim.
Start with high confidence colors when building a palette. Include medium for full coverage. Treat low as reference only.
Colour notation
Never convert a colour by hand and never re-derive one with your own maths. Every palette entry and every CSS variable already carries lch and oklch alongside the hex, so read the field you need straight from the JSON. --color-format only changes what the terminal prints, so it is the wrong tool when you are consuming JSON or MCP output.
Use hex (normalized) as the identity of a colour: it is what dedup, drift comparison and every downstream tool key on. Two entries with the same hex are the same token even when their emitted notations differ. When an author declared a token in a modern notation, cssVariables[name].value preserves it exactly, which is what you want when writing CSS back into that codebase, since it keeps the author's own notation and stays inside their gamut.
Flags Reference
Drift Detection & CI (dembrandt 0.19+)
--compare turns extraction into a gate. Save a known-good baseline, then compare later extractions against it:
- Runs the canonical drift engine over structured tokens — deterministic, not a pixel/render diff.
- Exit code:
0stable,1drift. Gates a pipeline directly. --htmlwrites a self-contained report; with--compareit includes a drift banner (added/removed/changed tokens). Attach it as a CI artifact.
Baselines churn once on 0.28.0. Three fixes move colour and typography values: the palette usage floor, body ending at the 24px reading range (non-heading text above it takes text, so hero copy stops landing on the body token), and families under 2% of counted text being dropped. Measured on dembrandt.com against a 0.27.1 extraction, drift came out at 15 against a threshold of 10 — enough to fail a gate. On the first run after upgrading, re-approve with --compare <baseline> --approve or regenerate the baseline. Drift after that is real drift.
0.32.0 needs no re-approval. Schema 1.12.0 measured 7 and 6 against a threshold of 10 on two reference sites, the only difference being the added mono context. The Tailwind shadow ladder does reorder by depth rather than blur alone, so --shadow-sm/md/lg/xl can move for an unchanged site.
0.35.0 changes what the gate can fail on. Before it, a changed brand colour was divided by every palette
entry that stayed the same: a real stripe.com baseline with semantic.primary turned magenta scored stable and
exited 0. The semantic map is scored on its own weight now, so a moved role reaches the threshold. Tolerance
for run-to-run variance is unchanged, and a single palette entry appearing or vanishing still does not gate.
Measured on two reference sites against 0.34.2: 0 and 4 against a threshold of 10, so no re-approval is needed
for a site that did not change. A site whose brand colour genuinely moved will fail a gate that passed before.
Also on 0.35.0: off-grid spacing findings were never produced at all, because the check compared against a
spacing.scaleType spelling the extractor stopped writing in 1.14.0. Sites with off-grid values now carry the
finding and a lower consistency score.
Determinism: capture the baseline in the same environment you check it in (both production, or both the same preview). A baseline from one environment compared against another shows false drift.
In CI: run --compare <baseline> --html report.html against a preview/deployed URL, fail the job on exit 1, upload the HTML artifact. Programmatic: import computeDrift from dembrandt/drift and generateHtmlReport from dembrandt/report to diff and render server-side without the CLI.
Anti-Bot and SPA Handling
Dembrandt handles common extraction challenges automatically:
- SPA hydration — waits 8s for React/Vue/Svelte to render before extracting
- Lazy content — scrolls the full page to trigger lazy-loaded components
- Cloudflare / bot walls — auto-retries with a visible browser if headless is blocked
- Slow sites — use
--slowfor 3× timeouts on heavy JS bundles - Cookie banners — dismisses common CMP dialogs (OneTrust, cookielaw, GDPR patterns) automatically
- Bot detection bypass — use
--stealthto opt in to navigator spoofing and human mouse simulation; off by default so the tool identifies itself honestly - robots.txt — read once per origin and matched against the User-Agent the browser actually sends. Advisory by default; set
DEMBRANDT_ENFORCE_ROBOTS=1for scheduled jobs and server-side use, where nobody is deciding what may be fetched, and a disallow or a file we could not read skips the target with exit4. A missing robots.txt is not a refusal (0.32+)
Checklist After Extraction
- Identify the 3–5 high-confidence colors — these are the core brand palette
- Check
colors.semantic.primary— is it correct? - Look at
typography.styles— what are the heading and body fonts? - Check
spacing.scaleType— 4px, 8px, or custom? custom is a real answer, not a gap - Review
components.buttons— how many variants exist? - Check
frameworks— thecss-frameworkandui-libraryentries shape how you apply the tokens. Acoveragenear 0 is a widget on the page, not what the page is built with. Noversionmeans the page does not state one; do not guess it. - Use
--dark-modeif the site has a dark theme - Use
--crawl 3if the site has a multi-section design system spread across routes

