Content
# Design System MCP Server
An [MCP](https://modelcontextprotocol.io) server that lets an LLM query your
design-system documentation (markdown). Built with **Elixir** and
[`anubis_mcp`](https://hex.pm/packages/anubis_mcp), served over **Streamable
HTTP** (with **stdio** also available). No database, no Phoenix — docs are
parsed once into memory at startup.
## How it works
```
systems/**/*.md ──► Loader ──► Index (ETS) ──► MCP tools ──► LLM
(parse) (search/lookup) (HTTP/stdio)
```
1. **Loader** walks the docs root (`systems/github/` by default), splits
optional YAML frontmatter from the body and
extracts the title, headings, raw markdown and a plain-text rendering.
2. **Index** is an ETS-backed `GenServer` providing fast lookup and ranked
search (title/heading/tag hits rank above body hits).
3. **Server** exposes the index through MCP tools.
## MCP tools
Generic:
| Tool | Input | Returns |
| --- | --- | --- |
| `list_categories` | – | categories with doc counts |
| `list_docs` | `category?`, `layer?`, `token?` | doc titles + slugs (filtered by category, layer, or the docs that declare a token) |
| `search` | `query`, `category?`, `layer?`, `limit?` | ranked matches with snippets |
| `get_doc` | `slug` or `title` | full markdown of one doc |
| `get_section` | `slug`, `heading` | markdown under a single heading |
| `get_related` | `slug` | the docs cross-linked from a doc's `related` frontmatter |
Design-system convenience wrappers (thin wrappers over the generic tools):
| Tool | Input | Returns |
| --- | --- | --- |
| `list_components` | – | component docs |
| `get_component` | `name` | full component doc |
| `list_layouts` | – | layout docs |
| `get_layout` | `name` | full layout doc |
| `get_tokens` | `group?` | tokens doc, or one token group's section |
| `get_token` | `name` | one token's value + role from the manifest, plus the docs that use it |
| `get_typography` | – | typography doc |
| `get_color_usage` | – | color-usage doc |
| `get_shape_language` | – | shape-language doc |
## Authoring docs
Drop markdown files anywhere under the docs root (`systems/github/`, nested
folders are fine). The
**slug** is the path relative to the docs root without `.md`, e.g.
`systems/github/components/button.md` → `components/button`.
Frontmatter is optional but recommended for richer queries. All fields are
optional; the loader falls back to path/content-based inference when they are
absent.
```markdown
---
title: Button # else first H1, else humanized filename
category: components # else the top-level folder (or `general` at the docs root)
layer: component # overview | usage | foundation | layout | component | pattern | meta
tags: [interactive, form]
status: stable
slug: components/button # pin a stable identity if the file may move
uses_tokens: [--button-primary-bgColor-rest, --fgColor-onEmphasis]
related: [design, foundations/color]
---
# Button
...
```
The structured fields drive the richer tools: `layer` powers the `layer` filter
on `list_docs`/`search`, `uses_tokens` powers the reverse token lookup
(`list_docs token:` and `get_token`'s "used by"), and `related` powers
`get_related`.
Because `uses_tokens` and `related` are mechanical projections of the body, they
are **generated** rather than hand-maintained: mention a token or sibling doc in
the prose (in backticks) and [`mix frontmatter`](#maintaining-the-docs) keeps the
lists complete, while [`mix consistency`](#maintaining-the-docs) keeps them
honest. See [Maintaining the docs](#maintaining-the-docs) below.
When frontmatter is absent the loader falls back to:
- **title** → first `# H1`, else the humanized filename
- **category** → the top-level folder (or `general` for files directly in the
docs root; configurable via `config :design_system_mcp, root_category:`)
- **slug** → the path relative to the docs root without `.md`
## Maintaining the docs
Three Mix tasks keep the docs internally consistent and in sync with the upstream
Primer packages. They are pure (no running server) and exit non-zero on failure,
so they gate CI. The full frontmatter contract is documented in
[`systems/github/meta/schema.md`](systems/github/meta/schema.md).
### `mix frontmatter` — keep `uses_tokens` / `related` complete
`uses_tokens` and `related` are derived from the backtick references in each
doc's body, so they never silently drift from the prose. Generation is
**additive**: it appends any reference the body mentions but the frontmatter
omits, while preserving hand-curated entries.
```bash
mix frontmatter # fix in place: append missing references
mix frontmatter --check # CI gate: fail if any doc is incomplete (writes nothing)
mix frontmatter --prune # destructive: rewrite lists to *exactly* the body refs
mix frontmatter PATH # scope writes to a file/dir (full doc set still loaded)
```
A doc opts out with `generate: false` — e.g. the standalone manifest `design.md`,
whose `uses_tokens` is a curated highlight rather than its exhaustive token table.
### `mix consistency` — validate the graph
```bash
mix consistency # validate related / uses_tokens / layer
mix consistency --strict # also fail on advisory warnings (orphans)
mix consistency path/to/docs # point at a different docs root
```
Loads every doc and flags (and exits non-zero on):
- a `related:` slug that doesn't resolve to an existing doc,
- a `uses_tokens:` entry absent from the token manifest (`design`), i.e. a token
`get_token` would fail to resolve, and
- a `layer:` value outside the known set (`overview`, `usage`, `foundation`,
`layout`, `component`, `pattern`, `meta`).
It also prints advisory **orphan** warnings (a doc with no `related` link in or
out); these don't fail the build unless `--strict` is given.
### `mix tokens.check` — verify token values against Primer
`design.md` is standalone and carries token values inline, so they can drift from
`@primer/primitives`. This compares each documented value against a vendored
light-mode snapshot and fails on any mismatch.
```bash
mix tokens.check
```
### Refreshing the upstream snapshots
Two vendored snapshots back the checks above. Refresh them (and bump the pinned
versions in [`meta/dependencies.md`](systems/github/meta/dependencies.md)) when
upgrading Primer:
| Script | Produces | Consumed by |
| --- | --- | --- |
| [`meta/fetch-primitives.sh`](systems/github/meta/fetch-primitives.sh) | `meta/primitives.tokens.json` — token values | `mix tokens.check` |
| [`meta/fetch-react.sh`](systems/github/meta/fetch-react.sh) | `meta/components.props.json` — component props | component `## API` sections |
```bash
systems/github/meta/fetch-primitives.sh [VERSION] # default: pinned version
systems/github/meta/fetch-react.sh [VERSION]
```
`fetch-react.sh` vendors a compact, props-only projection of `@primer/react`'s
`generated/components.json`; the component docs' `## API` sections are generated
from it so prop names, types, and defaults come from the package, not by hand.
## Running
Requires Elixir (see `.tool-versions` / your `mise` config) — this repo targets
Elixir 1.20 / OTP 29.
```bash
mix deps.get
mix test
# Run the server (Streamable HTTP on http://localhost:9821 by default, loopback only):
mix server
```
`mix server` is an alias for `mix run --no-halt`; it boots the app and keeps it
running until you interrupt it (Ctrl-C).
This serves the MCP Streamable HTTP transport via [Bandit](https://hex.pm/packages/bandit)
on a single endpoint that dispatches on the HTTP method:
| Method & path | Purpose |
| --- | --- |
| `GET /mcp` | opens the SSE stream for server-to-client messages |
| `POST /mcp` | receives the client's JSON-RPC messages |
| `DELETE /mcp` | closes the session |
Sessions are correlated with the `mcp-session-id` response/request header.
Quick smoke test:
```bash
# Initialize: the server returns a session id in the `mcp-session-id` header.
curl -i -X POST http://localhost:9821/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
# Reuse that id (replace <SID>) on subsequent calls, e.g. to list tools:
curl -X POST http://localhost:9821/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'mcp-session-id: <SID>' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
```
> **Note:** Streamable HTTP is the current MCP transport (spec 2025-03-26). The
> older HTTP+SSE transport (2024-11-05) is still available in `anubis_mcp` for
> legacy clients via `transport: {:sse, ...}` and a two-endpoint router, but is
> deprecated.
The docs location defaults to `systems/github` (see `config/config.exs`).
Override it without recompiling via the `DESIGN_SYSTEM_DOCS_PATH` environment
variable.
The transport, HTTP port, and the well-known slugs the convenience tools resolve
are all configurable in `config/config.exs`:
```elixir
config :design_system_mcp,
transport: :streamable_http, # or :stdio to speak over stdin/stdout
ip: {127, 0, 0, 1}, # bind address; {0,0,0,0} for all interfaces
port: 9821, # HTTP port for the Streamable HTTP transport
typography_slug: "foundations/typography", # get_typography
color_usage_slug: "foundations/color", # get_color_usage
shape_language_slug: "foundations/shape-language", # get_shape_language
token_manifest_slug: "design", # get_tokens / get_token
root_category: "general" # category for docs at the docs root
```
> Logs are routed to **stderr** (so they never corrupt a stdio JSON-RPC stream
> when `transport: :stdio` is used).
### Hot reload
The server watches the docs directory and reloads the index automatically when
files change, so authors see updates without a restart. Disable it with
`config :design_system_mcp, watch?: false`.
## Registering with an MCP client
Point your client (Claude Desktop, Copilot, Cursor, …) at the MCP URL of the
running server:
```json
{
"mcpServers": {
"design-system": {
"url": "http://localhost:9821/mcp"
}
}
}
```
Prefer the server to be launched by the client over **stdio** instead? Set
`config :design_system_mcp, transport: :stdio` and register a command:
```json
{
"mcpServers": {
"design-system": {
"command": "mix",
"args": ["server"],
"cwd": "/absolute/path/to/design_system_mcp",
"env": { "DESIGN_SYSTEM_DOCS_PATH": "/absolute/path/to/docs" }
}
}
}
```
## Project layout
```
lib/design_system_mcp/
doc.ex # the %Doc{} struct
markdown.ex # heading / section / plain-text helpers
loader.ex # systems/ -> [%Doc{}]
index.ex # ETS-backed GenServer (lookup + ranked search)
tokens.ex # precise single-token lookup over the manifest
watcher.ex # hot-reloads the index on docs changes
server.ex # Anubis MCP server, registers the tools
router.ex # Plug router mounting the Streamable HTTP endpoint
consistency.ex # static related / uses_tokens / layer validation + orphan warnings
frontmatter.ex # additive generator for uses_tokens / related (mix frontmatter)
token_audit.ex # compares design.md token values to the primitives snapshot
tools/ # one module per MCP tool
lib/mix/tasks/
consistency.ex # `mix consistency` (wraps DesignSystemMCP.Consistency)
frontmatter.ex # `mix frontmatter` (wraps DesignSystemMCP.Frontmatter)
tokens_check.ex # `mix tokens.check` (wraps DesignSystemMCP.TokenAudit)
systems/github/ # sample design-system docs (served by default)
meta/ # frontmatter contract (schema.md), fetch-*.sh scripts, vendored snapshots
patterns/ # task-shaped recipes (layout + components), incl. _template.md
test/ # unit tests + fixtures under test/fixtures/docs/
```
## License
`anubis_mcp` is licensed under **LGPL-v3**; confirm that is acceptable for your
use before distributing.
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available