Skip to content
design-system-mcp

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

app/settings/danger-zone.tsx29 lines, unchecked
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

  1. Write app/settings/danger-zone.tsx +29 lines

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.

Claude Haiku 4.520 runs

Components with no design-system errors

Without6 / 10
With plugin10 / 10

Design-system errors, all ten tasks

Without13
With plugin0
Cost
−11%
$1.31 → $1.17
Avg. time
46 → 42 s
Tool calls
5.6 per task
Claude Opus 520 runs

Components with no design-system errors

Without8 / 10
With plugin10 / 10

Design-system errors, all ten tasks

Without5
With plugin0
Cost
+7%
$7.74 → $8.31
Avg. time
108 → 141 s
Tool calls
7.3 per task
Every task, every runShow
Design-system errors per task, for each model, without and with the plugin
TaskHaiku 4.5withoutHaiku 4.5with pluginOpus 5withoutOpus 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-namecleancleanclean
Delete dialoga confirmation dialog for deleting all chats, with a Cancel action and a red, destructive confirm action.cleancleancleanclean
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-colorclean4 errors4 × no-hardcoded-colorclean
Empty historyan empty state for the chat history sidebar with an icon, a short message and a 'New chat' button.cleancleancleanclean
Message toolbara row of icon-only buttons under an assistant message: copy, regenerate, thumbs up and thumbs down.cleancleancleanclean
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.cleancleancleanclean
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-colorcleancleanclean
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-componentcleancleanclean
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.cleanclean1 error1 × no-hardcoded-colorclean
Shortcuts dialoga dialog listing keyboard shortcuts (New chat ⌘K, Toggle sidebar ⌘B, Copy last answer ⌘⇧C) in two columns, with styled key labels.cleancleancleanclean

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.

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() and tv() variants, and the classes each applies
  • Parts: CardHeader next to Card, or Card.Header
  • The native element each component wraps
  • Tokens from CSS variables, Tailwind v4 @theme or 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
> 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
> 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.
Claude Code, with the plugin's hook
⏺ 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 passes

Alongside @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.json in 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.

Baseline
$ 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.
.github/workflows/ci.yml
- 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