Skip to main content
Glama

ds-canon

Your design system's canon, queryable by agents. What exists, what is deprecated, what breaks if you touch it.

Design systems decay into tribal knowledge the moment the token sheet drifts from the code and the person who remembers why gets pulled onto another project. Agents writing UI code make this worse: they hallucinate plausible token names and confident-sounding component APIs because they have nothing authoritative to check against. ds-canon puts your design system's tokens, components, conventions, and deprecations behind a read-only MCP server, so agents and the humans directing them query the same system of record instead of guessing.

The path from zero to a drift sweep

The user journey, end to end:

flowchart LR
  A[Find the repo] --> B[npx -y ds-canon: try the demo]
  B --> C[Clone and build]
  C --> D[Register as an MCP server]
  D --> E[Query the tools]
  E --> F[Author your own canon]
  F --> G[Boot your system]
  G --> H[Sweep for drift]
  H --> I[Read the findings]

Related MCP server: Design System MCP Server

The 60-second demo

Run it straight from npm, nothing to clone:

npx -y ds-canon

You'll see a one-line banner confirming what loaded:

ds-canon v0.2.0 serving Nimbus DS (40 tokens, 9 components) from /path/to/ds-canon/fixtures, read-only. Waiting for an MCP client on stdin (Ctrl+C to exit). For a one-off CLI drift scan, run: ds-canon drift <path>.

Then it sits there. That is correct: ds-canon is a stdio MCP server, so after the banner it blocks waiting for a client to speak JSON-RPC on stdin. It is not hung. Register it with an MCP client (below), run the bundled smoke-test client (node examples/mcp-client.js), or press Ctrl+C to exit.

The banner prints to stderr, not stdout, because stdout is reserved for the MCP protocol stream; a stray log line on stdout would corrupt the JSON-RPC frames a client reads. So if you are watching stdout you will see nothing until a client connects, and that is by design.

The server ships with a fixture design system called Nimbus DS so you can try it immediately, no setup required. Point an MCP-aware agent at it and ask it real questions.

"Which accent color should I use, and is anything deprecated?"

The agent calls list_tokens with group: "color", query: "color.accent", then whats_deprecated. Real output, verbatim:

{
  "tokens": [
    {
      "name": "color.accent.primary",
      "value": "#3B5BDB",
      "type": "color",
      "group": "color",
      "status": "active",
      "description": "Primary brand accent. Used for primary actions, active navigation state, and focus affordances."
    },
    {
      "name": "color.accent.secondary",
      "value": "#5C7CFA",
      "type": "color",
      "group": "color",
      "status": "active",
      "description": "Secondary accent for lower-emphasis interactive elements that still need to read as brand-colored."
    },
    {
      "name": "color.accent.legacy",
      "value": "#4C6EF5",
      "type": "color",
      "group": "color",
      "status": "deprecated",
      "description": "Original brand blue from the v1.x palette. Slightly less saturated than accent.primary; kept only for Banner and LegacyButton until both migrate.",
      "deprecatedBy": "color.accent.primary"
    }
  ]
}

(The query parameter is a substring match on name and description, so a looser query like "accent" also surfaces tokens whose descriptions mention accent usage. Scoping the query to the name prefix keeps the answer tight.)

{
  "deprecated": [
    { "name": "color.accent.legacy", "kind": "token", "deprecatedBy": "color.accent.primary", "dependentCount": 1 },
    { "name": "LegacyButton", "kind": "component", "deprecatedBy": "Button", "dependentCount": 0 }
  ],
  "legacy": []
}

The agent now knows to recommend color.accent.primary and to flag color.accent.legacy as on its way out, with one live component (Banner) still depending on it. The separate legacy array (empty here) is for values that are live and intentionally kept, not scheduled for removal, so they never get mixed in with the migrate-off-me list.

"I want to change space.inset.md. What will it affect?"

This is the question a design system actually needs to answer before anyone touches a shared value. The agent calls find_usages:

{
  "entity": "space.inset.md",
  "usages": [
    { "dependent": "Button", "dependentKind": "component", "relation": "consumes token" },
    { "dependent": "Card", "dependentKind": "component", "relation": "consumes token" },
    { "dependent": "Field", "dependentKind": "component", "relation": "consumes token" },
    { "dependent": "Modal", "dependentKind": "component", "relation": "consumes token" }
  ]
}

