Skip to main content
Glama

ds-mcp-starter

A contract-first Model Context Protocol server for a design system.

It reads DTCG design tokens, indexes them into a typed, deterministic manifest, and exposes the same pure core through three facades: an MCP server for LLM agents, a CLI for humans and CI, and an importable TypeScript library.

The repository ships a small fictional multi-brand system (brands aurora and nord, modes light and dark, 81 tokens per theme) so every command below runs as-is.

Contract first, MCP second

MCP is a transport. The value lives in the contract underneath it: a manifest with a schema, and a set of pure functions that answer design system questions from it.

  • The indexer turns token files into dist/manifest.json (plus its JSON Schema). No network, no clock, same input gives the same bytes.

  • The core is a set of pure functions over that manifest, each with a zod input and output schema.

  • The facades are thin. The MCP server registers the core functions as tools; the CLI prints them for people and returns exit codes for CI; the library exports them.

CI never starts an MCP server. It runs the same code through the CLI.

flowchart LR
  subgraph Sources
    T[tokens/**/*.json<br/>DTCG 2025.10]
  end
  T --> I[Indexer<br/>flatten, resolve aliases,<br/>detect cycles]
  I --> M[(Manifest<br/>dist/manifest.json<br/>+ JSON Schema)]
  M --> C[Core tools<br/>pure functions<br/>zod in / zod out]
  C --> S[MCP server<br/>stdio]
  C --> L[CLI ds<br/>humans + CI gates]
  C --> B[Library<br/>import from ds-mcp-starter]
  S --> A[Any MCP client<br/>LLM agents]

Related MCP server: Designesy

Tools

Tool

Access

Answers

search_tokens

read

Which token expresses this intent? Ranked search on paths and descriptions; semantic before primitive, deprecated last.

resolve_token

read

What does this token become in nord/dark, and through which aliases? Accepts color.fg.default, --color-fg-default or var(...).

find_token_for_value

read

Which token should replace this hard-coded #1a73e8 or 16px? Exact matches first, then perceptually nearest colors (OKLab). Optional CSS property to rank by intent.

lint_naming

read

Does this name follow category.role.variant[.state]? Returns violations and a corrected name.

audit_css

read

Where does this CSS hard-code values, use deprecated tokens or reference tokens that do not exist? With line, column and suggestions.

arbitrate_semver

read

Is this change set a major, minor or patch release, and why?

propose_token

write

What exactly would adding this token change? Returns a JSON Patch and a unified diff; writes only on explicit confirmation.

Tool descriptions are written for an LLM: when to call the tool and what it returns. Read-only and write tools are flagged with MCP annotations (readOnlyHint, destructiveHint).

Governance: AI prepares, humans decide

Agents are fast at preparing changes and bad at owning them. The server is built so that the default path is always a reviewable proposal.

  • Write tools preview by default. propose_token returns the target file, a JSON Patch and a diff. confirm: true is required to write.

  • Two keys for writes over MCP. Even with confirm: true, the MCP server refuses to write unless it was started with --allow-writes. The intended flow is: the agent previews, a person runs ds propose ... --confirm or applies the diff in a pull request.

  • Validation before preview. A proposal is rejected before any diff is produced if the name breaks the grammar, collides with an existing token or group, aliases a token that does not exist in every affected theme, or mixes types.

  • Naming lint. One grammar for people and agents. CSS property names (padding), brand or mode names (aurora, dark) and misplaced states are rejected with a suggested fix.

  • Semver arbitration. Removed or renamed tokens are majors, additions and deprecations are minors, value changes are patches unless the type changes. Removing a token that was never deprecated produces a warning: deprecate before you remove.

  • CI gate. ds audit fails the build on hard-coded values or unknown tokens; ds semver --max minor fails a change set that would require a major.

Quickstart

Requires Node.js 20 or later.

npm install
npm run build          # compile TypeScript to dist/
npm run manifest       # write dist/manifest.json and dist/manifest.schema.json
npm link               # optional: puts the ds and ds-mcp-server commands on your PATH

