September 22, 2025
LLMs keep hallucinating button sizes and not sticking to your other styles? Let’s fix that by letting your agent read your actual design tokens and components
instead of guessing. The Model Context Protocol (MCP)
is the emerging “USB-C for AI apps.” A standard way to expose tools and data to agents like Copilot/Claude/Cursor. We’ll build a tiny MCP server that serves your tokens + component docs so your AI can generate compliant React
on the first swing. (Anthropic)
tldr; We’ll ship a Node/TS
MCP server
that exposestokens
,components
, and a couple of utility tools, then wire it into Copilot/Claude. Along the way we’ll cover security, scoping, and how to publish to the newGitHub MCP Registry
. (The GitHub Blog)
OpenAI’s Responses API & ChatGPT Dev Mode also support remote MCP servers now.
(OpenAI)We’ll make a minimal but real MCP server:
Resources
: tokens.json
, components.json
Tools
: get_token(name)
, list_tokens(prefix?)
, get_component(name)
Prompts
: small scaffolds that tell the agent how to consume theseNote: The protocol concept is from Anthropic (open standard). Figma’s Dev Mode MCP server shows how powerful “design-as-context” can be. We’ll mimic the spirit, not their internals. (Anthropic)
mkdir mcp-design-system && cd mcp-design-system
npm init -y
npm i typescript ts-node zod
npm i -D @types/node
Create tsconfig.json
(strict on):
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}
/data/tokens.json
(sample):
{
"color.primary.600": "#0a58ca",
"space.2": "8px",
"radius.lg": "12px",
"font.body": "'Source Sans 3', system-ui"
}
/data/components.json
(sample):
{
"Button": {
"props": {
"variant": ["primary", "secondary", "link"],
"size": ["sm", "md", "lg"],
"disabled": "boolean"
},
"tokens": ["color.primary.600", "radius.lg", "space.2"],
"usage": "Primary is default, never use 'link' for destructive actions."
},
"Alert": {
"props": { "status": ["info", "success", "warning", "error"] },
"tokens": ["space.2"],
"usage": "One sentence max in banners, multiline for page alerts."
}
}
/src/server.ts
:
import http from 'node:http'
import { readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { ZodError, z } from 'zod'
const tokens = JSON.parse(readFileSync('./data/tokens.json', 'utf-8'))
const components = JSON.parse(readFileSync('./data/components.json', 'utf-8'))
/`
* Simple auth: require an X-API-Key header (scoped key per repo/team).
* In production, load from env/secrets manager.
*/
const API_KEY = process.env.API_KEY || 'dev-key-do-not-use'
/`
* MCP message envelopes (simplified):
* - Clients POST tool invocations to /mcp/tools
* - Clients GET resource content at /mcp/resources/:id
* - Clients GET /mcp/manifest to discover capabilities
* Most MCP clients support HTTP or HTTP+SSE transports. We keep it minimal.
*/
type ToolRequest = {
id: string
name: string
args?: Record<string, unknown>
}
const getTokenSchema = z.object({ name: z.string() })
const listTokensSchema = z.object({ prefix: z.string().optional() })
const getComponentSchema = z.object({ name: z.string() })
function requireAuth(req: http.IncomingMessage): boolean {
const key = req.headers['x-api-key']
return key === API_KEY
}
function json(res: http.ServerResponse, status: number, body: unknown) {
res.writeHead(status, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(body))
}
function notAuthorized(res: http.ServerResponse) {
json(res, 401, { error: 'Unauthorized' })
}
const server = http.createServer(async (req, res) => {
// Basic health/manifest
if (req.url === '/mcp/manifest' && req.method === 'GET') {
return json(res, 200, {
name: 'design-system-mcp',
version: '0.1.0',
resources: [
{ id: 'tokens', title: 'Design Tokens', mimeType: 'application/json' },
{ id: 'components', title: 'Component Catalog', mimeType: 'application/json' },
],
tools: [
{ name: 'get_token', schema: { name: 'string' } },
{ name: 'list_tokens', schema: { prefix: 'string?' } },
{ name: 'get_component', schema: { name: 'string' } },
],
prompts: [{ name: 'use_tokens', description: 'How to consume tokens in React/TS' }],
})
}
if (!requireAuth(req)) return notAuthorized(res)
// Resources
if (req.url?.startsWith('/mcp/resources/') && req.method === 'GET') {
const id = req.url.split('/').pop()
if (id === 'tokens') return json(res, 200, tokens)
if (id === 'components') return json(res, 200, components)
return json(res, 404, { error: 'Resource not found' })
}
// Tools
if (req.url === '/mcp/tools' && req.method === 'POST') {
let body = ''
req.on('data', (chunk) => (body += chunk))
req.on('end', () => {
try {
const { id, name, args } = JSON.parse(body) as ToolRequest
let result: unknown
if (name === 'get_token') {
const { name } = getTokenSchema.parse(args ?? {})
result = { name, value: tokens[name] ?? null }
} else if (name === 'list_tokens') {
const { prefix } = listTokensSchema.parse(args ?? {})
const entries = Object.entries(tokens)
.filter(([k]) => (prefix ? k.startsWith(prefix) : true))
.map(([k, v]) => ({ name: k, value: v }))
result = entries
} else if (name === 'get_component') {
const { name } = getComponentSchema.parse(args ?? {})
result = components[name] ?? null
} else {
return json(res, 400, { error: 'Unknown tool', id, name })
}
return json(res, 200, { id, ok: true, result })
} catch (e) {
if (e instanceof ZodError) {
return json(res, 400, { error: 'Bad args', details: e.errors })
}
return json(res, 500, { error: 'Server error' })
}
})
return
}
// Prompts (tiny helpers the agent can request)
if (req.url === '/mcp/prompts/use_tokens' && req.method === 'GET') {
const hash = createHash('sha256').update(JSON.stringify(tokens)).digest('hex').slice(0, 8)
return json(res, 200, {
text:
`When generating UI, you MUST use tokens from /mcp/resources/tokens.\n` +
`If a token is missing, call tool:list_tokens to find the closest match.\n` +
`Prefer semantic tokens over raw hex/px.\n` +
`Token set hash: ${hash} (use for caching).`,
})
}
json(res, 404, { error: 'Not found' })
})
server.listen(8787, () => console.log('MCP server on :8787'))
Run it:
API_KEY=dev-key-do-not-use npx ts-node src/server.ts
Copilot / MCP-aware IDEs:
Add a connection pointing to http://localhost:8787
with X-API-Key
. (Exact UI varies by client, check your tool’s “Add MCP server” flow.)OpenAI Responses API:
declare a remote MCP server
tool and call it from your Next.js route/edge function. (OpenAI)Claude/Cursor/Windsurf:
add a server entry in their MCP settings, they’ll auto-discover /mcp/manifest
. (Concept documented by Anthropic, product specifics vary.) (Anthropic)Read-only by default.
Our sample exposes read-only resources/tools, prefer additive power later (e.g., “open PR” tool).API keys per consumer
(repo/team), rotate them, and enforce least privilege
.Prompt-injection controls
: your agent should treat external docs as untrusted
, MCP helps with structure, but your client still needs guards. (This risk is why platform vendors emphasize permissions/consent flows around MCP.) (The Verge)GitHub MCP Registry
If you want others on your team/org to find it like an “app store,” publish metadata to the MCP Registry
. It’s new, but discoverability is the point, with one-click add from clients that support it. Start with the preview repo’s docs. (The GitHub Blog)
# registry-entry.yaml (sketch)
id: com.yourorg.design-mcp
name: YourOrg Design System
transport: http
endpoint: https://mcp.yourorg.com
auth: apiKey
resources:
- tokens
- components
tools:
- get_token
- list_tokens
- get_component
Expand resources
: add patterns.json
, a11y-rules.json
, and MDX usage guides.Add “code actions”
: a tool that opens a PR
to apply token fixes in a repo (after human approval).Cache & ETags
: include hashes in /mcp/manifest
and ETag
headers so clients cache aggressively.Figma tie-in
: mirror Figma Dev Mode IDs → components so agents can crosswalk design → code reliably (Figma’s MCP server shows this works well). (Figma)Edge deploy
: stick this on Cloudflare/Workers or a minimal Node host.That’s a wrap! With ~150 lines of TypeScript you’ve turned “use our styles, please” into a protocol-level guarantee. MCP is new, but the momentum is real. Anthropic defined it, Figma
is piping designs through it, OpenAI
and GitHub
are wiring it into their stacks. Your agent can finally stop guessing and start following your system. (Anthropic)