Blast radius, in one call: four components, named exactly. No spelunking through a component library to find every place 12px got typed in by hand.

"Write a secondary button that follows our conventions."

The agent calls get_component for Button (props, variants, the tokens it consumes, and its doNotUse guidance) and get_conventions for the color topic, then writes the component. Now suppose it (or a human) had instead hardcoded the color:

<button style={{ background: '#3B5BDB', padding: '12px' }}>Save</button>

Running that snippet through check_token_drift catches both literals:

{
  "findings": [
    {
      "severity": "error",
      "raw": "#3B5BDB",
      "suggestion": "color.accent.primary",
      "message": "Hardcoded value #3B5BDB matches token \"color.accent.primary\". Use the token instead of the raw value."
    },
    {
      "severity": "error",
      "raw": "12px",
      "suggestion": "space.inset.md",
      "message": "Hardcoded value 12px matches token \"space.inset.md\". Use the token instead of the raw value."
    }
  ]
}

#3B5BDB and 12px are exact token values, so each finding is error: a token already exists, so this should be fixed. Findings come in three tiers. error means an exact token exists for the value. warn means a near-miss (a value close to a token, or an off-scale spacing value near a step) worth a look. info covers everything else: unmatched values, and matches whose token role does not fit the property (a background token suggested for a color property is reported honestly, not as an action).

Install

ds-canon runs as a local MCP server over stdio. There is no separate service to deploy.

mcp.json (Claude Desktop, or a project-level .mcp.json for Claude Code):

{
  "mcpServers": {
    "ds-canon": {
      "command": "npx",
      "args": ["-y", "ds-canon"]
    }
  }
}

Claude Code, one line:

claude mcp add ds-canon -- npx -y ds-canon

Claude Desktop: add the same mcpServers entry to your claude_desktop_config.json and restart the app.

Working from a clone instead (for development or custom fixtures): git clone, npm install, npm run build, then point command at node with args: ["/absolute/path/to/ds-canon/dist/index.js"].

Tools

Eight tools, all read-only.

Tool

What it answers

Key inputs

list_tokens

What tokens exist?

group?, status?, query?

get_token

What is this token and who uses it?

name

list_components

What components exist?

status?, tag?

get_component

What does this component look like?

name

find_usages

What breaks if I change this?

entity

whats_deprecated

What should I stop using?

none

get_conventions

What are the house rules?

topic?

check_token_drift

Does this code drift from the token system?

snippet or path, lang?

get_token, get_component, and find_usages look up by exact name; a miss returns a not_found error with up to three closest-name suggestions instead of an empty result, so a typo doesn't read as "this doesn't exist."

check_token_drift takes either a snippet string or a path to a single file (read-only, 20MB cap). Give it a path and every finding carries a 1-based line. whats_deprecated returns two arrays: deprecated (scheduled for removal) and legacy (live and intentionally kept, see below), so a rebrand alias you keep on purpose does not read as something to migrate off.

Drift on the command line (CI)

Drift is also reachable without an MCP client, for pre-commit hooks and CI:

npx ds-canon drift "src/**/*.css" --fixtures /path/to/your/fixtures --fail-on error

It reads the files directly (a file, a directory, or a glob), prints a file:line table, and exits non-zero when findings meet the threshold. --fail-on error (the default) fails only when an exact token exists for a hardcoded value; --fail-on warn also fails on near-misses. --fixtures defaults to DS_CANON_FIXTURES, then the bundled Nimbus fixtures. The CLI and the MCP tool share one scan path, so a finding reads the same either way.

The bundled smoke-test client

examples/mcp-client.js is a tiny, dependency-free stdio client (initialize, tools/list, tools/call). It doubles as a smoke test: after npm run build, run

node examples/mcp-client.js

and it starts the server, lists the tools, runs a drift check, and prints a token count, then exits. If you want to see the JSON-RPC framing ds-canon expects from a client, this is the shortest complete example.

Pitfalls