Without npm link, replace ds with node dist/cli/main.js below.

CLI

ds search "secondary text"
ds resolve color.text.primary --brand aurora --mode dark
ds find "#1a73e8" --brand nord --mode light
ds lint color.fg.subtle spacing.padding.md color.dark.surface
ds audit examples/button.css examples/legacy.css
ds semver examples/changes.json --max minor
ds propose color.fg.subtle --type color --value "{color.neutral.500}" \
  --description "Placeholder text and disabled labels." --brand aurora --mode light

Every command accepts --json. Exit codes: 0 success, 1 a check failed (lint, audit, semver above --max, rejected proposal), 2 usage or data error.

$ ds resolve color.text.primary --brand aurora --mode dark
color.text.primary (aurora/dark) = #f7f8fa
  chain:   color.text.primary -> color.fg.default -> color.neutral.50
  css:     var(--color-text-primary)
  type:    color, semantic, brands/aurora/modes/dark.json
  about:   Former name of the default text color.
  status:  deprecated. Use {color.fg.default}. Scheduled for removal in 2.0.0.

$ ds audit examples/legacy.css --brand nord --mode light    # excerpt
examples/legacy.css:3:12  error   hard-coded-dimension padding: Hard-coded dimension 16px. Use var(--spacing-inset-md).
examples/legacy.css:5:21  error   hard-coded-color     border: Hard-coded color #dde1e8. Nearest token: var(--color-border-subtle).
examples/legacy.css:14:14  warning deprecated-token     color: --color-text-primary is deprecated. Use {color.fg.default}. Scheduled for removal in 2.0.0.
examples/legacy.css:22:14  error   unknown-token        color: --color-fg-subtle looks like a design token but does not exist. Did you mean --color-bg-subtle, --color-fg-muted, --color-bg-surface?
examples/legacy.css: 14 error(s), 1 warning(s)

MCP client

Any MCP client that launches stdio servers can use it. See examples/mcp-client-config.json:

{
  "mcpServers": {
    "design-system": {
      "command": "node",
      "args": ["/absolute/path/to/ds-mcp-starter/dist/server/main.js", "--tokens", "/absolute/path/to/ds-mcp-starter/tokens"]
    }
  }
}

Server options: --tokens <dir> (default tokens, or $DS_TOKENS_DIR), --manifest <file> to serve a prebuilt manifest in read-only mode, --allow-writes to let propose_token write on confirmation. Logs go to stderr.

Library

import { buildManifestFromDirectory, findTokenForValue, auditCss } from "ds-mcp-starter";

const manifest = await buildManifestFromDirectory("tokens");
findTokenForValue(manifest, { value: "#1a73e8", property: "background" }).recommendation;
auditCss(manifest, { css: ".a { padding: 16px }" }).ok; // false

Token layout

