ds-mcp-server
# ds-mcp-starter
A contract-first [Model Context Protocol](https://modelcontextprotocol.io) server for a design system.
It reads [DTCG](https://www.designtokens.org/tr/2025.10/format/) 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.
```mermaid
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]
```
## 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.
```sh
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
```sh
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.
```text
$ 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`](examples/mcp-client-config.json):
```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
```ts
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
```text
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
```text
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
```sh
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](LICENSE), Florian Cazé.
TDQS
Scored across 7 tools
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.
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.
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.
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.