Two behaviors surprised early users. Both are handled now; this is how they work so the output is never misread.

  • Definitions are not usages. A line that defines a custom property (--card: #fff;) is the token's own source, not a place that drifted from it, so check_token_drift never flags the value on a --name: declaration. It handles several declarations packed on one line and names with digits (--paper-2:). Only actual usages (background: #fff) are flagged. Earlier versions flagged every :root definition, which made a fully tokenized stylesheet look like nothing but drift.

  • No space tokens means no spacing noise. If your canon defines zero space.* tokens (a legitimate choice), px values are not measured against a scale that does not exist. Instead of one "off-scale" finding per literal, you get a single info noting that spacing literals were not checked. The same rule applies to font-size and line-height literals when no type/dimension tokens are defined.

Point it at your own system

ds-canon reads three files from a fixture directory: tokens.json, components.json, and conventions.md. By default it loads the bundled Nimbus DS fixtures. Set DS_CANON_FIXTURES to point it at your own:

DS_CANON_FIXTURES=/path/to/your/design-system node dist/index.js

or in mcp.json:

{
  "mcpServers": {
    "ds-canon": {
      "command": "npx",
      "args": ["-y", "ds-canon"],
      "env": { "DS_CANON_FIXTURES": "/path/to/your/design-system" }
    }
  }
}

tokens.json accepts the tested flat, string-valued W3C DTCG token shape: groups may be nested, and each token leaf must provide string $value and $type fields, with optional string $description. Style Dictionary or Tokens Studio exports work only when they match that shape. Composite values and tool-specific export shapes need an adapter before loading. Deprecation and aliasing use two extensions on top of the base spec: an $extensions["ds-canon"] block for status/deprecatedBy, and DTCG's own {token.path} reference syntax for aliases. The server also requires components.json, a flat { meta, components } shape matching the DsComponent type in src/types.ts, and conventions.md, with one ## topic section per convention (naming, spacing, color, accessibility, deprecation) plus Rule:, Rationale:, and Example: lines. The loader validates all three at startup and throws a specific file-and-field-level error on malformed input rather than serving a partially loaded system.

Two $extensions["ds-canon"] features cover the messy parts of a real system. Set a token's status to legacy for a value that is live and intentionally kept but no longer preferred (a rebrand alias), distinct from deprecated, which implies removal. And declare intentional raw values so drift never flags them, with a top-level sanctionedLiterals list where each entry needs a value and a required reason:

{
  "$extensions": {
    "ds-canon": {
      "sanctionedLiterals": [
        { "value": "#f7f7f5", "reason": "page background, intentionally not tokenized" }
      ]
    }
  },
  "color": {}
}

For a full worked example that converts a flat CSS custom-property sheet into this shape, see docs/adopting-your-own-system.md.

Why read-only, why stdio, why no network

Every tool in ds-canon reads from an in-memory index built once at startup. Nothing in this server writes to the fixture files, calls out to a network, or accepts write operations of any kind. That's not an implementation gap, it's the point: a design system's system of record should not be mutable by the same agents that consume it, and a tool that only answers "what exists" cannot be tricked into becoming a tool that changes what exists.

Running over stdio means ds-canon has no exposed network listener. Risk is limited to local process input, file access, and parser behavior. The process caps each fixture file at 20 MB and caps fuzzy name and entity lookups at 256 characters before similarity work begins. The deployment model doubles as the governance model: install it, point it at your fixtures, and every agent that would otherwise guess now has one unwritable source of truth to query instead.

How this was built

ds-canon was built by a multi-agent factory in an afternoon: parallel contract-first builders working from frozen type definitions, adversarial challengers doing black-box QA and architecture review against the built server, and agent-to-agent fix loops that dispositioned every finding before the next phase started. Of 28 findings, 24 were fully fixed, 2 were accepted or required no change with written rationale, and 2 were partially fixed with scoped follow-on enhancements deferred. The full build log, prompts, and challenge reports are in factory/.

License

MIT. See LICENSE.

Jay Trainer, Sr. Director, Product Design, AI-Native. jaytrainerdesign.com

Install Server
A
license - permissive license
A
quality
A
maintenance

Maintenance

–Maintainers
–Response time
–Release cycle
1Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server that exposes your design system components and tokens to AI agents, preventing duplicate component creation and hardcoded token values.
    19
    9
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides resources, tools, and prompts for a Design System via MCP protocol, enabling component search, reading, and related component discovery.
    381
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    A read-only MCP server that provides AI coding agents with a queryable contract for design system tokens, components, patterns, and anti-patterns.
    24
    1
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    An MCP server that gives AI assistants structured access to a design system's tokens, components, guidelines, and patterns, enabling them to read, lint, and author design system data.
    1
    MIT

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jtrainer357/ds-canon'

If you have feedback or need assistance with the MCP directory API, please join our Discord server