tokens/
  core/*.json                      primitives shared by every theme (color scales, spacing, radius, font, border)
  semantic/*.json                  semantic tokens shared by every brand (spacing.inset.md -> spacing.4)
  brands/<brand>/brand.json        semantic tokens of one brand (radius.control, font.family.brand)
  brands/<brand>/modes/<mode>.json semantic colors of one brand in one mode

A theme is one brand in one mode: core + semantic + brand + mode, merged in that order. A path may be defined only once per theme. Values follow DTCG 2025.10 ({ "colorSpace": "srgb", "components": [...], "hex": ... } for colors, { "value": 16, "unit": "px" } for dimensions), aliases use {path} and $deprecated names its replacement. In code, paths are dot-separated (color.fg.default); in CSS they become --color-fg-default.

Project structure

src/
  core/            pure: DTCG parsing, alias resolution, manifest schema, and the 7 tools
    tools/         one file per tool: zod input, zod output, pure function
  node/            the only file system code: discover token files, build/read/write the manifest, apply proposals
  tools/           facade-agnostic tool registry (name, LLM-facing description, schemas, read/write)
  server/          MCP facade: registers the registry on an McpServer, stdio entry point
  cli/             CLI facade: argument parsing, human formatting, exit codes
  index.ts         library facade
tokens/            demo DTCG tokens (2 brands x 2 modes)
examples/          MCP client config, clean and legacy CSS, a semver change set
scripts/           stdio smoke test of the built server
test/              Vitest suites, including an in-memory MCP end-to-end test

Testing

npm test            # 63 tests
npm run typecheck
npm run lint        # Biome
npm run smoke       # starts dist/server/main.js over stdio and calls a tool
npm run check       # everything CI runs, in order

The suites cover alias resolution (multi-level chains, cycles, unknown references, type mismatches, duplicates), value serialization, search ranking, nearest-color matching, every naming rule, CSS audit positions and suggestions, semver rules, proposal preview versus confirmation, and an end-to-end MCP session through the SDK client over an in-memory transport. Two repository guards run with them: no stdout writes in server-side modules, and token files kept in the canonical format the tools write. The demo tokens are also linted with the naming grammar, so the example system follows its own rules.

Design decisions

  • Contract first. The manifest has a zod schema, published as JSON Schema next to it. Consumers validate what they read. The MCP tools publish input and output schemas and return structuredContent validated against them.

  • One core, three facades. Business rules live in src/core only. The MCP server and the CLI only parse input and format output, so behavior is tested once, without starting a server.

  • No network in the indexer. Token sources are local files; the indexer is a pure function from sources to manifest. Deterministic output (sorted, no timestamps) keeps the manifest diffable in code review.

  • stdout belongs to the protocol. The server logs to stderr only. A Biome rule (noConsole outside the CLI), a test scanning server-side modules and a stdio smoke test protect the stream.

  • Deprecate before you remove. Deprecations name their replacement ($deprecated: "Use {color.fg.default}."). The audit reports usages with the replacement, search ranks deprecated tokens last, and semver arbitration warns when a token disappears without a deprecation period.

  • Intent over value. Matching a value is not enough: 16px is both a padding and a radius. find_token_for_value and audit_css rank tokens by the intent of the CSS property and prefer semantic tokens over primitives.

  • Small on purpose. Two runtime dependencies (@modelcontextprotocol/sdk, zod), exact versions, no bundler.

Roadmap

  • Figma variables export: map brands and modes to variable collections and modes from the same manifest.

  • Code Connect hints: link components to the tokens they consume, so agents can answer "which token does this component use".

  • Streamable HTTP transport, for a shared read-only server in front of a published manifest.

  • Manifest diff: compute arbitrate_semver input from two manifests instead of a hand-written change set.

  • Composite types (typography, shadow, gradient) and partial references.

License

MIT, Florian Cazé.

Available Tools

7 tools
arbitrate_semverArbitrate the semver bump of token changesA
Read-onlyIdempotent

Given the token changes of a release (added, removed, renamed, value-changed, deprecated), return the required version bump (major, minor, patch) with a reason per change, and warnings when a token is removed without having been deprecated first. Use it to prepare a release or review a change set.

Inputs: changes (required). Returns: bump, changes, warnings, principle

audit_cssAudit CSS for token usageA
Read-onlyIdempotent

Audit a CSS snippet: reports hard-coded colors and dimensions (with the token to use instead), usages of deprecated tokens (with their replacement) and references to tokens that do not exist. Returns ok=false when there is at least one error. Use it to review generated or legacy CSS.

Inputs: css (required), mode, allow, brand. Returns: ok, findings, errorCount, warningCount

find_token_for_valueFind the token for a hard-coded valueA
Read-onlyIdempotent

Given a hard-coded value (#1a73e8, rgb(...), 16px, 1rem), return the tokens that carry it: exact matches first (semantic before primitive), then the perceptually nearest colors or nearest dimensions. Use it whenever code or a design contains a literal value, to replace it with a token.

Inputs: value (required), mode, type, brand, +2 more. Returns: type, exact, value, nearest, +2 more

lint_namingLint token namesA
Read-onlyIdempotent

Check proposed token names against the naming grammar category.role.variant[.state]: lowercase kebab segments, known category, intent words instead of CSS properties (inset, not padding), state last, no brand or mode in the name. Returns violations and a corrected name when one can be derived. Call it before proposing a token.

Inputs: names (required). Returns: valid, results

propose_tokenPropose a new tokenA
Destructive

Prepare the addition of a token. Validates the name (lint_naming), collisions, the scope and the value (aliases must exist and have the same type), then returns a preview: the target file, a JSON Patch and a unified diff. Nothing is written unless confirm is true AND the server was started with writes allowed. Default to a preview and let a human review it.

Inputs: path (required), type (required), value (required), mode, +3 more. Returns: diff, path, patch, status, +4 more

resolve_tokenResolve a tokenA
Read-onlyIdempotent

Get the final value of one token for a given brand and mode, with the alias chain that produced it (for example color.fg.default -> color.neutral.900). Accepts a dot path, a CSS custom property or var(). Use it to explain a value or to check what a token becomes in dark mode.

Inputs: mode (required), path (required), brand (required). Returns: mode, path, type, brand, +9 more

search_tokensSearch design tokensA
Read-onlyIdempotent

Find design tokens by intent or name, for example 'muted text', 'control radius' or 'danger'. Use it before writing any color, spacing, radius or font value. Returns ranked tokens with their CSS custom property, description, deprecation status and resolved value per brand and mode. Semantic tokens rank above primitives; deprecated tokens rank last.

Inputs: query (required), mode, brand, limit. Returns: query, total, results

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.3/5.0

    Scored across 7 tools

    Disambiguation5/5

    Each tool targets a distinct operation: intent/name search (search_tokens), single-token resolution (resolve_token), reverse lookup from a literal value (find_token_for_value), naming validation, CSS auditing, semver arbitration, and token proposal. The three token-lookup tools could superficially look similar, but their descriptions make the input types (intent phrase vs dot path vs literal value) unambiguous.

    Naming Consistency5/5

    All seven tools follow a strict snake_case verb_noun pattern (search_tokens, resolve_token, find_token_for_value, lint_naming, audit_css, arbitrate_semver, propose_token). Verbs are meaningful and consistently chosen, with no camelCase or stylistic mixing.

    Tool Count5/5

    Seven tools is well-scoped for a design-token governance server; each covers a distinct phase (discover, resolve, validate, audit, version, propose). No tool feels redundant or like padding.

    Completeness4/5

    The surface covers discovery, resolution, reverse lookup, validation, auditing, semver, and additive proposals well. Gaps remain around modifying or deleting existing tokens (rename/remove are only analyzed, not executed), but agents can work around this via propose_token and arbitrate_semver.

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      Design contract layer for AI agents. Scans Figma, code, Storybook, and token files, reconciles conflicts, and serves a single machine-readable source of truth so every agent gets the same authoritative design rules before it builds. Local-first.
      9
      37 npm
      21
      Apache 2.0
    • A
      license
      A
      quality
      B
      maintenance
      Enables AI agents to score live URLs against a deterministic design contract, validate DTCG tokens and Lottie animations, audit accessibility, and retrieve design-system contracts, catalogs, and review rubrics.
      17
      5 npm
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables coding agents to query a workspace's design system before writing UI and validate generated code against the same system afterward, using configurable token and component sources.
      13 npm
      1
      MIT
    • F
      license
      Not graded
      quality
      B
      maintenance
      Enables AI coding agents to inspect a project's design tokens and component catalog, audit React/Tailwind files against seven static UI quality rules, score overall UI health, and apply auto-fixes that are re-audited and reverted on regression. It works both locally over filesystem access and remotely on raw TSX/JSX or CSS snippets passed directly as strings.
      -