MCP server · Claude Code plugin · MIT
Coding agents write UI from training data, not from your design system.
design-system-mcp gives them the ground truth, read from your source, and a linter they run on their own output.
It serves your React components, props, cva variants and tokens over MCP, and hands the agent check_ui: every finding has a rule id, a location and a fix, so the agent corrects itself before you review anything.
Claude Code plugin· server, skill, hook
/plugin marketplace add dgesteves/design-system-mcp/plugin install design-system@dgesteves
Any MCP client, over stdio
$ npx -y @dgesteves/design-system-mcp~/acme-app — agent session
import { Trash2 } from "lucide-react"import { Badge } from "@/components/ui/badge"import { Button } from "@/components/ui/button"import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"export function DangerZone({ onDelete }: { onDelete: () => void }) { return ( <Card className="border-[#ef4444] p-[13px]"> <CardHeader> <CardTitle>Delete workspace</CardTitle> <Card.Description>This cannot be undone.</Card.Description> </CardHeader> <CardContent className="flex items-center gap-3"> <Badge tone="warning">Irreversible</Badge> <Button variant="danger" onClick={onDelete}> Delete workspace </Button> <Button variant="ghost" size="icon"> <Trash2 /> </Button> <button className="rounded-[7px] bg-gray-100 px-3 text-sm"> Cancel </button> <p style={{ color: "#737373", marginTop: 6 }}>Owners only.</p> </CardContent> </Card> )}Add a danger-zone card to the workspace settings page
- Write app/settings/danger-zone.tsx +29 lines
- design-system · check_ui (path: "app/settings/danger-zone.tsx")
8 errors·3 warningseach with a rule id, a location and a fix
- 9:22no-hardcoded-colorerrorborder-[#ef4444] → border-destructive
- 9:39no-hardcoded-spacingwarningp-[13px] → p-3
- 12:10no-unknown-componenterror<Card.Description> → <CardDescription>
- 15:16no-unknown-properrortone= → variant=
- 16:25no-unknown-varianterrorvariant="danger" → variant="destructive"
- 19:10icon-button-accessible-nameerror<Button size="icon"> → aria-label="Delete"
- 22:10prefer-design-system-componenterror<button> → <Button>
- 22:28no-hardcoded-radiuswarningrounded-[7px] → rounded-sm
- 22:42no-hardcoded-colorerrorbg-gray-100 → bg-muted
- 25:29no-hardcoded-colorerrorcolor: "#737373" → text-muted-foreground
- 25:50no-hardcoded-spacingwarningmarginTop: 6 → mt-1.5
- Edit app/settings/danger-zone.tsx resolved all 11 findings
- check_ui No design-system problems
The agent writes the card from what it learned in training.
Real output: the findings and fixes are what check_ui returns for this file against the demo design system, generated when this site was built. Try it on your own code
The problem
Your agent can't see your Storybook. TypeScript only catches part of it.
Ask for a settings card in a shadcn/ui project and you get bg-[#ef4444], p-[13px], a native <button> with hand-rolled classes, variant="danger" on a Button that only knows destructive, and an icon button nobody can name with a screen reader.
On the draft above, tsc reports 3 of the 11 problems: the invalid variant, the unknown prop and the missing member. It has no opinion on hex colors, off-scale spacing, native elements or accessible names. Review catches the rest, when someone has the time.
Does it help?
The same ten components, built twice per model.
Claude Code built ten components a chat product needs for vercel/ai-chatbot, a real shadcn/ui app: once as it ships, once with the plugin. Then design-system-mcp check scored every file it wrote.
Components with no design-system errors
Design-system errors, all ten tasks
- Cost
- −11%
- $1.31 → $1.17
- Avg. time
- 46 → 42 s
- Tool calls
- 5.6 per task
Components with no design-system errors
Design-system errors, all ten tasks
- Cost
- +7%
- $7.74 → $8.31
- Avg. time
- 108 → 141 s
- Tool calls
- 7.3 per task
Every task, every runShowHide
| Task | Haiku 4.5without | Haiku 4.5with plugin | Opus 5without | Opus 5with plugin |
|---|---|---|---|---|
| Usage bannera dismissible warning banner shown when the user is close to their monthly message limit ("You've used 90% of your messages this month"), with an Upgrade button and an icon-only close button. | 1 error1 × icon-button-accessible-name | clean | clean | clean |
| Delete dialoga confirmation dialog for deleting all chats, with a Cancel action and a red, destructive confirm action. | clean | clean | clean | clean |
| Settings carda card with a title, a description, an API key text input, a Save button and a small green 'Connected' status label. | 2 errors2 × no-hardcoded-color | clean | 4 errors4 × no-hardcoded-color | clean |
| Empty historyan empty state for the chat history sidebar with an icon, a short message and a 'New chat' button. | clean | clean | clean | clean |
| Message toolbara row of icon-only buttons under an assistant message: copy, regenerate, thumbs up and thumbs down. | clean | clean | clean | clean |
| Upgrade carda pricing card for the Pro plan with the price, a list of four features with check icons, a 'Most popular' label and an 'Upgrade to Pro' button. | clean | clean | clean | clean |
| Rate limit alertan inline red error alert telling the user they were rate limited, showing when they can retry, with a 'Try again' button. | 9 errors9 × no-hardcoded-color | clean | clean | clean |
| Share dialoga dialog showing the share link in a read-only input, with an icon-only copy button next to it and a secondary 'Make private' action. | 1 error1 × prefer-design-system-component | clean | clean | clean |
| Profile carda small card with the user's avatar initials in a colored circle, their name and email, a 'Free plan' label and a 'Sign out' button. | clean | clean | 1 error1 × no-hardcoded-color | clean |
| Shortcuts dialoga dialog listing keyboard shortcuts (New chat ⌘K, Toggle sidebar ⌘B, Copy last answer ⌘⇧C) in two columns, with styled key labels. | clean | clean | clean | clean |
Without the plugin, the misses were raw colors for things the prompt described (“a red alert”, “a green label”), a native <label> where the project has Label, and an icon button nobody could name with a screen reader. With it, the agent looked components and tokens up before writing (5.6 to 7.3 design-system tool calls per task), and the hook that checks each file never had to step in.
It is one project and forty runs, one per task, model and condition, so read it as a direction rather than a rate. Opus 5 already does well by copying the codebase; the smaller model gained the most, and cost less with the plugin than without. The method, per-run results and every generated file are in the repository.
On real codebases, with zero config
Run as is, with no config, on public apps (with version 0.2.0). These counts are not a judgement of the teams: hardcoded values and unlabeled icon buttons slip through review everywhere, and agents copy what they see.
vercel/ai-chatbot
c2f8235- Found with zero config
components.json→ 23 components, 52 tokenscheckfindings- 77 in
app/andcomponents/: 41 raw colors, 20 native elements the design system wraps, 11 icon-only buttons without an accessible name
midday
5158731,apps/dashboard- Found with zero config
- workspace package
@midday/ui→ 78 components checkfindings- 1,237 in
src/, including 105 icon-only buttons without an accessible name. Adopted with a baseline
shadcn/ui website
0132174- Found with zero config
- custom
uialias → 66 components checkfindings- 115 in
app/andcomponents/, mostly raw colors
How it works
Reads your source. Answers over MCP. Checks what the agent wrote.
Static analysis from start to finish: no model calls, no API key, and it runs offline. One check of the demo file takes 1.2 ms.
01
Reads your design system
One TypeScript program over your component files, with your tsconfig, so path aliases and dependency types resolve.
- Props, with types, defaults and JSDoc
cva()andtv()variants, and the classes each applies- Parts:
CardHeadernext toCard, orCard.Header - The native element each component wraps
- Tokens from CSS variables, Tailwind v4
@themeor a v3 config, and DTCG JSON - Docs and examples from Markdown next to the components
No config for a shadcn/ui components.json, a monorepo whose components live in a workspace package (@acme/ui), or the design-system package itself. How it finds them.
02
Answers the agent over MCP
list_components
List design-system components
get_component
Get a component contract
search_components
Search components by intent
get_tokens
Get design tokens
check_ui
Check UI code against the design system
Plus ds://tokens and ds://components/{name} as resources, and a build-with-design-system prompt. Every tool is read-only.
03
Checks what it wrote
check_ui parses the code on its own, so it works on fragments the agent has not saved, and resolves each tag through its imports.
- no-hardcoded-colorerror
- no-hardcoded-spacingwarn
- no-hardcoded-radiuswarn
- prefer-design-system-componenterror
- no-unknown-componenterror
- no-unknown-properror
- no-unknown-varianterror
- icon-button-accessible-nameerror
The same rules run in CI with check, and in Claude Code after every edit through the plugin's hook.
What the agent sees
Compact Markdown for the model, with JSON structuredContent for programs. These are the server's responses on the demo design system, captured when this site was built.
> get_component name: "Badge"# BadgeA small status label: counts, states ("Active", "Overdue") or categories.Not interactive by default; pass `asChild` to render a link.import { Badge } from "@/components/ui/badge"Renders <span> · components/ui/badge.tsx:29 · docs: docs/badge.md## Props- variant?: "default" | "secondary" | "destructive" | "success" | "outline" = "default"- asChild?: boolean = false — Render the child element with badge styles instead of a `<span>`.- …plus 280 props from React.ComponentProps<"span"> (onClick, id, role, children, aria-*, data-*, …)## Variantsvariant (default "default") default border-transparent bg-primary text-primary-foreground secondary border-transparent bg-secondary text-secondary-foreground destructive border-transparent bg-destructive text-white success border-transparent bg-success text-success-foreground outline text-foreground## Design tokens usedradius-md (rounded-md), ring (ring-ring), primary (bg-primary), primary-foreground (text-primary-foreground), secondary (bg-secondary), secondary-foreground (text-secondary-foreground), destructive (bg-destructive), success (bg-success), success-foreground (text-success-foreground), foreground (text-foreground)## Docs (docs/badge.md)### Guidelines- Keep the label to one or two words.- Map meaning to variants, not to colors: `success` for healthy or completed states, `destructive` for errors and overdue items, `secondary` for neutral metadata, `outline` for filters and tags.- Badges are not buttons. Wrap them in a link with `asChild` if they navigate.## Examples### Status```tsx<Badge variant="success">Active</Badge><Badge variant="destructive">Overdue</Badge>```### Tag```tsx<Badge variant="outline">Design system</Badge>```> check_ui code:<Button variant="primary" className="bg-blue-600 px-[18px]">Save</Button>snippet.tsx: 2 errors, 1 warning1:17 error [no-unknown-variant] "primary" is not a valid variant for <Button>. Allowed: default, destructive, outline, secondary, ghost, link. Did you mean "default"?1:38 error [no-hardcoded-color] `bg-blue-600` is Tailwind's default palette, not a design-system color. No token has this hue; nearest is ring (ΔE 0.294), a gray. Pick the semantic token that fits. <Button> already sets bg-* through `variant`; prefer a variant over overriding it.1:50 warning [no-hardcoded-spacing] `px-[18px]` is 18px, which is on the spacing scale: use `px-4.5`.Fix every error, then run check_ui again.⏺ Write(app/promo/page.tsx) ⎿ PostToolUse hook: app/promo/page.tsx breaks the project's design system 1:58 error [no-hardcoded-color] Hardcoded color `bg-[#f5f5f5]` → `bg-muted`. 1:77 error [prefer-design-system-component] Native <button> where the design system has <Button>. …⏺ The hook flagged five issues. Looking up Button and the color tokens before fixing.⏺ design-system - get_component (MCP)(name: "Button")⏺ Write(app/promo/page.tsx) → <Button variant="destructive"> on bg-muted, hook passesAlongside @shadcn/lint
Different layers. Running both is a sensible setup.
@shadcn/lint is a linter: it polices the classes written against a component and the theme, in React, Svelte and Vue, on Tailwind v4. design-system-mcp gives the agent the components, props, variants and tokens before it writes, and catches what does not exist.
Both
- Read your components and theme through
components.jsonin a shadcn/ui project - Flag raw colors in classes (
bg-[#ef4444]) and inline styles - Flag arbitrary values (
p-[13px],rounded-[7px])
design-system-mcp adds
- Ground truth before the agent writes: components, props, variants, parts and tokens over MCP
- Components, props and variant values that do not exist (
<Card.Header>,tone,"danger") - Native elements the design system wraps (
<button>→<Button>) - Icon-only buttons without an accessible name
- The agent loop: check_ui as a tool, and a Claude Code hook on every edit
- Tailwind v3 as well as v4, design-system packages and monorepos
@shadcn/lint adds
- Restyling a component with classes it should not take
- Unknown and dynamic classes
- Per-component contracts you write
- Svelte and Vue, as well as React
- Runs as ESLint 9.30+ or Oxlint rules
Works with
Your agent, your stack.
React only (.tsx and .jsx), with Node.js 22.18 or later. Styling is checked in Tailwind classes, style objects and color attributes, not in CSS-in-JS or CSS Modules. The full list of limits is in the docs.
Agents and editors
- Claude Code
- Cursor
- VS Code
- Claude Desktop
- Codex CLI
- Zed
- Gemini CLI
- Any stdio MCP client
Configs for each are in the setup guide.
Codebases
- shadcn/ui
- Radix
- Base UI
- React Aria Components
- Tailwind v3 and v4
- pnpm, npm, Yarn and Bun workspaces
- Design-system packages
- W3C DTCG tokens
Other layouts take a config file with a few globs.
Adopting it
An existing codebase fails only on new findings.
An established app can start with hundreds of findings: midday's dashboard has about 1,300. Record them once, commit the file, and check fails only on what is new.
$ npx -y @dgesteves/design-system-mcp check "src/**/*.tsx" --update-baselineBaseline: 1,307 findings in 275 files → design-system-mcp.baseline.json$ npx -y @dgesteves/design-system-mcp check "src/**/*.tsx"No new problems in 504 files (1,307 in the baseline).- Entries are keyed by file, rule and the offending text, with a count, not by line: edits elsewhere in a file do not invalidate them.
- When findings get fixed, check says so and prints the command that drops them, which locks the progress in.
- The baseline is for the CLI: check_ui still shows the agent every finding in the file it is editing.
- run: npx design-system-mcp check . --format github --require-design-system--format github turns findings into pull request annotations. --require-design-system fails the job when the globs stop matching, so CI cannot pass by checking nothing. More on CI.
Give your agent the design system it is supposed to use.
Install it, then run npx -y @dgesteves/design-system-mcp inspect in your app to see what it found. If it gets something wrong on your codebase, an issue with the snippet is the most useful thing you can send.
Claude Code plugin· server, skill, hook
/plugin marketplace add dgesteves/design-system-mcp/plugin install design-system@dgesteves
Any MCP client, over stdio
$ npx -y @dgesteves/design-system-mcp