ds-mcp-server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js"
import { readFile } from "node:fs/promises"
import path from "node:path"
// 1. Load the system's data once at startup
const root = process.env.DS_ROOT ?? process.cwd()
const tokens = JSON.parse(
await readFile(path.join(root, "tokens/tokens.json"), "utf8")
)
const components = JSON.parse(
await readFile(path.join(root, "tokens/components.json"), "utf8")
)
// 2. Create the server
const server = new Server(
{ name: "design-system", version: "1.0.0" },
{ capabilities: { tools: {}, resources: {} } }
)
// 3. Register tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "list_tokens",
description:
"List design tokens by category. Use when applying a token but you don't know the exact name. Prefer semantic tokens (action.primary) over primitives (blue.500).",
inputSchema: {
type: "object",
properties: {
category: { type: "string", enum: ["color", "space", "type", "radius", "shadow"] },
},
},
},
{
name: "find_component",
description:
"Locate a UI component by name or purpose. Use when picking a primitive (Button vs IconButton). For composition patterns, use get_pattern instead.",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
},
],
}))
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name === "list_tokens") {
const cat = (req.params.arguments as { category?: string } | undefined)?.category
const filtered = cat ? filterByCategory(tokens, cat) : flattenTokens(tokens)
return {
content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }],
}
}
if (req.params.name === "find_component") {
const q = (req.params.arguments as { query: string }).query.toLowerCase()
const hits = components.filter(
(c: { name: string; description: string }) =>
c.name.toLowerCase().includes(q) ||
c.description.toLowerCase().includes(q)
)
return { content: [{ type: "text", text: JSON.stringify(hits, null, 2) }] }
}
throw new Error(`Unknown tool: ${req.params.name}`)
})
// 4. Register resources
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{ uri: "ds://tokens", name: "Token catalog", mimeType: "application/json" },
{ uri: "ds://components", name: "Component contracts", mimeType: "application/json" },
],
}))
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
if (req.params.uri === "ds://tokens") {
return {
contents: [
{ uri: req.params.uri, mimeType: "application/json", text: JSON.stringify(tokens) },
],
}
}
if (req.params.uri === "ds://components") {
return {
contents: [
{ uri: req.params.uri, mimeType: "application/json", text: JSON.stringify(components) },
],
}
}
throw new Error(`Unknown resource: ${req.params.uri}`)
})
// 5. Helpers (omitted: flattenTokens, filterByCategory — see your repo)
// 6. Run over stdio so editors can spawn this directly
await server.connect(new StdioServerTransport())What's missing: error handling, caching, the get_pattern and get_decision tools (same shape as list_tokens and find_component), and the helper functions. Add them once the skeleton works. Don't over-engineer before agents prove the API.