Neumo MCP Server — Setup Guide
How to install, register, and use @kofile/neumo-mcp-server — the Model
Context Protocol (MCP) server that exposes the Neumo / GDS design system
(components, tokens, usage examples, and audit tools) to AI coding assistants
like Claude Code, Cursor, and VS Code.
In practice it acts as a guardrail: when an assistant writes UI code, it can ask the server for the real component API, the real token for a color, or whether a stylesheet drifts from the design system — instead of guessing.
No hosting, no API key, no LLM calls. The server runs locally over stdio and only reads bundled design-system data. Any tool use bills to your AI session, never the publisher's.
Prerequisites
- Node.js ≥ 18 (tested on 20.x)
- An MCP-capable client — Claude Code, Cursor, or Claude Desktop
- Access to the
@kofilenpm scope (the package resolves fromhttps://registry.npmjs.org)
0. Authenticate to npm (private package)
@kofile/neumo-mcp-server is private, so both npm install and npx need
an npm access token first — otherwise the server fails to launch with a 404/401.
Create a Read-only token at npmjs.com (avatar → Access Tokens, with
@kofile org membership), then:
npm config set //registry.npmjs.org/:_authToken=PASTE_TOKEN_HERE
Verify with npm view @kofile/neumo-mcp-server version. Full walkthrough and
troubleshooting: the npm access token guide.
1. Install
There are two supported ways to run the server. Pick one.
Option A — Install into your project (recommended)
Add it as a dev dependency so the version is pinned in package.json and the
setup works offline:
npm install --save-dev @kofile/neumo-mcp-server
This installs the prebuilt server at
node_modules/@kofile/neumo-mcp-server/dist/index.js and a neumo-mcp-server
bin in node_modules/.bin/.
Option B — Run on demand with npx (no install)
Skip installing and let your MCP client fetch it when launched. Useful for a quick try or for repos that don't want the dependency. See the config in the next section.
Which to choose? Option A is deterministic (pinned version, no network at launch) and is the right default for a team repo. Option B is zero-footprint but re-resolves the package and needs network access.
2. Register with your AI client
MCP clients read a JSON config that tells them how to launch each server. The file location depends on the client:
| Client | Config location |
|---|---|
| Claude Code | .mcp.json in your project root |
| Cursor | .cursor/mcp.json (or Settings → MCP) |
| Claude Desktop | claude_desktop_config.json |
If you installed it (Option A)
Point the client at the local build. Create .mcp.json in your project root:
{
"mcpServers": {
"neumo-ds": {
"command": "node",
"args": ["./node_modules/@kofile/neumo-mcp-server/dist/index.js"]
}
}
}
If you prefer npx (Option B)
{
"mcpServers": {
"neumo-ds": {
"command": "npx",
"args": ["-y", "@kofile/neumo-mcp-server@latest"]
}
}
}
Pin a version for reproducibility (e.g.
@kofile/neumo-mcp-server@0.1.0). The bundled design-system data is a snapshot from publish time, so the server's answers track the version you install.
.mcp.json is safe to commit — teammates get the server automatically (after a
one-time trust prompt in Claude Code, since project-scoped servers require
approval).
3. Verify the connection
- Reload / restart your AI client so it picks up the new config.
- In Claude Code, run
/mcp— you should seeneumo-dslisted and connected. - Ask it something like "list the Neumo components" — you should get a list back.
Quick command-line smoke test (confirms the server boots over stdio):
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' \
| node ./node_modules/@kofile/neumo-mcp-server/dist/index.js
Expected: a line @kofile/neumo-mcp-server running on stdio followed by a JSON
result advertising resources and tools capabilities.
Testing the tools
Three ways to exercise the tools, from everyday use to CI.
A. Interactively, through your AI client
The way you'll use it day to day. Confirm neumo-ds is connected (/mcp in
Claude Code), then prompt something that triggers each tool — see the Prompt
line under every tool in the Tool catalog. For example:
"List the Neumo components." "Show the API for neumo-button." "Check contrast of #562c9e on #f7f7f7."
B. Raw JSON-RPC by hand (debugging one tool)
To poke a single tool at the protocol level, pipe line-delimited JSON-RPC into
the binary. Send initialize, the notifications/initialized notification,
then a tools/call (or tools/list to inspect schemas):
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"check-contrast","arguments":{"foreground":"#000","background":"#fff"}}}' \
| node ./node_modules/@kofile/neumo-mcp-server/dist/index.js
The response with "id":2 contains the tool result under
result.content[0].text.
C. Automated smoke test (optional, for CI)
A small script that performs the handshake and calls every tool with sample
arguments makes a good CI gate — run it whenever you bump the
@kofile/neumo-mcp-server version to catch a tool that changed shape or broke.
The repo doesn't ship one yet; the JSON-RPC pattern in B is the building
block.
Resources
Read-only blobs the client can pull on demand:
| URI | Description |
|---|---|
neumo://components/manifest |
Full Custom Elements Manifest (every component's props, events, slots) |
neumo://tokens/global |
Global / primitive design tokens |
neumo://tokens/light |
Light-mode tokens |
neumo://tokens/dark |
Dark-mode tokens |
Tool catalog
Fourteen tools, grouped by purpose. Each entry lists its inputs, what it returns, a natural-language prompt that triggers it in an AI client, and an example response.
Quick reference
| Tool | Purpose | Required inputs |
|---|---|---|
list-components |
List all public components | (none) |
search-components |
Fuzzy-search components | query |
get-component |
Full API for one component | tagName |
get-usage-example |
HTML snippet for a component | tagName |
get-tokens |
Search/filter design tokens | (none — all optional) |
suggest-token |
Nearest token for a hex color | hexValue |
remap-variables |
Map CSS custom props → tokens | css |
audit-spacing |
Audit CSS spacing vs scale | css |
audit-typography |
Audit CSS typography vs scale | css |
audit-tokens |
Audit CSS colors vs token catalog | css |
check-contrast |
WCAG ratio for two hex colors | foreground, background |
audit-a11y |
Scan HTML/JSX for WCAG 2.2 violations | markup |
score-alignment |
0–100 alignment score from audit counts | (none — all optional) |
find-component-replacements |
Find off-system elements to swap for Neumo | files or code |
Components
list-components
List every public Neumo web component with its tag name and description.
- Inputs: none
- Returns: array of
{ tagName, description } - Prompt: "List the Neumo components."
[
{ "tagName": "neumo-accordion-content", "description": "Expandable content panel for an accordion item. Controlled by neumo-accordion-item — do not use standalone." },
{ "tagName": "neumo-button", "description": "`<neumo-button>` — design system button. …" }
]
search-components
Fuzzy-search components by any term — matches tag name, description, slot names, event names, and attribute names. Returns a ranked list with match details.
- Inputs:
query(string, required) — e.g."password","change event","tooltip" - Returns: array of
{ tagName, description, score, matchedOn[] } - Prompt: "Search Neumo components for password."
[
{
"tagName": "neumo-input",
"description": "Composed Input component that combines Input field with Addons and Icons. …",
"score": 10,
"matchedOn": ["property description: type"]
}
]
get-component
Full details for a single component: props, events, slots, and CSS parts.
- Inputs:
tagName(string, required) — e.g."neumo-button" - Returns: the component's manifest entry (
kind,description, attributes, events, slots, cssParts, …) - Prompt: "Show the API for neumo-button."
{
"kind": "class",
"description": "`<neumo-button>` — GovOS design system button. … Events: neumo-button-click …"
}
get-usage-example
A ready-to-paste HTML snippet with realistic attribute values.
- Inputs:
tagName(string, required);variant(string, optional) — e.g."disabled","dark","error" - Returns:
{ tagName, variant, description, html } - Prompt: "Give me a usage example for neumo-button." / "…the disabled variant."
{
"tagName": "neumo-button",
"variant": "default",
"description": "Primary button",
"html": "<neumo-button variant=\"primary\">Save changes</neumo-button>"
}
Tokens
get-tokens
Search and filter Neumo design tokens (sourced from Style Dictionary output).
- Inputs (all optional):
query(fuzzy match on name/cssVar/value/description),category(e.g."color","spacing","button"),source("global"|"light"|"dark") - Returns: array of
{ name, cssVar, value, type, description, source } - Prompt: "What's the Neumo token for primary-700?"
- Note: semantic tokens may alias a primitive (
"value": "{primitive.purple.700}"). Query the primitive (e.g.purple-700) to resolve the actual hex.
[
{ "name": "color.primary.700", "cssVar": "--color-primary-700", "value": "{primitive.purple.700}", "type": "color", "source": "global" }
]
suggest-token
Find the closest token for any hex color, with the RGB delta and a recommendation.
- Inputs:
hexValue(string, required) — e.g."#6640c2";category(optional);threshold(number, default15— RGB distance above which it recommends creating a new token) - Returns:
{ input, closest, nearMatches[], recommendation }where recommendation isuse-tokenorcreate-token - Prompt: "Is #6640c2 a Neumo token?"
{
"input": "#6640c2",
"closest": { "token": "--primitive-purple-600", "value": "#7c4ccd", "delta": 27.37 },
"nearMatches": [{ "token": "--primitive-purple-700", "value": "#562c9e", "delta": 44.18 }],
"recommendation": "create-token"
}
remap-variables
Map every custom-property value in a CSS block to the nearest token, and emit a
ready-to-paste :root migration snippet.
- Inputs:
css(string, required) — e.g. the contents of a:rootblock - Returns:
{ variables[], migrationSnippet }; each variable hasrecommendation=direct-map|near-map|no-match - Prompt: "Map these :root vars to Neumo tokens:
--brand: #562c9e; --gap: 16px;"
{
"variables": [
{ "name": "--brand", "value": "#562c9e", "neumoToken": "--primitive-purple-700", "delta": 0, "recommendation": "direct-map" },
{ "name": "--gap", "value": "16px", "neumoToken": "--primitive-16", "delta": 0, "recommendation": "direct-map" }
],
"migrationSnippet": ":root {\n --brand: var(--primitive-purple-700); /* was #562c9e, direct-map */\n --gap: var(--primitive-16); /* was 16px, direct-map */\n}"
}
Audits
audit-spacing
Audit CSS spacing/sizing declarations against the Neumo spacing scale (and
border-radius against the radius scale). Splits shorthands, handles rem/em
and var() tokens, and skips calc()/percentages/keywords.
- Inputs:
css(string, required) - Returns:
{ findings[], counts }; each finding'sbucketison-scale|off-scale-near|off-scale-far|uses-token|ignored. Thecountsfeedscore-alignment'sspacinginput. - Prompt: "Audit this CSS spacing against Neumo:
.x { padding: 16px; margin: 13px; }"
{
"findings": [
{ "property": "padding", "value": "16px", "bucket": "on-scale", "nearestToken": "--spacing-md", "delta": 0 },
{ "property": "margin", "value": "13px", "bucket": "off-scale-near", "nearestToken": "--spacing-sm", "delta": 1 },
{ "property": "gap", "value": "var(--spacing-md)", "bucket": "uses-token" }
]
}
audit-typography
Audit CSS typography (font-family, font-size, font-weight, line-height,
letter-spacing, and the font shorthand) against the Neumo type scale.
- Inputs:
css(string, required) - Returns:
{ findings[], counts }; each finding'sbucketison-scale|off-scale-near|off-scale-far|wrong-family. Thecountsfeedscore-alignment'stypographyinput. - Prompt: "Audit this CSS typography:
.x { font-size: 16px; font-weight: 600; line-height: 1.5; }"
{
"findings": [
{ "property": "font-size", "value": "16px", "bucket": "on-scale", "nearestToken": "--fontSize-md", "delta": 0 },
{ "property": "font-weight", "value": "600", "bucket": "on-scale", "nearestToken": "--fontWeights-semibold", "delta": 0 },
{ "property": "line-height", "value": "1.5", "bucket": "off-scale-far", "nearestToken": null }
]
}
audit-tokens
Audit CSS color declarations against the Neumo color-token catalog. Scans
color, background, border, outline, fill, stroke, box-shadow, etc.;
extracts colors from shorthands; understands hex (3/6/8-digit), rgb()/rgba(),
hsl()/hsla(), and var() token usage; skips keywords like
transparent/currentColor.
- Inputs:
css(string, required) - Returns:
{ findings[], counts, byProperty }; each finding'sbucketisdirect-match|near-match(within perceptual ΔE) |no-match|uses-token|ignored. Thecountsfeedscore-alignment'scolorsinput. - Prompt: "Audit the colors in this CSS against Neumo:
.x { color: #6741c3; background: #fff; }"
{
"findings": [
{ "property": "color", "value": "#6741c3", "bucket": "near-match", "nearestToken": "--color-primary-600", "delta": 0.6 },
{ "property": "background", "value": "#fff", "bucket": "no-match", "nearestToken": null, "delta": null }
],
"counts": { "total": 2, "matched": 0, "nearMatched": 1 }
}
check-contrast
WCAG 2.1 contrast ratio between two hex colors, with pass/fail at AA and AAA for normal and large text.
- Inputs:
foreground(string, required),background(string, required) —#fff,#ffffff,#1a1a1aall accepted - Returns:
{ foreground, background, ratio, AA: {normal, large}, AAA: {normal, large} } - Thresholds: AA ≥ 4.5 normal / ≥ 3.0 large; AAA ≥ 7.0 normal / ≥ 4.5 large. "Large" = ~18pt+ (or 14pt+ bold).
- Limitation: flat fg/bg only — no opacity, no gradients. For a whole page, see Audit color contrast across a real page.
- Prompt: "Check contrast of #562c9e text on #f7f7f7."
{
"foreground": "#562c9e",
"background": "#f7f7f7",
"ratio": 8.71,
"AA": { "normal": "pass", "large": "pass" },
"AAA": { "normal": "pass", "large": "pass" }
}
audit-a11y
Scan HTML or JSX markup for WCAG 2.2 violations. Runs axe-core over a
jsdom parse for structural rules (alt text, accessible names, roles, labels),
plus static checks for color-contrast (1.4.3, cross-referenced with
check-contrast on inline styles) and target-size (2.5.8).
- Inputs:
markup(string, required);jsx(boolean, optional — set true for JSX soclassName→classetc. normalization applies) - Returns:
{ violations[], summary, totalViolations, limitations[] }; each violation has{ sc, rule, severity, count, occurrences, remediation }, grouped by Success Criterion. - Limitation: static-only — runtime ARIA states and class/stylesheet colors aren't evaluated (the
limitationsfield spells this out). - Prompt: "Audit this markup for accessibility:
<button></button>"
{
"violations": [
{ "sc": "4.1.2", "rule": "button-name", "severity": "critical", "count": 1,
"occurrences": [{ "target": "button", "summary": "Element has no accessible name" }],
"remediation": "Buttons must have discernible text (…helpUrl)" }
],
"summary": "1 occurrence across 1 rule: button-name (4.1.2)×1.",
"totalViolations": 1
}
score-alignment
Combine audit results into a single 0–100 alignment score. Accepts partial input — pass whichever audits you have run.
- Inputs (all optional):
colors,spacing,typography,components— each{ total, matched, nearMatched? }; plus optionalweights(defaults: colors 30, spacing 25, typography 25, components 20) - Returns:
{ overall, breakdown, summary, recommendations[] } - Prompt: "Score alignment: spacing 8/10 matched, typography 5/5 matched."
{
"overall": 90,
"breakdown": {
"spacing": { "score": 80, "total": 10, "matched": 8, "weight": 25 },
"typography": { "score": 100, "total": 5, "matched": 5, "weight": 25 }
},
"summary": "Your codebase is strongly aligned with the Neumo design system at 90%. …",
"recommendations": ["Alignment is healthy. Consider auditing near-matches in spacing tokens …"]
}
Migration
find-component-replacements
Scan React (JSX/TSX) source for off-system elements that could be swapped for
Neumo @kofile/gds-react components. AST-parses the source, matches intrinsic
HTML elements (and ARIA-role patterns like role="tablist") to their Neumo
equivalents, and estimates swap effort. Validates/enriches targets against the
component manifest.
- Inputs:
files(array of{ path, code }) for a codebase, or a singlecodestring (with optionalpath) - Returns: a list ranked quick-wins-first of
{ file, line, candidate, suggestedComponent, package, effort, rationale }, pluscounts.byEffort.effortissimple|medium|complex. - Prompt: "What off-system components in this file could be Neumo?
const X = () => <button onClick={f}>Go</button>;"
{
"findings": [
{ "file": "X.tsx", "line": 1, "candidate": "<button>", "suggestedComponent": "Button",
"package": "@kofile/gds-react", "effort": "medium",
"rationale": "<button> → <Button> from @kofile/gds-react — props-only swap; wires an event handler; Button accepts: variant, size, disabled, loading" }
],
"counts": { "total": 1, "byEffort": { "simple": 0, "medium": 1, "complex": 0 } }
}
Common workflows
Generate a component correctly the first time
"Add a Neumo select with a label and an error message."
The assistant calls search-components → get-component (neumo-select) →
get-usage-example, then writes markup using the real props, slots, and
events instead of guessing.
Replace a hardcoded color with a token
"Is
#6640c2a Neumo token?"
suggest-token returns the nearest token and the RGB delta, and recommends
whether to reuse it or create a new one.
Migrate a stylesheet's custom properties to tokens
"Map the
:rootvariables in this file to Neumo tokens."
remap-variables analyzes the CSS and returns per-variable mappings plus a
ready-to-paste :root migration snippet.
Score a component's design-system alignment
Run audit-spacing and audit-typography on the component's CSS, then feed the
counts into score-alignment for a 0–100 score and prioritized fixes.
Audit color contrast across a real page
check-contrast compares two hex values — it has no stylesheet mode, and a
stylesheet rarely pairs color with the background it actually renders on. The
reliable workflow is to read computed styles from the running app:
- Load the page in a browser / headless driver (e.g. Playwright).
- For each visible text node, read its computed
colorand walk up the ancestor chain for the first non-transparentbackground-color. - De-duplicate to unique
foreground|backgroundpairs. - Run each pair through
check-contrast.
This catches inherited backgrounds and resolves nested var()s to their
rendered hex automatically. Caveats: it only covers what's rendered on
screen (states like hover/focus and other breakpoints need their own pass),
and gradients / semi-transparent text aren't flat colors — pick the
worst-case effective color for those manually.
A quick single-pair check is just:
"Check contrast of
#562c9etext on#f7f7f7." → ratio 8.71, AA & AAA pass.
How it loads data
On startup the server loads the component manifest + token JSON from two places, in order:
- Bundled data at
data/, resolved relative to the server module — this is what makes installed /npxusage work from any directory. - Live monorepo files at
process.cwd()(packages/wc/custom-elements.json,packages/foundations/tokens/{Global,Light,Dark}.json) — used when running from a clone of the design-system monorepo, so you always get current files.
Bundled data wins when present. The bundle is a snapshot produced at build/publish time, so the installed package's answers reflect the design system at the version you installed. Bump the package to get newer data.
Troubleshooting
| Symptom | Fix |
|---|---|
Server doesn't appear in /mcp |
Reload the client after editing config. Confirm .mcp.json is valid JSON and in the project root. |
| Claude Code shows it but it won't connect | Run the smoke test. If node can't find the file, check the path in args. |
| "Command not found" / wrong Node | Ensure Node ≥ 18 is on PATH for the client's environment. |
| Project server never prompts for trust | Project-scoped MCP servers require a one-time approval — re-open the project or check client trust settings. |
| Answers look stale / missing a new component | The bundled data is a publish-time snapshot — npm update @kofile/neumo-mcp-server (or bump the pinned version). |
| Component tools say data is missing (clone usage) | In a monorepo clone, regenerate custom-elements.json (nx run wc:cem) and rebuild. |
Server version documented: @kofile/neumo-mcp-server@0.1.0.