The Glitched Goblet Logo

The Glitched Goblet

Where Magic Meets Technology

Teach Your AI Your Stack: Build an MCP Server for Your Design System

September 22, 2025

Intro

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 exposes tokens, 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 new GitHub MCP Registry. (The GitHub Blog)

Setting Up / Prerequisites

  • Node 18+ and TypeScript
  • A design-token source (JSON or Style Dictionary build output works)
  • Optional: your component documentation (MD/MDX) to surface usage notes
  • An editor/agent that speaks MCP (Copilot, Claude Code, Cursor, etc.). OpenAI’s Responses API & ChatGPT Dev Mode also support remote MCP servers now. (OpenAI)

Implementation Steps

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 these

Note: 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)

Scaffolding

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"]
}

2 Put your “truth” in one place

/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."
  }
}

3 Minimal MCP server (HTTP/SSE)

/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

4 Wire it into your agent

  • 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)

5 Security & scoping

  • 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)

6 Optional: publish to the 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

Next Steps

  • 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.

Outro

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)