ons.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const uiKitServer = new McpServer({
name: "nexus-ui-bridge",
version: "2.1.0",
description: "Semantic context provider for internal component library",
});
const transport = new StdioServerTransport();
await uiKitServer.connect(transport);
Tools should map to the mental model developers use when working with a design system. Instead of generic CRUD operations, expose functions that answer specific implementation questions.
// Tool 1: Catalog discovery
uiKitServer.tool(
"list_ui_primitives",
"Returns a flat index of all available components, including tags, categories, and deprecation status",
{},
async () => {
const registry = await loadComponentRegistry();
const index = registry.map((item) => ({
identifier: item.name,
tag: item.tagName,
category: item.category,
deprecated: item.deprecated ?? false,
}));
return {
content: [{ type: "text", text: JSON.stringify(index, null, 2) }],
};
}
);
// Tool 2: Contract resolution
uiKitServer.tool(
"resolve_component_spec",
"Fetches full implementation details: props, slots, events, accessibility rules, and composition constraints",
{ component: z.string().describe("Component name or HTML tag") },
async ({ component }) => {
const spec = await lookupComponentSpec(component);
if (!spec) {
return {
content: [{ type: "text", text: `Spec not found for "${component}". Check catalog for valid identifiers.` }],
};
}
return {
content: [{ type: "text", text: JSON.stringify(spec, null, 2) }],
};
}
);
Step 3: Connect to Source of Truth
Your data layer should pull from machine-readable artifacts rather than parsing raw source files at runtime. Two primary sources dominate modern design systems:
Custom Elements Manifest: If your library uses Web Components, the manifest already contains structured declarations. Parse it once during build time and cache the result.
import fs from "fs";
import path from "path";
interface UiDeclaration {
name: string;
tagName?: string;
category: string;
deprecated?: boolean;
props: Array<{ name: string; type: string; description: string }>;
slots: Array<{ name: string; description: string }>;
events: Array<{ name: string; description: string }>;
a11y: { roles: string[]; keyboardNav: string[]; contrastNotes: string };
}
async function loadComponentRegistry(): Promise<UiDeclaration[]> {
const manifestPath = path.resolve(process.cwd(), "dist/custom-elements.json");
const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
return raw.modules.flatMap((mod: any) =>
mod.declarations
.filter((decl: any) => decl.customElement)
.map((decl: any) => ({
name: decl.name,
tagName: decl.tagName,
category: decl.metadata?.category ?? "uncategorized",
deprecated: decl.metadata?.deprecated ?? false,
props: decl.members?.filter((m: any) => m.kind === "field") ?? [],
slots: decl.slots ?? [],
events: decl.events ?? [],
a11y: decl.metadata?.a11y ?? { roles: [], keyboardNav: [], contrastNotes: "" },
}))
);
}
Storybook AST Extraction: For React/Vue/Angular libraries, Storybook stories contain working usage patterns. Parse the Abstract Syntax Tree to extract export definitions and reconstruct them as queryable examples.
import ts from "typescript";
import { glob } from "glob";
interface StoryArtifact {
name: string;
code: string;
description: string;
}
async function queryStorybookRegistry(target: string): Promise<StoryArtifact[]> {
const matches = await glob(`**/${target}.stories.{ts,tsx}`, { ignore: "node_modules/**" });
if (!matches.length) return [];
const source = fs.readFileSync(matches[0], "utf-8");
const ast = ts.createSourceFile(matches[0], source, ts.ScriptTarget.Latest, true);
const artifacts: StoryArtifact[] = [];
ts.forEachChild(ast, (node) => {
if (ts.isVariableStatement(node) && node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
for (const decl of node.declarationList.declarations) {
if (decl.initializer && ts.isObjectLiteralExpression(decl.initializer)) {
artifacts.push({
name: decl.name.getText(ast),
code: decl.initializer.getText(ast),
description: `Usage pattern for ${target}`,
});
}
}
}
});
return artifacts;
}
Architecture Decisions & Rationale
- Tool-First Over Endpoint-First: LLMs operate on semantic intent, not HTTP verbs. Named tools with clear descriptions allow agents to discover capabilities dynamically. REST endpoints force agents to guess routing patterns; MCP tools eliminate that friction.
- Loose Response Formatting: Strict JSON schemas break when design systems evolve. MCP tools return flexible text/JSON payloads. Agents parse semantic meaning, not field positions. This future-proofs the server against prop additions or slot renames.
- Build-Time Manifest Generation: Parsing source files at runtime introduces latency and brittleness. Generating
custom-elements.json or Storybook metadata during CI/CD ensures the MCP server serves a stable, versioned snapshot.
- Stdio Transport for IDEs, HTTP for Enterprise: Local editors (VS Code, Cursor, Windsurf) expect
stdio for seamless agent integration. Enterprise deployments benefit from HTTP endpoints with authentication, rate limiting, and centralized logging.
Pitfall Guide
Explanation: Agents rely heavily on tool descriptions to decide when to invoke a function. Generic descriptions like "Get component info" cause misfires and context pollution.
Fix: Write constraint-aware descriptions. Example: "Resolves full component contract including prop types, slot requirements, accessibility rules, and known composition restrictions."
2. Ignoring Composition & Accessibility Context
Explanation: Prop types alone don't convey behavioral rules. Agents will combine incompatible variants or omit ARIA attributes if the tool response lacks explicit guidance.
Fix: Include a11y, compositionRules, and usageConstraints fields in tool responses. Document why certain combinations are prohibited, not just that they exist.
3. Stale Manifest Data
Explanation: Serving outdated custom-elements.json or Storybook metadata causes agents to generate code for deprecated or renamed components.
Fix: Tie manifest generation to your build pipeline. Invalidate the MCP cache on every release. Add a version field to tool responses so agents can detect mismatches.
4. Over-Fetching Context Windows
Explanation: Returning entire component catalogs or full documentation pages in a single tool call exhausts context limits and degrades agent reasoning.
Fix: Implement pagination, field filtering, and summary/detail separation. Let agents request list_ui_primitives first, then drill down with resolve_component_spec.
5. Missing Error Semantics
Explanation: Generic errors like "Not found" force agents to retry blindly. They lack fallback strategies.
Fix: Return structured error messages with actionable hints. Example: "Component 'CardV2' not found. Did you mean 'Card'? Available alternatives: ['Card', 'CardGroup', 'CardStack']."
6. Hardcoded File Paths
Explanation: Assuming ./dist/custom-elements.json exists in every environment breaks CI, monorepos, and containerized deployments.
Fix: Use environment variables (DESIGN_SYSTEM_MANIFEST_PATH, STORYBOOK_OUTPUT_DIR) with sensible defaults. Validate paths at server startup and fail fast with clear logs.
7. Neglecting Rate Limits & Caching
Explanation: Unthrottled tool calls during bulk refactors or multi-agent workflows can overwhelm file I/O or external documentation APIs.
Fix: Implement in-memory caching for frequently requested specs. Add simple rate limiting per session. Log tool invocation frequency to identify optimization opportunities.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Solo developer / local IDE | stdio transport + local manifest | Zero network overhead, instant feedback, no auth complexity | Near-zero infrastructure cost |
| Mid-size team / monorepo | stdio + shared build artifacts | Consistent context across editors, CI-integrated manifest generation | Low (shared storage + build time) |
| Enterprise / multi-tenant | HTTP endpoint + auth + caching | Centralized control, audit logging, rate limiting, cross-tool compatibility | Moderate (hosting + auth layer + cache infra) |
| Figma-to-code workflow | MCP + Figma MCP server pairing | Agents read design specs, map to components, fetch implementation details automatically | Low (leverages existing Figma MCP) |
Configuration Template
{
"servers": {
"nexus-ui-mcp": {
"type": "stdio",
"command": "node",
"args": ["./scripts/mcp-server.js"],
"env": {
"DESIGN_SYSTEM_MANIFEST_PATH": "./dist/custom-elements.json",
"STORYBOOK_OUTPUT_DIR": "./storybook-static",
"MCP_LOG_LEVEL": "warn"
}
}
}
}
{
"scripts": {
"mcp:dev": "tsx watch ./src/mcp/server.ts",
"mcp:build": "tsc -p tsconfig.mcp.json && cp dist/custom-elements.json ./dist/",
"mcp:validate": "node ./scripts/validate-manifest.js"
}
}
Quick Start Guide
- Install dependencies:
npm install @modelcontextprotocol/sdk zod typescript tsx
- Create server entry: Scaffold
src/mcp/server.ts with McpServer initialization and StdioServerTransport connection.
- Define two core tools:
list_ui_primitives for catalog discovery and resolve_component_spec for detailed contracts. Wire them to your manifest loader.
- Run locally: Execute
npm run mcp:dev and point your IDE's MCP configuration to the stdio process. Verify tool invocation with a simple agent prompt.
- Integrate into CI: Add manifest generation to your build pipeline, version the output, and validate paths before deployment.
Implementing an MCP server for your design system shifts AI agents from speculative guesswork to deterministic tool use. The protocol handles the semantic bridge; your responsibility is to expose accurate, constraint-aware context. When executed correctly, the result is faster implementation cycles, consistent design adherence, and measurable reductions in token waste and review overhead.