tenet-ui-mcp
Click on "Deploy 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., "@tenet-ui-mcplist Button props and tokens for tenet-ui 0.4.0"
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.
tenet-ui-mcp
The design-to-code MCP server for the tenet-ui design system, built to the design in mcp-masterclass/design/ds-mcp-server/DESIGN.md (rev 4). The server supplies context and judgment; the agent writes the code and runs the checks. The server never asks the user anything: it decides and explains.
Status: build order steps 1 to 4 done, on MCP 2026-07-28 with the TypeScript SDK v2: ingest pipeline, five catalog tools, resources, Streamable HTTP with self-hosted auth, path one (ingest_design → match_components → resolve_tokens → plan_component) with the server deciding close matches and off-scale values, run_checks judging the capture script's results, path two (audit_code, audit_page), plan_tests, the three prompts, the hooks plugin and the first dashboard. The talk needs these four. Step 5 (Figma) is dropped: no Figma source exists for tenet-ui. Next: step 6, the eval harness, the skill moved into the plugin, external OAuth.
Run it
npm ci
npm run ingest -- latest # or use the committed snapshots/
AUTH_MODE=none npm start # http://127.0.0.1:3000/mcp, no auth (local rehearsal)With the built-in issuer (DESIGN.md §9, self-hosted row):
export AUTH_HMAC_KEY="$(openssl rand -base64 48)" # issuer secret, never a bearer credential
npm start # AUTH_MODE defaults to hs256 when the key is set
npm run token -- --sub apurv --scope ds:read # prints a bearer token for a clientClaude Code, Cursor or any Streamable HTTP client: endpoint http://127.0.0.1:3000/mcp, header Authorization: Bearer <token>. For a local stdio client: npm run stdio.
Environment: PORT, HOST, PUBLIC_URL (the aud of tokens and the resource in PRM), AUTH_MODE (none | hs256), AUTH_HMAC_KEY, AUTH_CLIENT_ID / AUTH_CLIENT_SECRET / AUTH_CLIENT_SCOPES (optional client-credentials client for CI at POST /oauth/token), ALLOWED_ORIGINS, SNAPSHOTS_DIR, DATA_DIR (design store + event spool, default .data/), TELEMETRY=off, TELEMETRY_SALT (key for the principal hash in telemetry; defaults to AUTH_HMAC_KEY), VISION=on and VISION_MODEL (opt-in server-side vision, off by default; see Vision below).
Dashboard: GET /dashboard (HTML) and GET /metrics.json?days=7, bearer-authenticated (header or ?token=), read the event spool the server writes for its own calls and the companion plugin posts to POST /events.
Related MCP server: open-design-system-mcp-starter
What the server serves
Catalog tools (ds:read, read-only, idempotent; fixed order; every tool returns structuredContent + a text summary + resource_links + _meta.traceId). A result trimmed to its cap carries truncated: true and truncatedNote saying how to get the rest. Annotations are per tool: the catalog tools and plan_tests are read-only; the tools that write to the design store (ingest_design, match_components, resolve_tokens, plan_component, run_checks, audit_code, audit_page) are not read-only and not destructive, and only resolve_tokens is idempotent.
Tool | Cap | What it does |
| 2 KB | Ranked, synonym-aware search (dropdown → Select, Menu). Zero results return the five nearest names. |
| 8 KB (64 KB with | Import, props from the types with defaults and deprecations, subcomponents, stories, a11y, guidelines. |
| 4 KB | Tokens by meaning or group, per theme, with documented AA contrast pairs. |
| 2 KB | Icons by keyword with the exact import statement. |
| 4 KB | Nearest token for raw values: colours by ΔE in Lab against the semantic tokens of their role (a background among |
Every tool takes dsVersion. Its JSON schema carries x-mcp-header: DsVersion, so a 2026-07-28 client mirrors it as the Mcp-Param-DsVersion request header and the SDK checks the header against the argument: a mismatch or a missing header is 400 with JSON-RPC error -32020. Resolution: exact → newest patch of that minor → nearest earlier minor → latest of that major → latest, and the effective version is echoed in every result.
Path one, design to code (design:ingest; DESIGN.md §4):
Tool | Cap | What it does |
| 6 KB | The agent describes the screenshot it already sees as a layout tree (regions with roles, bounds, parents, raw styles and candidate component ids; |
| 8 KB | Ranks catalog components per region (role and label synonyms, the analyser's candidates, structural cues such as icon-only, bordered vs plain containers, children) and records confidence. Where the top two are within 0.15 it decides with the guideline test for that pair (Card or plain section, Tag or Badge, Menu or Select, Tabs or SegmentedControl, Banner or Toast, Button or Link, and others) and returns the pick with the rule, a short reason, the guideline pages it cites, the confidence and the runner-up. The Tag or Badge test reads only the region's own text (a word or a count), so it applies at any gap: a region the agent called |
| 6 KB | Every raw value the analyser read → nearest token with delta and the runner-up token. A value beyond tolerance is snapped to the nearest scale step with the delta, or kept as a documented exception with a reason when snapping would change its meaning (a colour visibly different from every semantic token) or |
| 24 KB | The plan: tree with exact imports, prop mapping per node (variant from colour and state, heading level from size, placeholders, wrappers), token references, a11y per node, files, contract rules, catalog excerpts, story and test ideas. A plan, never source. |
Checks (checks:run; DESIGN.md §5, §11, rev 3):
Tool | Cap | What it does |
| 16 KB | Takes the files the agent wrote and the |
Path two, audit and test (DESIGN.md §5):
Tool | Scope | Cap | What it does |
|
| 16 KB | The static scan of |
|
| 16 KB | Judges |
|
| 12 KB | From the component's files (props interface, callbacks, unions, the primitives it imports, its stories) or from a |
Reports from run_checks, audit_code and audit_page share one shape and one store: audit://{auditId}/findings.json (every finding, beyond the inline page), delta.json, report.json; private to the principal, 24 h.
Prompts: design-to-code, audit-ui, test-ui: the path (design-to-code starts with the agent describing the screenshot as a layout tree), the anti-duplicate-call rule, the stop condition (no critical or serious finding, or three rounds), and the rule that the agent acts on the server's decisions and reports them instead of asking the user to choose.
Companion plugin (plugin/): the local colour-measuring script and the describe-design skill that tells the agent when to run it (see above), and the hooks for DESIGN.md §7 layer two: a Claude Code plugin with a PostToolUse hook on the server's tools, a PostToolUse hook on Write/Edit, and a Stop hook. It records tool name, trace id and outcome; that a file changed and what it followed (plan, audit, checks, catalog lookup); and at the end of the turn the rounds of run_checks, whether the last one was green, writes after a plan, lookups before a write, suppressed findings. Spooled locally under ~/.tenet-ui/hooks/ and posted to POST /events when DS_SERVER_URL and DS_SERVER_TOKEN are set. Never arguments, contents or paths in clear. See plugin/README.md.
Dashboard (GET /dashboard): the eight questions of DESIGN.md §7. Calls and principals per tool, p50/p95 and bytes per tool, close calls decided, decisions that fell back to the structural score, values snapped beyond tolerance, first-run pass rate per check, findings per audit and fixed in session, rounds to green and writes after a plan (from hooks), zero-result searches and token exceptions, version distribution and fallbacks, full: true rate. Server events carry names, counts, durations, outcomes and per-check statuses only.
Decisions. The server holds the design system's knowledge, so it decides and never puts a question to the user or the agent. No tool returns input_required or an elicitation form. A close match is settled by a guideline test over the design's structure: a region inside a Card is a plain section (Cards never nest), a page root is never a Card, repeated same-size tiles are Cards, an own heading or action makes a Card, a borderless block that flows with the page is plain. When no guideline test separates two candidates, the higher structural score wins and the decision says basis: "score", so the dashboard can count those. Every decision lists the rule, the reason, the ds:// guideline pages it cites, the confidence and the runner-up, and the plan carries them under "Decisions" and "Documented exceptions". Decisions are deterministic for a given design and dsVersion.
Design store: rows (dsg_…, mtc_…, pln_…, aud_…) are bound to the caller's sub, expire after 24 h, live under DATA_DIR (default .data/). Another principal gets not-found, never forbidden. Private resources: design://{designId}/layout.json, screenshot.png, match.json, matches/{matchId}.json, plan.md, plans/{planId}.md|.json.
Perception: the agent's model sees, the server knows. By default the server makes no model call. The agent's model in the harness already sees the screenshot the user pasted, so it describes the layout itself, in the catalog's vocabulary, following ds://{v}/layout-schema (the same rules the opt-in vision call uses, the role list, this version's component ids for candidates, the JSON Schema of the layout and an example). A model's hex colours read off a screenshot are estimates (ΔE 3 to 20 off the real pixels in a live run), whichever model reads them, so the colours are re-measured from the pixels inside each region's bounds (the interior for a background, the edge lines for a border, the glyph fill for text, including small text with subpixel fringes). By default that happens on the agent's machine: plugin/scripts/measure-colors.mjs <screenshot.png> <layout.json> rewrites the layout with the measured colours and prints the corrections, so the agent sends only the layout and the screenshot never leaves the machine. The script is a dependency-free port of src/design/pixels.ts, and the tests check both give identical corrections. For an agent that cannot run it, the server measures a PNG sent as image the same way and records each change in design://{designId}/layout.json as colorCorrections. On the stat-cards baseline the measurement took the token exceptions from 9 to 0 and fixed a border that had snapped to the wrong token. A design from Figma has exact values and skips measuring. Why not on the server: a server-side call costs the operator money (about 8 s and 3.3k input / 0.9k output tokens per screenshot on Opus 5.5), sends user screenshots to a provider the user did not choose, and may use a weaker model than the user's own; and sampling is deprecated in MCP 2026-07-28, so the server cannot borrow the client's model.
Vision (opt-in, for internal deployments): VISION=on turns on the server-side call: @anthropic-ai/sdk, model VISION_MODEL (default claude-opus-5-5), structured output via betaZodOutputFormat, server-side refusal fallbacks on, credentials from ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN or an ant auth login profile. Then {type: "image"} alone is described by that model and corrected from the pixels the same way, server-side. A model key without VISION=on does not turn it on, so a key that happens to be in the environment never starts model calls on users' screenshots; the startup line says vision off and notes the key.
Resources: ds://versions, ds://{v}/components/{name} (markdown), ds://{v}/tokens/{group}.json, ds://{v}/guidelines/{topic} (the nine system pages, any component id, contract, audit-rules), ds://{v}/gaps, ds://{v}/changelog, ds://{v}/deprecations, ds://{v}/baselines (the release's story screenshots), ds://{v}/baselines/{storyId}.{theme}.png, and ds://{v}/layout-schema (how to describe a screenshot for ingest_design). get_component links each story to its light baseline.
Discovery: GET /healthz, GET /.well-known/oauth-protected-resource (also at the path-aware /.well-known/oauth-protected-resource/mcp), GET /.well-known/oauth-authorization-server. MCP clients call server/discover for versions, capabilities and server info. No token → 401 with WWW-Authenticate: Bearer resource_metadata=…; missing scope → one 403 insufficient_scope naming every missing scope.
Telemetry: one JSON line per request / tool call / resource read on stderr with a W3C trace id. The trace comes from params._meta.traceparent (SEP-414) when the client sends one, else from the traceparent header, else a fresh one; it is echoed back in the response header and in each result's _meta.traceId. Names, durations, statuses, versions, byte counts, and the principal as a keyed hash (p_…), never the raw sub. Never arguments or findings.
Protocol note
The server runs on the TypeScript MCP SDK v2 (@modelcontextprotocol/server 2.3.1; tests use @modelcontextprotocol/client 2.3.1), which implements MCP 2026-07-28:
No initialize, no sessions. Every request carries its protocol version, client info and capabilities in
_meta.src/http.tsbuilds onecreateMcpHandlerper process and the SDK makes a freshMcpServerper request from the same factory;src/stdio.tsusesserveStdiowith that factory.server/discoveris answered by the SDK: supported versions, capabilities, instructions, and server info in the result's_meta.List caching (SEP-2549).
tools/list,prompts/list,resources/templates/listandserver/discovercarryttlMsof one hour,resources/listfive minutes, allcacheScope: "private". Ads://read at an exact version is cached for 24 h;latestandds://versionsfor five minutes;design://andaudit://reads havettlMs: 0.Header mirroring (SEP-2243). The SDK requires
MCP-Protocol-Version,Mcp-MethodandMcp-Name, and validatesMcp-Param-DsVersionagainst thedsVersionargument.Trace propagation (SEP-414).
traceparentin_metais read through the SDK'sTRACEPARENT_META_KEY.Older hosts. A client on 2025-11-25 is still served through the SDK's stateless legacy path (
initializeanswered, no session id). Header mirroring does not apply to those requests, so thedsVersionargument alone decides.
tools/list is about 26 KB for thirteen tools with output schemas. The 7 KB figure in DESIGN.md §6 is not reachable with per-tool output schemas and is tracked as a budget of 2 KB per tool in the tests.
Layout
src/ingest/ the data pipeline (DESIGN.md §10), see below
src/catalog/store.ts snapshot reads + dsVersion resolution
src/catalog/resolve.ts nearest-token resolution (ΔE / distance)
src/catalog/synonyms.ts what people call components and token groups
src/tools/ tools as plain functions (unit-tested without a transport): catalog-, design-, audit-, test-, checks-tools
src/design/ layout schema and rules, opt-in vision call, pixel colour correction, matcher, token resolution, plan builder, test-plan builder
src/checks/ static scan + contract rules, source parser, judgment over capture results, page judgment, SSIM, shared report plumbing
src/store/ the design store (principal-bound rows, 24 h TTL; designs, matches, plans, reports)
src/resources.ts ds:// public, design:// and audit:// private resources
src/prompts.ts design-to-code, audit-ui, test-ui
src/metrics.ts event spool, the §7 summary, the dashboard page
src/server.ts McpServer factory: registration order, structured output, cache hints, _meta, telemetry with derived metrics
src/http.ts Streamable HTTP via createMcpHandler: Origin, bearer, scopes, PRM, AS metadata, client credentials; /events, /metrics.json, /dashboard
src/stdio.ts stdio via serveStdio, same server factory
src/worker/ Cloudflare Worker entry: workers-oauth-provider login (owner + read-only guest), bundled snapshots, design store on KV
src/catalog/files.ts where snapshot files come from: disk on Node, bundle + ASSETS on the Worker
src/design/png.ts PNG decoding on zlib.inflateSync (pngjs's sync reader does not run on Workers)
plugin/ the companion Claude Code plugin: scripts/measure-colors.mjs + the describe-design skill, hooks.json + ds-hook.mjs (layer two)
src/auth/tokens.ts HS256 issuer + verifier (jose); src/auth/cli.ts mints tokens
src/content/ the component contract and audit rule catalog (ds://…/guidelines/contract, audit-rules)
snapshots/ one folder per ingested version + one per guidelines revision (committed)
.github/workflows/ ci.yml (check, then deploy the Worker from main) · ingest.yml
scripts/ bundle-snapshots.mjs, with-env.mjs (fills wrangler.jsonc from .env), logs.mjs, oauth-smoke.mjs
Dockerfile single container, no browserIngest
npm run ingest -- 0.4.0 # one version
npm run ingest -- --new # every registry version newer than the newest snapshot (the registry watch)
npm run ingest -- --listSource | Read from | Yields |
Provenance | npm attestations endpoint | The SLSA statement must name |
Types |
| Every export: components, hooks, props with types, literal unions expanded, required flags, |
Catalog |
| Status, |
Guidelines |
| A guidelines revision |
Tokens, icons, deprecations, changelog |
| The token registry with documented contrast pairs; the icon manifest; the migration registry; parsed releases |
Stories |
| Story ids, titles, play-function tags |
Baselines |
| 1280×800 screenshots per story per theme with a sha256 manifest |
Every snapshot carries a manifest.json (each source with sourceRef, content hash, extractedAt) and a gaps.json. The version index is only updated after the whole snapshot validates. ingest.yml runs on repository_dispatch: tenet-ui-released from tenet-ui's release workflow, on a 6-hourly schedule, and by hand, and commits new snapshots.
Deploy (Cloudflare Workers)
Live at https://tenet-ui-mcp.<sub>.workers.dev, set up the same way as the expensify MCP server: OAuth 2.1 through @cloudflare/workers-oauth-provider, a login page with an owner (password, every scope) and a guest (no password, ds:read only), so a host connects with
claude mcp add --transport http tenet-ui https://tenet-ui-mcp.<sub>.workers.dev/mcpWhat differs from the Node server: the snapshot text files are bundled into the Worker and the baseline PNGs are its static assets (npm run worker:bundle packs both into .worker/), the design store is Workers KV with KV expiry instead of the purge timer, telemetry and hook events go to Workers Logs, and /dashboard and /metrics.json exist only on the Node server. No Cloudflare id is in git: wrangler.jsonc has ${...} tokens that scripts/with-env.mjs fills from .env.
cp .env.example .env
npx wrangler kv namespace create tenet-ui-mcp-OAUTH_KV # id -> OAUTH_KV_ID
npx wrangler kv namespace create tenet-ui-mcp-DESIGNS # id -> DESIGNS_KV_ID
# WORKERS_SUBDOMAIN: the <sub> of your workers.dev URL
npm run deploy
npx wrangler secret put OWNER_PASSWORD --config wrangler.generated.jsonc
npm run logs # readable live tail
OWNER_PASSWORD=... node scripts/oauth-smoke.mjs https://tenet-ui-mcp.<sub>.workers.dev apurv # OAuth + every tool, timed
npm run worker:dev # local, http://localhost:8789 (.dev.vars: OWNER_PASSWORD, PUBLIC_URL)CI deploys every green main, including the snapshot commits of ingest.yml (which starts CI itself, since a push made with GITHUB_TOKEN triggers no workflows). It needs the CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID secrets and the OAUTH_KV_ID, DESIGNS_KV_ID and WORKERS_SUBDOMAIN repository variables; without the token the deploy job does nothing.
CPU. The free plan allows 10 ms of CPU per request. Measured on the deployed Worker (2026-10-25): catalog calls 6 to 22 ms, match_components 43 ms, ingest_design with a 1280×800 screenshot 175 ms, run_checks with SSIM against that screenshot 286 ms. All of them completed, but they are over the documented limit, so Cloudflare may reject them under steady use (error 1102). Workers Paid ($5 a month, 30 s of CPU) removes that risk.
Next
DESIGN.md §15 step 6 (step 5, Figma, is dropped: no Figma source for tenet-ui): the dual-path eval harness over golden screenshots and golden source files, the skill moved into this repo's plugin next to the hooks, external OAuth (PRM against a real authorization server, CIMD) if the server is ever shared beyond one team. design_to_plan as a Task once the composite is worth it.
This server cannot be deployed
Maintenance
Related MCP Connectors
Serves your design system and coding standards to coding agents, so they stop guessing.
Access and maintain design system docs, tokens, components, skills, and contexts across any project.
Read-only canon of design and copy rules for AI agents to audit generated UI, served as resources.
Live React design-system APIs, patterns, and code validation so AI agents build real UI, not slop.
Related MCP Servers
- AlicenseAqualityAmaintenanceRead-only MCP server that exposes a design system's tokens, components, conventions, and deprecations as queryable tools, enabling agents to look up canonical values, assess change impact, and detect hardcoded value drift.82 npmMIT
- AlicenseNot gradedqualityBmaintenanceProvides a real API for coding agents to look up design system components, props, and tokens, preventing guessed answers.9MIT
- AlicenseNot gradedqualityBmaintenanceProvides coding agents with local tools to inspect project UI inventories, propose and compare visual direction boards, compile versioned design contracts and DTCG tokens, retrieve section-specific blueprints, and audit running interfaces with browser evidence including screenshots, accessibility findings, and overflow measurements.MIT
- FlicenseNot gradedqualityAmaintenanceEnables coding agents building UI to pull real product screens and design materials from a catalogue, returning inline reference images plus typed structured content such as colors, typography, and components. It also serves bundled skill documents, help, and status resources locally so agents can settle a screen's contract before writing code.-