open-design-system-mcp-starter
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@open-design-system-mcp-starterWhat props does the Button component accept?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
open-design-system-mcp-starter
A system-agnostic MCP server template for a design system. Point it at your own catalog, tokens and docs, and coding agents get a real API to ask instead of a memory to guess from: what components exist, what props they take, which token to write, whether a name they are about to use is actually real. A lookup miss never returns a fabricated answer; it returns "not in this system, nearest is X, because Y".
Quick start
Clone the repo.
Install dependencies:
npm installThe repo ships a fictional example system,
acme-elements, underexamples/acme-elements, so the server runs immediately with no configuration:npm run serveTry it. Register the example server in Claude Code:
claude mcp add acme-elements -- npx tsx /absolute/path/to/src/cli.ts serveThen ask the agent "does this system have a Card component?" and expect a correction pointing at the nearest real element instead of an agent that invents one.
Point it at your own system next. When it is a checkout, run
init --id <id> --root <path>; when it is only ever installed via npm, runinit --id <id> --package <npm name>instead (see "Point it at your system" and "Systems consumed from npm" below).
Related MCP server: WordPress Design System MCP Server
Point it at your system
The recommended way to add a system is the init wizard. It scans a checkout, detects a catalog
adapter, a tokens adapter and doc globs, and writes (or merges into) ds.config.json for you:
npx tsx src/cli.ts initAnswer its prompts, or skip them entirely with --id and --root:
npx tsx src/cli.ts init --id acme-elements --root ../acme-elementsBoth forms accept --config <path> to target a config file other than ./ds.config.json. When
only one of --id / --root is given, init still asks interactively, using the one you passed
as that prompt's default.
When your system is only ever npm installed, never checked out, use package mode instead of
--root: init --id <id> --package <npm name> [--foundations <npm name>] reads the installed
package's own node_modules/<npm name> (barrel, compiled CSS, README) instead of scanning a
source tree. See "Systems consumed from npm" below for the full walkthrough and what it detects.
To edit the config by hand instead, or to see every field init can set, the file that ships in
the repo looks like this:
{
"$schema": "./schema/ds.config.schema.json",
"systems": {
"acme-elements": {
"name": "Acme Elements (example)",
"description": "Placeholder system shipped with the starter so the server runs out of the box. Replace with your own.",
"root": "examples/acme-elements",
"componentModel": "custom-elements",
"componentsPkg": "@acme/elements",
"catalog": { "adapter": "custom-elements-manifest", "path": "custom-elements.json" },
"tokens": { "adapter": "css-vars", "files": ["tokens.css"] },
"docs": { "include": ["docs/**/*.md"] }
}
}
}Every field of a system entry, from SystemConfig in src/types.ts:
Field | Required | Meaning |
| No | Human name shown to agents, e.g. "Acme Elements". Defaults to the id. |
| No | Short description shown alongside the name. |
| No | Checkout root the adapters read from. Relative paths resolve against the config file's directory. |
| No | Env var that overrides |
| No | Where generated ground truth lives. Default: |
| No |
|
| No | The specifier consumers import (react) or register (custom-elements), e.g. |
| No | Tokens/foundations package, if separate from |
| Yes | Where the catalog comes from. See the adapters table below. |
| No | Where tokens come from. See the adapters table below. |
| No |
|
| No | Team-maintained alias file, relative to the config dir. Default: |
| No |
|
Once the config points at your system, run:
npx tsx src/cli.ts extract --system <id>
npx tsx src/cli.ts doctor
npx tsx src/cli.ts serveextract reads your source through the configured adapters and writes the ground-truth files
into the system's data dir (data/<id>/catalog.json, tokens.json, docs-index.json, plus
aliases.json when you maintain one). That data dir is committed on purpose: it is the
server's ground truth, and a diff to it should be reviewed like a diff to code, not treated as
build output. Pass --out <dir> to extract to write into a different directory instead, for a
dry run or a CI check.
Every command accepts --config <path> (default ./ds.config.json); extract and doctor
accept --system <id> to limit the run to one system.
Each extracted file is stamped with a hash of the source it came from. doctor compares that
stamp against the current source and reports whether the catalog, tokens, docs and (when
codeConnect is configured) the Code Connect mappings are each fresh. serve runs the same check
on startup: it warns when any of them is older than the source, and refuses to start with
--strict until you re-run extract.
Adapters
Adapters are the only place format knowledge lives. Everything downstream (search, tools,
resources) reads the same SystemCatalog / SystemTokens shape regardless of which adapter
produced it.
Adapter | Kind | Input |
| catalog | An already-built catalog matching the |
| catalog | A |
| catalog | A source tree ( |
| tokens | CSS files declaring custom properties, e.g. a compiled |
| tokens | Design Tokens Community Group (DTCG) JSON files ( |
react-docgen
{ "adapter": "react-docgen", "src": "<dir relative to root>", "barrel": "<optional, relative to root>" }The public API is whatever the barrel exports, not whatever docgen manages to document: named
re-exports (export { A, B as C } from './x', including export { default as X }), export * from './x' followed recursively through nested barrels up to 6 hops, and local export const/function/class. A barrel written against the compiled output (export * from './core/index.js') resolves fine: .js/.jsx/.mjs/.cjs specifiers are matched back to their
.ts/.tsx source before being read. barrel defaults to <src>/index.ts or index.tsx, or
index.d.ts when neither exists, since a published package installed into node_modules ships
only declaration files.
Props come from react-docgen-typescript, run once over every .tsx file under src, plus every
.d.ts file so a published package's own declaration files are documented too. Literal union
types are expanded into their member values rather than left as an alias name. A prop declared
outside the system's own source, such as an inherited DOM attribute or a spread from a
third-party base component, is recorded by name only, under inheritedProps, not given a full
entry. A tsconfig compiler option this project's pinned TypeScript does not recognize (written for
a newer TypeScript than the adapter runs on) is warned about and skipped rather than treated as a
hard failure. When a barrel re-exports components (always true for a published package's
dist/index.d.ts), the same component would otherwise be documented twice, once from its own
file and once from the barrel; the adapter keeps one entry per name, and the component's own file
wins over the barrel's re-export.
Props-type fallback. react-docgen-typescript's component detection is name- and shape-based,
so it misses some perfectly ordinary components: a callable object
(declare const Stack: { (props): JSX.Element }), or a props type that is an intersection with a
union. For every barrel export docgen left with no props at all, the adapter reads the export's
own <Name>Props (or <Name>BaseProps) type straight off a TypeScript checker built over the
exports' declaring files, or, when there is no such type, the first parameter type of the export's
call signature. An export documented this way carries docSource: 'props-type' instead of the
default (docgen). A prop resolved this way whose declaration lives outside the system's own source
is still recorded by name only, under inheritedProps, exactly like a docgen-documented export -
an inherited DOM prop is name-only either way. extract prints a one-line summary of how many
exports this closed the gap for, e.g.
[extract] props-type fallback documented 12 of 34 undocumented exports. What is left after that
line is what "Known limits" below, and the catalog overlay, are for.
css-vars
{ "adapter": "css-vars", "files": ["<file relative to root>", "..."] }Parses --name: value; custom-property declarations out of plain CSS. :root/html/body
selectors are the default ("light") theme; [data-theme="x"], .x-theme and .dark/.light
classes, and @media (prefers-color-scheme: dark) are recognized as other themes. Tailwind 4
@theme blocks are read as default-theme declarations, and @layer wrappers (@layer utilities { :root { ... } }) are unwrapped and their contents processed normally. A brace-less at-rule ahead
of a block, such as @import "./x.css"; or Tailwind's @custom-variant dark (...);, is ignored
rather than folded into the next selector's header (which would otherwise mislabel that block's
theme).
dtcg
{ "adapter": "dtcg", "files": ["tokens.json"] }Parses Design Tokens Community Group JSON: $type is inherited from the nearest ancestor group
when a token doesn't declare its own, {value} references are captured, and when multiple files
are given each is matched to a theme (light/dark, or high-contrast) by its basename.
markdown-docs
docs.include (and optional docs.exclude) globs, relative to root, are chunked by heading
into data/<id>/docs-index.json: one chunk per heading section, truncated to 1200 characters,
each recording which catalog exports it mentions. This index is what get_guidance searches, and
it is checked for freshness the same way the catalog and tokens are.
A glob walk skips node_modules and dot-directories, so it never reaches a README shipped inside
an installed package. An entry in docs.include with no glob characters (*, ?, {, [) is
taken as a literal path instead and read directly, bypassing that skip -- this is how a system
consumed from npm points at a package's own README.md under node_modules.
Known limits
Neither docgen nor the props-type fallback above can document every component: some polymorphic
or factory-built component (one assembled at runtime rather than declared as a plain function, for
example) resolves to no props type and no callable signature the checker can read. Such a
component still appears in allExports and still resolves through resolve_component as a real,
existing symbol; it just has no props table. overlay-scaffold lists exactly which exports are
still in this state and gives a team a starting point to fill them in by hand; see "Catalog
overlay" below.
Catalog overlay
data/<id>/overlay.json holds hand-written facts merged over the extracted catalog at load time:
for exports neither docgen nor the props-type fallback could document, or simply to add an
example, a11y note, deprecation, or Figma link the source has no place to author. An overlay entry
can only enrich a symbol that already exists in the catalog's allExports -- it never invents one,
so a typo reports as unknown instead of silently creating a fake export.
{
"exports": [
{
"displayName": "AcmeStack",
"description": "Vertical layout primitive with a configurable gap.",
"props": [{ "name": "gap", "type": "string", "required": false, "defaultValue": "\"md\"" }],
"examples": [{ "title": "Basic stack", "code": "<AcmeStack gap=\"lg\">...</AcmeStack>", "language": "tsx" }],
"a11y": { "accessibleName": "none" },
"guidance": { "do": ["Use for vertical rhythm between blocks."], "dont": ["Don't use for inline layout."] },
"deprecated": { "since": "2.0", "replacement": "AcmeStackV2" },
"figma": { "nodeId": "1:23", "url": "https://figma.com/file/...?node-id=1-23" }
}
]
}Every field is optional except displayName. Precedence: an overlay props entry is merged into
the extracted props list by name -- an overlay prop with the same name replaces the extracted one,
any other extracted prop is kept, and a name the extractor never found is added. Every other field
(description, a11y, guidance, deprecated, figma, tagName, inheritedProps, events,
slots) is a plain overwrite when the overlay sets it; examples and docs are appended to,
de-duplicated by content. Setting props also stamps docSource: 'overlay' on that export, same
as when the overlay creates a whole new entry for a name docgen never produced one for.
unknown names -- a displayName the overlay lists that is not in allExports -- are never
invented into existence. They are reported by doctor (overlay: N exports enriched, unknown names: ..., marked a failing check) so a typo or a renamed component gets caught instead of
silently doing nothing.
The scaffold workflow:
npx tsx src/cli.ts overlay-scaffold --system acme-elementswrites data/<id>/overlay.json with one empty entry per undocumented export (the same set
"Known limits" describes), each carrying a _note telling a human what to do: read the
declaration file, fill in props (name, type, required, defaultValue, description), then delete
the note. Re-running without --force only appends entries for names not already listed, so a
team's edits to existing entries are never touched; --force rewrites the file from the current
undocumented set.
Systems consumed from npm
No checkout is required. When your design system is only ever installed, not cloned, run the
init wizard's package mode first:
npx tsx src/cli.ts init --id acme-elements --package @acme/elementspointed at the app folder that has @acme/elements installed (default: the current directory).
It reads node_modules/@acme/elements/package.json for the .d.ts barrel (types, typings, or
exports['.'].types), finds compiled CSS from main/style/exports or by walking the package
for a token-shaped stylesheet, picks up the package's own README.md, and writes the
react-docgen + css-vars config below for you. Pass --foundations @acme/foundations when
tokens ship from a separate package the components package does not declare as a peer dependency
(when it does, the wizard finds it on its own). If @types/react is not installed in the app, the
wizard prints a reminder: react-docgen-typescript needs it to resolve the package's declaration
files.
Point the config at the app that has the packages installed instead of at the package's own repo, by hand, when the wizard's detection does not fit your package's layout, or to hand-tune what it wrote:
{
"systems": {
"acme-elements": {
"root": "../my-app",
"componentsPkg": "@acme/elements",
"catalog": { "adapter": "react-docgen", "src": "node_modules/@acme/elements/dist", "barrel": "node_modules/@acme/elements/dist/index.d.ts" },
"tokens": { "adapter": "css-vars", "files": ["node_modules/@acme/foundations/dist/tokens.css"] },
"docs": { "include": ["node_modules/@acme/elements/README.md"] }
}
}
}rootis the app folder that has the packages installed (itsnode_modules), not the design system's own checkout.catalog.srcandcatalog.barrelpoint intonode_modules/<componentsPkg>/distand itsdist/index.d.ts; thereact-docgenadapter falls back toindex.d.tsfor the barrel and reads.d.tsfiles alongside.tsxones for exactly this case (see react-docgen above).tokens.filespoints at the foundations package's compiled CSS, e.g.node_modules/<foundationsPkg>/dist/tokens.css, when tokens ship from a separate package than the components.docs.includeneeds a literal file path, not a glob, to reach a README insidenode_modules: globs skipnode_modulesby design, but a literal path is read directly regardless (see markdown-docs above).Install
@types/reactin the app so the components' declaration files resolve; without it,react-docgen-typescriptcannot type-check the.d.tsfiles and documents nothing.When the package's barrel re-exports its components, as
dist/index.d.tsnormally does, the component's own declaration file wins over the barrel's re-export for the recorded props.
Aliases
Agents write the vocabulary they already know, not necessarily your system's vocabulary. Aliases are merged at load time from three layers, later layers winning on the same alias: team, then Code Connect, then the ecosystem lexicon.
Ecosystem lexicon (
src/data/convention-lexicon.json): names AI models tend to invent when a design system does not have them, mined from 898 graded agent generations across two production design systems. Never hand-edited; extended only with new mined evidence.Code Connect aliases (
data/<id>/aliases.code-connect.json): written byextractwhencodeConnectis configured, from Figma enum property maps whose value differs from the code value (e.g. Figma's "Primary" versus the code'svariant="primary"). See "Figma Code Connect" below. Not hand-maintained; regenerated on everyextract.Team file (
data/<id>/aliases.json): your own mapping from an alias to your system's real name. This is the file you maintain, and it wins over both of the above.
An alias without a team target still helps: the server can say "there is no Card here, the nearest concept is Surface" even before your team has mapped anything. A team entry that names a target turns that into a direct resolution.
Example data/acme-elements/aliases.json:
{
"components": [
{
"alias": "Card",
"concept": "surface-card",
"target": "acme-surface",
"source": "team"
}
],
"props": [
{
"alias": "spacing",
"concept": "stack-gap",
"target": "gap",
"source": "team"
}
]
}Figma Code Connect
When your team already maintains Figma Code Connect
files (figma.connect(Component, url, { props, example }) calls, usually in *.figma.tsx),
point codeConnect at them and extract folds the mapping into the catalog and aliases:
{
"systems": {
"acme-elements": {
"root": "../acme-elements",
"catalog": { "adapter": "react-docgen", "src": "src" },
"codeConnect": { "include": ["**/*.figma.tsx"] }
}
}
}include (default **/*.figma.tsx, **/*.figma.ts) is a glob list, or a literal path,
relative to root; set codeConnect.root instead when the mapping files live in a different
checkout than the system itself (a separate storybook repo, say).
For every figma.connect(...) call that names a real catalog export, extract adds:
A Figma node link --
figma: { nodeId, url, source }-- on the matching export, resolved from the connect call's URL. When docgen produced noCatalogExportfor that name at all, a bare stub is created so the link still surfaces, the same way the catalog overlay does for an undocumented export.A Code Connect example -- the connect call's
examplecallback, with its JSX pulled out andprops.<name>occurrences rewritten to{<name>}placeholders -- appended to the export'sexamples.Component-value aliases -- for every
props: { x: figma.enum('<FigmaValue>', { a: 'valueA' }) }map where a Figma-side value differs from the code-side value it maps to, an alias entry (source: 'code-connect') is written todata/<id>/aliases.code-connect.json. See "Aliases" above for how this merges: team beats Code Connect beats the lexicon on the same alias.
doctor reports Code Connect coverage when it is configured: code connect: N of M documented exports mapped (F files). Mappings are stamped and checked for freshness exactly like catalog,
tokens and docs: a hash over the sorted list of matched files (paths and contents) is compared on
every doctor and serve run, and serve --strict refuses to start when it is stale.
This does not replace Figma's own MCP server. Figma's MCP answers "what component is this node, with its current, live property values" straight from an open Figma file. This adapter answers a different question, offline: it reads the mapping files your team already wrote and maintains, and uses them to enrich this server's own catalog and alias data -- node links, a worked example, and vocabulary aliases -- without ever opening a Figma session itself.
Patterns
Nothing here is required to run the server, but a component-by-component API is not always
enough: an agent asking "how do I compose a labeled field with an error" needs a recipe, not a
props table. Patterns fill that gap, and get_pattern serves them.
Add markdown files to data/<id>/patterns/*.md. Each file is one pattern, with optional YAML-ish
front matter between --- lines:
---
title: Labeled field with error
description: A single-line text field with its own label, ready to show a validation error.
tags: form, validation, error
language: html
---
# Labeled field with error
... prose ...
```html
<acme-text-field label="Email" invalid error-text="Enter a valid email address."></acme-text-field>
```Front matter wins when present; anything missing falls back to the body: the first # heading
becomes the title, the first paragraph becomes the description, and the first fenced code block
becomes the code (its info string is the language, defaulting to tsx for a react system or
html for custom-elements when neither front matter nor the fence say). Which catalog
components a pattern uses is detected automatically from tags in the code, not authored by hand.
get_pattern searches these by plain-language query ("confirm dialog", "empty state") and
returns the matched pattern's full prose and code. A system with no patterns/ directory yet
gets a plain note back instead of an error, pointing at search_components in the meantime.
The example system ships three: confirm-actions.md, labeled-field.md and status-notice.md
under data/acme-elements/patterns/.
Migration lookups
get_migration answers "is this deprecated, and what do I do instead" for a component or a
specific prop (Button or Button.type), from three sources, in order:
The catalog's own
deprecatedfield on the export or the prop, extracted mechanically by the catalog adapters -- this is the authoritative answer when present.CHANGELOG*.mdat the system's root (CHANGELOG.mdpreferred over any otherCHANGELOG.*): lines that word-match the component or prop name, with the nearest preceding##/###heading (usually the version) attached.A
codemods/orscripts/codemods/directory at the root: file names that mention the component are listed as available codemods; when a codemods directory exists but nothing matches, that is reported too, so an agent doesn't assume none exists.
Tools
Every tool returns a short text summary for the model plus a structuredContent object matching
a type in src/types.ts. Nine tools ship:
Tool | Question it answers |
| "What do I use for a dismissible notice?" |
| "What are Button's real props and values?" |
| "Does TextField exist here?" |
| "Which token is |
| Lists tokens, filtered by category (color, space, size, typography, radius, shadow, motion, border, opacity, z-index) or a word; for browsing what's available rather than guessing a name. |
| Gets written guidance from the system's own docs for a component or a question (e.g. "when not to use a modal", "how do I migrate off the deprecated variant prop"); returns the most relevant doc sections, ranked, or a plain note when no docs are indexed for the system. |
| Checks a JSX/TSX or HTML snippet against the design system before it's committed: unknown components, invented props, raw colors and lengths, missing accessible names, disallowed imports, deprecated API. Meant to be called on every file a build task writes. |
| "How does this system compose a labeled field with an error?" Gets a complete, correct recipe composed from real components for a common UI need, in preference to composing from scratch. |
| "Button's |
No get_figma_mapping tool is planned: "which code component is this Figma node" is exactly what
Figma's own MCP server already answers, with live property values from an open file. This starter's
job is the catalog and validation side, which is why the Code Connect adapter feeds node links and
aliases into the tools above instead of adding a tenth tool that would duplicate Figma's own.
Resources and prompts
Resources serve clients that attach context up front; tools serve agents mid-task. Both read the same data:
ds://<system>/catalogds://<system>/tokensds://<system>/components/<name>ds://<system>/llms.txt
The build-ui prompt starts a UI task with the system's conventions and the tool routine already
loaded, so an agent reaches for the right tool before it writes anything.
Registering the server
All registrations run the same command, with an absolute path to this repo's src/cli.ts and
config:
npx tsx <abs>/src/cli.ts serve --config <abs>/ds.config.jsonThis is the stdio transport: one process per client, spawned by the client itself. For a team that wants one server everyone points at instead, see "Shared HTTP instance" below.
The repository ships a project-scoped .mcp.json that runs npx tsx src/cli.ts serve with a
relative path, so opening a clone in Claude Code registers the example system with no setup. Edit
the server name once you point the config at your own system, or delete the file if you register
the server globally instead.
Claude Code
claude mcp add acme-elements -- npx tsx <abs>/src/cli.ts serve --config <abs>/ds.config.jsonOr in .mcp.json:
{
"mcpServers": {
"acme-elements": {
"command": "npx",
"args": ["tsx", "<abs>/src/cli.ts", "serve", "--config", "<abs>/ds.config.json"]
}
}
}Cursor
.cursor/mcp.json:
{
"mcpServers": {
"acme-elements": {
"command": "npx",
"args": ["tsx", "<abs>/src/cli.ts", "serve", "--config", "<abs>/ds.config.json"]
}
}
}Codex
~/.codex/config.toml:
[mcp_servers.acme-elements]
command = "npx"
args = ["tsx", "<abs>/src/cli.ts", "serve", "--config", "<abs>/ds.config.json"]Shared HTTP instance
For a team, run one server everyone points at instead of every client spawning its own stdio
process: serve --http starts the same server over streamable HTTP.
npx tsx <abs>/src/cli.ts serve --config <abs>/ds.config.json --http --port 3333 --host 127.0.0.1 --path /mcp--portdefaults to 3333,--pathto/mcp.--hostdefaults to127.0.0.1(loopback only). Set it to0.0.0.0to expose the server beyond localhost, and setDS_MCP_TOKENin the environment whenever you do -- the server prints a startup warning if it is bound to a non-loopback host with no token configured.DS_MCP_TOKEN, when set, is required as a bearer token on every request to--path(Authorization: Bearer <token>); with no token set, the endpoint is unauthenticated, same as the stdio transport.GET /healthz(no auth) returns each configured system's id, name, export and token counts, and catalog/tokens/docs freshness, plus the number of live sessions and the session limits -- point a load balancer's health check or a quick "is this instance current" glance at it.Sessions expire: a client that stops talking without a
DELETEis evicted after 30 minutes idle (DS_MCP_SESSION_TTL_MS), and at most 200 sessions are held at once (DS_MCP_MAX_SESSIONS); when the cap is reached the least recently seen session is evicted first. Both are logged.Security: the token check is constant-time, but there is no rate limiting and no TLS. Put a reverse proxy with TLS in front of an exposed instance, keep
--host 127.0.0.1for a single machine, and treat the catalog as what it is: a description of your public component API, not a secret.
Registering a shared instance in Claude Code is a URL, not a spawned command (fictional host and id, use your own):
claude mcp add --transport http acme-elements http://ds-mcp.internal:3333/mcpgenerate well-known --server-url <url> advertises the same URL alongside the stdio start command
in the generated .well-known/mcp/servers.json, so a client that reads that file can reach either
transport:
npx tsx src/cli.ts generate well-known --system acme-elements --target . --server-url http://ds-mcp.internal:3333/mcpGenerating the agent surface
extract produces the ground truth the server reads. generate writes the agent-facing
surface derived from that data into an adopting team's own repo (their design system repo, or an
app that consumes it) -- so an agent that hasn't attached this MCP server yet, or a plain-text
client that can't call tools, still gets a grounded starting point.
npx tsx src/cli.ts generate <agents-md|llms-txt|skill|editor-rules|well-known|all> --system <id> --target <dir> [--force] [--server-url <url>]--target is the repo to write into, defaulting to the current directory. --system picks the
system when more than one is configured. --server-url only matters for well-known: it
advertises a hosted streamable-http endpoint (a "Shared HTTP instance", see above) alongside the
stdio command; omit it when the only registration you support is stdio.
Generated surface
Target | Writes | Rule when the path already exists |
| A section in | Fenced marker: only the marked section is replaced. |
|
| Fully generated: skipped unless |
|
| Fully generated: skipped unless |
|
|
|
|
| Fully generated: skipped unless |
| Every target above. | As above, per file. |
Every generated section carries the same content: the system's name and consumption rule (import
by name, or register-once-then-tag), the MCP server start command, the fixed 8-step tool routine
(search_components -> resolve_component -> get_component -> find_token -> check_usage
-> get_pattern -> get_guidance -> get_migration), the never-invent rule, any team aliases
that resolve to a real target, and a few do/don't lines derived from the live catalog (a
deprecated component or prop, a component whose accessible name is required, always calling
find_token before a raw value) -- never a generic placeholder.
Two write rules, so generate is always safe to re-run as the catalog changes:
Begin/end markers.
AGENTS.md,CLAUDE.mdandcopilot-instructions.mdmix hand-written and generated text. A re-run replaces only the text between<!-- ds-mcp:begin <id> -->and<!-- ds-mcp:end <id> -->for that system id; everything else in the file, including the team's own prose above and below, is untouched.AGENTS.mdandcopilot-instructions.mdare created if missing;CLAUDE.mdis only ever updated, never created, since not every team has one.Skip unless
--force. The fully-generated files (llms.txt,llms-full.txt, bothSKILL.mdfiles,<id>.mdc,servers.json) carry an embedded signature marking them as written by this tool. A file already at that path is left alone -- and reported asskipped-- unless it carries that signature (a previous run of this tool) or--forceis passed, so a team's own hand-written file at the same path is never silently clobbered.
generate llms-txt (and all) always reports the size of both files, e.g.
llms.txt 1.4 KB, llms-full.txt 12.7 KB, so the context cost of attaching them is visible before
anyone does.
Skills
The repo ships one skill for onboarding the starter itself: .claude/skills/ds-mcp-setup/. It
walks a setup session through adding a system (init), extracting, running doctor,
smoke-testing the server, generating the agent surface (generate all), registering the server
with a client, and authoring aliases.json from what an agent got wrong on its first real
session -- each step names a concrete check rather than "looks fine."
generate skill writes two more, generated per system rather than shipped: use-<id> (the build
routine with concrete tool calls, this system's own aliases and do/don'ts) and review-<id> (how
to review a diff against the system -- run check_usage per changed file, treat an
unknown-component or invented-prop finding as blocking, and name the deprecated replacement
instead of letting it pass silently). See "Generating the agent surface" above for where these
land and when a re-run touches them.
Multi-kit systems
A design system with more than one consumable kit, for example a React kit and a web-component
kit, declares one system per kit in ds.config.json rather than one system covering both. Every
tool takes an optional system argument: omit it when only one system is configured, pass it to
disambiguate when several are.
Development
npm run check # typecheck + the offline test suite
npx tsx src/cli.ts extract && npx tsx src/cli.ts doctor # the shipped example must stay idempotent and greenNode 22 or newer (.nvmrc pins 22). The GitHub Actions workflow in .github/workflows/ci.yml
runs the same checks plus a git diff --exit-code -- data guard, so a change that alters the
committed example data without meaning to fails CI. See CONTRIBUTING.md before opening a pull
request.
Conventions: ESM TypeScript, strict, explicit .ts import extensions, no build step. Tests are
offline (node:test via tsx, the MCP SDK's in-memory transport, synthetic data in temp dirs) and
never call a live model or network endpoint. Nothing under src/ names a real design system,
package or component; every name resolves from ds.config.json.
typescript is pinned to ^5 on purpose: TypeScript 7, the native rewrite, ships no classic
Compiler API, so neither react-docgen-typescript nor this repo's own barrel parser can load
under it. Keep typescript@^5 as the installed package, or if you need TypeScript 7 elsewhere in
your workflow, install typescript@5 as a nested dependency of react-docgen-typescript instead
of upgrading the top-level package.
Later
Pairing with a benchmark. The catalog and token contracts here are a superset of what
open-design-system-bench extracts, so its output loads through the catalog-json adapter with no
conversion. The starter does not depend on this; see docs/bench-pairing.md for the full note.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Serves your design system and coding standards to coding agents, so they stop guessing.
Live React design-system APIs, patterns, and code validation so AI agents build real UI, not slop.
Access and maintain design system docs, tokens, components, skills, and contexts across any project.
Versioned documentation registry and semantic search for AI tools and coding assistants.
Related MCP Servers
- AlicenseAqualityCmaintenanceProvides AI coding assistants with on-demand access to component specs, test scenarios, accessibility requirements, and build guides from the Web UI Component Specification.10MIT
- FlicenseNot gradedqualityFmaintenanceProvides AI assistants with access to WordPress Design System component information and design guidance.9-
- AlicenseAqualityBmaintenanceProvides deterministic, read-only design knowledge for AI coding agents to help them choose visual directions, plan UI states, and compose design tokens, all without network access.6164MIT
- AlicenseNot gradedqualityCmaintenanceProvides AI agents with component documentation, usage examples, and design tokens from the WordPress Design System, enabling them to follow the latest design system guidance for creating user interfaces.470GPL 2.0
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/christophhdesign/open-design-system-mcp-starter'
If you have feedback or need assistance with the MCP directory API, please join our Discord server