@deepgram/styles 0.2.13 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -292,6 +292,67 @@ Auto-generated custom elements with Shadow DOM, built from the same YAML source.
292
292
 
293
293
  ---
294
294
 
295
+ ## MCP Server (AI Agent Integration)
296
+
297
+ The package includes a built-in [Model Context Protocol](https://modelcontextprotocol.io/) server that gives AI coding agents direct access to the design system — component names, BEM classes, rendered HTML examples, and design tokens.
298
+
299
+ ### Setup
300
+
301
+ Install the package, then configure your AI tool:
302
+
303
+ **Claude Code** (CLI or `.mcp.json`):
304
+
305
+ ```bash
306
+ claude mcp add deepgram-styles --scope project -- npx -y -p @deepgram/styles mcp
307
+ ```
308
+
309
+ **Claude Desktop** (`claude_desktop_config.json`), **Cursor** (`.cursor/mcp.json`), or **Windsurf** (`.windsurf/mcp.json`):
310
+
311
+ ```json
312
+ {
313
+ "mcpServers": {
314
+ "deepgram-styles": {
315
+ "command": "npx",
316
+ "args": ["-y", "-p", "@deepgram/styles", "mcp"]
317
+ }
318
+ }
319
+ }
320
+ ```
321
+
322
+ ### Available Tools
323
+
324
+ | Tool | Description |
325
+ |------|-------------|
326
+ | `list_components` | List all components with metadata, tags, and counts. Filter by category. |
327
+ | `get_component` | Full details for a component: BEM classes, variants, rendered HTML examples. |
328
+ | `get_design_tokens` | Design tokens (colors, spacing, fonts, shadows, border-radius). |
329
+ | `search_components` | Keyword search across names, titles, tags, and descriptions. |
330
+
331
+ ### Custom YAML
332
+
333
+ To point the server at a custom design system YAML:
334
+
335
+ ```json
336
+ {
337
+ "mcpServers": {
338
+ "deepgram-styles": {
339
+ "command": "npx",
340
+ "args": ["-y", "-p", "@deepgram/styles", "mcp", "--yaml", "./path/to/design-system.yaml"]
341
+ }
342
+ }
343
+ }
344
+ ```
345
+
346
+ ### Testing
347
+
348
+ Use the MCP Inspector to test interactively:
349
+
350
+ ```bash
351
+ npx @modelcontextprotocol/inspector npx -y -p @deepgram/styles mcp
352
+ ```
353
+
354
+ ---
355
+
295
356
  ## Package Exports
296
357
 
297
358
  | Import | Description |
@@ -303,6 +364,7 @@ Auto-generated custom elements with Shadow DOM, built from the same YAML source.
303
364
  | `@deepgram/styles/react` | Typed React components |
304
365
  | `@deepgram/styles/utils` | Theme utility functions |
305
366
  | `@deepgram/styles/design-system` | Raw YAML design tokens |
367
+ | `@deepgram/styles/mcp` | MCP server for AI agent integration |
306
368
  | `@deepgram/styles/logo/*` | 12 SVG logo variants |
307
369
 
308
370
  ---
@@ -660,6 +660,13 @@ categories:
660
660
  web-components:
661
661
  name: Web Components
662
662
  description: Framework-agnostic custom elements
663
+ integrations:
664
+ name: Integrations
665
+ icon: fa-plug
666
+ subsections:
667
+ mcp-server:
668
+ name: MCP Server
669
+ description: Give AI agents access to the design system
663
670
  components:
664
671
  btn:
665
672
  metadata:
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for @deepgram/styles
4
+ *
5
+ * Exposes the design system (tokens, components, examples) as MCP tools
6
+ * so AI agents can query BEM classes, rendered HTML examples, and tokens.
7
+ *
8
+ * Usage:
9
+ * npx -p @deepgram/styles mcp # uses bundled design-system.yaml
10
+ * node dist/mcp/server.js --yaml /path/to/design-system.yaml
11
+ */
12
+ export {};
@@ -0,0 +1,477 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for @deepgram/styles
4
+ *
5
+ * Exposes the design system (tokens, components, examples) as MCP tools
6
+ * so AI agents can query BEM classes, rendered HTML examples, and tokens.
7
+ *
8
+ * Usage:
9
+ * npx -p @deepgram/styles mcp # uses bundled design-system.yaml
10
+ * node dist/mcp/server.js --yaml /path/to/design-system.yaml
11
+ */
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { readFileSync } from "node:fs";
15
+ import { resolve, dirname } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { parseDocument } from "yaml";
18
+ import { z } from "zod";
19
+ // ---------------------------------------------------------------------------
20
+ // Type guard
21
+ // ---------------------------------------------------------------------------
22
+ function isUseRefNode(node) {
23
+ return (typeof node === "object" &&
24
+ node !== null &&
25
+ "$ref" in node &&
26
+ typeof node.$ref === "string" &&
27
+ !("tag" in node));
28
+ }
29
+ // ---------------------------------------------------------------------------
30
+ // Inline AST → HTML renderer (from generators/html.ts)
31
+ // ---------------------------------------------------------------------------
32
+ const SELF_CLOSING = new Set(["img", "br", "hr", "input", "meta", "link"]);
33
+ function formatHTMLProps(props) {
34
+ return Object.entries(props)
35
+ .map(([key, value]) => {
36
+ if (typeof value === "boolean")
37
+ return value ? key : "";
38
+ const attrName = key === "className" ? "class" : key;
39
+ return `${attrName}="${value}"`;
40
+ })
41
+ .filter(Boolean)
42
+ .join(" ");
43
+ }
44
+ function indentStr(code, level = 1) {
45
+ const spaces = " ".repeat(level);
46
+ return code
47
+ .split("\n")
48
+ .map((line) => (line ? spaces + line : line))
49
+ .join("\n");
50
+ }
51
+ function astToHTML(node, depth = 0) {
52
+ const { tag, props = {}, children = [], text } = node;
53
+ const propsStr = Object.keys(props).length > 0 ? " " + formatHTMLProps(props) : "";
54
+ if (SELF_CLOSING.has(tag) && !text && children.length === 0) {
55
+ return `<${tag}${propsStr} />`;
56
+ }
57
+ if (text) {
58
+ return `<${tag}${propsStr}>${text}</${tag}>`;
59
+ }
60
+ if (children.length > 0) {
61
+ const childrenStr = children
62
+ .map((child) => {
63
+ if (typeof child === "string")
64
+ return child;
65
+ if ("tag" in child)
66
+ return astToHTML(child, depth + 1);
67
+ return "";
68
+ })
69
+ .filter(Boolean)
70
+ .join("\n");
71
+ if (children.length > 1 ||
72
+ (children[0] && typeof children[0] !== "string")) {
73
+ return `<${tag}${propsStr}>\n${indentStr(childrenStr, 1)}\n</${tag}>`;
74
+ }
75
+ return `<${tag}${propsStr}>${childrenStr}</${tag}>`;
76
+ }
77
+ return `<${tag}${propsStr}></${tag}>`;
78
+ }
79
+ // ---------------------------------------------------------------------------
80
+ // Inline $ref resolver (from parser/resolve-refs.ts)
81
+ // ---------------------------------------------------------------------------
82
+ function resolveComponentRefs(ds) {
83
+ const resolved = { ...ds, components: { ...ds.components } };
84
+ for (const [key, component] of Object.entries(resolved.components)) {
85
+ resolved.components[key] = resolveComponentExamples(ds, component);
86
+ }
87
+ return resolved;
88
+ }
89
+ function resolveComponentExamples(ds, component) {
90
+ const result = { ...component };
91
+ if (result.examples) {
92
+ result.examples = result.examples.map((ex) => ({
93
+ ...ex,
94
+ ast: resolveNode(ds, ex.ast),
95
+ }));
96
+ }
97
+ if (result.variants) {
98
+ result.variants = { ...result.variants };
99
+ for (const [vKey, variant] of Object.entries(result.variants)) {
100
+ if (variant.examples) {
101
+ result.variants[vKey] = {
102
+ ...variant,
103
+ examples: variant.examples.map((ex) => ({
104
+ ...ex,
105
+ ast: resolveNode(ds, ex.ast),
106
+ })),
107
+ };
108
+ }
109
+ }
110
+ }
111
+ if (result.components) {
112
+ result.components = { ...result.components };
113
+ for (const [cKey, child] of Object.entries(result.components)) {
114
+ if (child.examples) {
115
+ result.components[cKey] = {
116
+ ...child,
117
+ examples: child.examples.map((ex) => ({
118
+ ...ex,
119
+ ast: resolveNode(ds, ex.ast),
120
+ })),
121
+ };
122
+ }
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ function resolveNode(ds, node) {
128
+ if (isUseRefNode(node)) {
129
+ return resolveUseRef(ds, node);
130
+ }
131
+ if (!node.children)
132
+ return node;
133
+ const resolvedChildren = node.children.map((child) => {
134
+ if (typeof child === "string")
135
+ return child;
136
+ return resolveNode(ds, child);
137
+ });
138
+ return { ...node, children: resolvedChildren };
139
+ }
140
+ function resolveUseRef(ds, ref) {
141
+ const segments = ref.$ref.split("/");
142
+ const componentKey = segments[0];
143
+ const component = ds.components[componentKey];
144
+ if (!component) {
145
+ return { tag: "div", props: { class: `dg-${componentKey}` }, text: `[unresolved: ${ref.$ref}]` };
146
+ }
147
+ const blockClass = `dg-${componentKey}`;
148
+ let tag = component.tag ?? "div";
149
+ const classes = [blockClass];
150
+ if (segments.length > 1) {
151
+ const pathSegment = segments.slice(1).join("/");
152
+ const resolved = resolvePathSegment(ds, component, componentKey, blockClass, pathSegment);
153
+ tag = resolved.tag;
154
+ classes.length = 0;
155
+ classes.push(...resolved.classes);
156
+ }
157
+ const mergedProps = {};
158
+ if (classes.length > 0) {
159
+ mergedProps.class = classes.join(" ");
160
+ }
161
+ if (ref.props) {
162
+ for (const [key, value] of Object.entries(ref.props)) {
163
+ if (key === "class" && typeof value === "string") {
164
+ mergedProps.class = mergedProps.class
165
+ ? `${mergedProps.class} ${value}`
166
+ : value;
167
+ }
168
+ else {
169
+ mergedProps[key] = value;
170
+ }
171
+ }
172
+ }
173
+ let resolvedChildren;
174
+ if (ref.children) {
175
+ resolvedChildren = ref.children.map((child) => {
176
+ if (typeof child === "string")
177
+ return child;
178
+ return resolveNode(ds, child);
179
+ });
180
+ }
181
+ const result = { tag, props: mergedProps };
182
+ if (resolvedChildren)
183
+ result.children = resolvedChildren;
184
+ if (ref.text)
185
+ result.text = ref.text;
186
+ return result;
187
+ }
188
+ function resolvePathSegment(ds, component, componentKey, blockClass, pathSegment) {
189
+ const parts = pathSegment.split("+");
190
+ if (parts.length > 1) {
191
+ for (const part of parts) {
192
+ if (!component.variants?.[part]) {
193
+ return {
194
+ tag: component.tag ?? "div",
195
+ classes: [blockClass, ...parts.map((p) => `${blockClass}--${p}`)],
196
+ };
197
+ }
198
+ }
199
+ return {
200
+ tag: component.tag ?? "div",
201
+ classes: [blockClass, ...parts.map((p) => `${blockClass}--${p}`)],
202
+ };
203
+ }
204
+ const name = parts[0];
205
+ if (component.variants?.[name]) {
206
+ return {
207
+ tag: component.tag ?? "div",
208
+ classes: [blockClass, `${blockClass}--${name}`],
209
+ };
210
+ }
211
+ if (component.components?.[name]) {
212
+ const child = component.components[name];
213
+ const elementClass = `dg-${componentKey}__${name}`;
214
+ if (child.$ref) {
215
+ const refComponent = ds.components[child.$ref];
216
+ return {
217
+ tag: child.tag ?? refComponent?.tag ?? "div",
218
+ classes: [`dg-${child.$ref}`, elementClass],
219
+ };
220
+ }
221
+ return { tag: child.tag ?? "div", classes: [elementClass] };
222
+ }
223
+ // Graceful fallback for unknown segments
224
+ return {
225
+ tag: component.tag ?? "div",
226
+ classes: [blockClass, `${blockClass}--${name}`],
227
+ };
228
+ }
229
+ function collectAllExamples(component) {
230
+ const results = [];
231
+ for (const example of component.examples) {
232
+ results.push({ example });
233
+ }
234
+ if (component.variants) {
235
+ for (const [vKey, variant] of Object.entries(component.variants)) {
236
+ if (variant.examples) {
237
+ for (const example of variant.examples) {
238
+ results.push({ example, variantKey: vKey });
239
+ }
240
+ }
241
+ }
242
+ }
243
+ return results;
244
+ }
245
+ // ---------------------------------------------------------------------------
246
+ // CSS class extractor
247
+ // ---------------------------------------------------------------------------
248
+ function extractBEMClasses(component, name) {
249
+ const classes = new Set();
250
+ const blockClass = `dg-${name}`;
251
+ classes.add(blockClass);
252
+ // From css rules
253
+ if (component.css) {
254
+ for (const selector of Object.keys(component.css)) {
255
+ const matches = selector.match(/\.dg-[\w-]+(?:__[\w-]+)?(?:--[\w-]+)?/g);
256
+ if (matches)
257
+ matches.forEach((m) => classes.add(m.replace(/^\./, "")));
258
+ }
259
+ }
260
+ // From variants
261
+ if (component.variants) {
262
+ for (const vKey of Object.keys(component.variants)) {
263
+ classes.add(`${blockClass}--${vKey}`);
264
+ const variant = component.variants[vKey];
265
+ if (variant.css) {
266
+ for (const selector of Object.keys(variant.css)) {
267
+ const matches = selector.match(/\.dg-[\w-]+(?:__[\w-]+)?(?:--[\w-]+)?/g);
268
+ if (matches)
269
+ matches.forEach((m) => classes.add(m.replace(/^\./, "")));
270
+ }
271
+ }
272
+ }
273
+ }
274
+ // From elements/components
275
+ if (component.components) {
276
+ for (const cKey of Object.keys(component.components)) {
277
+ classes.add(`${blockClass}__${cKey}`);
278
+ }
279
+ }
280
+ // From elements (legacy css-based)
281
+ if (component.elements) {
282
+ for (const eKey of Object.keys(component.elements)) {
283
+ classes.add(`${blockClass}__${eKey}`);
284
+ }
285
+ }
286
+ return [...classes].sort();
287
+ }
288
+ // ---------------------------------------------------------------------------
289
+ // Load YAML
290
+ // ---------------------------------------------------------------------------
291
+ function loadDesignSystem(yamlPath) {
292
+ const __dirname = dirname(fileURLToPath(import.meta.url));
293
+ const resolvedPath = yamlPath
294
+ ? resolve(yamlPath)
295
+ : resolve(__dirname, "..", "..", "design-system.yaml");
296
+ const raw = readFileSync(resolvedPath, "utf-8");
297
+ const doc = parseDocument(raw);
298
+ const ds = doc.toJSON();
299
+ return resolveComponentRefs(ds);
300
+ }
301
+ // ---------------------------------------------------------------------------
302
+ // MCP Server
303
+ // ---------------------------------------------------------------------------
304
+ function createServer(ds) {
305
+ const server = new McpServer({
306
+ name: "deepgram-styles",
307
+ version: ds.version ?? "0.0.0",
308
+ });
309
+ // ---- list_components ----
310
+ server.tool("list_components", "List all components in the Deepgram design system. Returns component names, titles, categories, tags, and counts of variants and examples. Use the optional category filter to narrow results.", { category: z.string().optional().describe("Filter by category (e.g. 'application-ui', 'marketing', 'documentation')") }, async ({ category }) => {
311
+ const entries = Object.entries(ds.components)
312
+ .filter(([, comp]) => !category || comp.metadata.category === category)
313
+ .map(([name, comp]) => ({
314
+ name,
315
+ title: comp.metadata.title,
316
+ category: comp.metadata.category,
317
+ section: comp.metadata.section,
318
+ subsection: comp.metadata.subsection,
319
+ tags: comp.metadata.tags ?? [],
320
+ variantCount: comp.variants ? Object.keys(comp.variants).length : 0,
321
+ exampleCount: collectAllExamples(comp).length,
322
+ }));
323
+ return {
324
+ content: [
325
+ {
326
+ type: "text",
327
+ text: JSON.stringify(entries, null, 2),
328
+ },
329
+ ],
330
+ };
331
+ });
332
+ // ---- get_component ----
333
+ server.tool("get_component", "Get full details for a specific Deepgram design system component. Returns BEM CSS classes, variant names, and rendered HTML examples you can copy-paste. Use list_components first to discover available component names.", { name: z.string().describe("Component key (e.g. 'btn', 'card', 'alert')") }, async ({ name }) => {
334
+ const component = ds.components[name];
335
+ if (!component) {
336
+ return {
337
+ content: [
338
+ {
339
+ type: "text",
340
+ text: `Component "${name}" not found. Use list_components to see available components.`,
341
+ },
342
+ ],
343
+ };
344
+ }
345
+ const bemClasses = extractBEMClasses(component, name);
346
+ const variants = component.variants
347
+ ? Object.entries(component.variants).map(([vKey, v]) => ({
348
+ name: vKey,
349
+ class: `dg-${name}--${vKey}`,
350
+ title: v.title,
351
+ description: v.description,
352
+ }))
353
+ : [];
354
+ const examples = collectAllExamples(component).map(({ example, variantKey }) => ({
355
+ title: example.title,
356
+ description: example.description,
357
+ variant: variantKey,
358
+ html: astToHTML(example.ast),
359
+ }));
360
+ const result = {
361
+ name,
362
+ title: component.metadata.title,
363
+ description: component.metadata.description,
364
+ category: component.metadata.category,
365
+ section: component.metadata.section,
366
+ subsection: component.metadata.subsection,
367
+ tags: component.metadata.tags ?? [],
368
+ tag: component.tag ?? "div",
369
+ bemClasses,
370
+ variants,
371
+ examples,
372
+ };
373
+ return {
374
+ content: [
375
+ {
376
+ type: "text",
377
+ text: JSON.stringify(result, null, 2),
378
+ },
379
+ ],
380
+ };
381
+ });
382
+ // ---- get_design_tokens ----
383
+ server.tool("get_design_tokens", "Get design tokens (colors, spacing, fonts, shadows, border-radius, CSS variables) from the Deepgram design system. Tokens define the visual language — use these values instead of hardcoded colors or sizes.", { group: z.string().optional().describe("Token group to return: 'colors', 'spacing', 'fonts', 'shadows', 'border-radius', 'variables'. Omit for all.") }, async ({ group }) => {
384
+ const tokens = ds.tokens;
385
+ if (group) {
386
+ const key = group;
387
+ if (!(key in tokens)) {
388
+ return {
389
+ content: [
390
+ {
391
+ type: "text",
392
+ text: `Token group "${group}" not found. Available groups: ${Object.keys(tokens).join(", ")}`,
393
+ },
394
+ ],
395
+ };
396
+ }
397
+ return {
398
+ content: [
399
+ {
400
+ type: "text",
401
+ text: JSON.stringify({ [key]: tokens[key] }, null, 2),
402
+ },
403
+ ],
404
+ };
405
+ }
406
+ return {
407
+ content: [
408
+ {
409
+ type: "text",
410
+ text: JSON.stringify(tokens, null, 2),
411
+ },
412
+ ],
413
+ };
414
+ });
415
+ // ---- search_components ----
416
+ server.tool("search_components", "Search for Deepgram design system components by keyword. Matches against component name, title, tags, description, category, section, and subsection. Returns matching components with metadata.", { query: z.string().describe("Search keyword (e.g. 'button', 'navigation', 'form', 'card')") }, async ({ query }) => {
417
+ const q = query.toLowerCase();
418
+ const matches = Object.entries(ds.components)
419
+ .filter(([name, comp]) => {
420
+ const searchable = [
421
+ name,
422
+ comp.metadata.title,
423
+ comp.metadata.description ?? "",
424
+ comp.metadata.category,
425
+ comp.metadata.section,
426
+ comp.metadata.subsection,
427
+ ...(comp.metadata.tags ?? []),
428
+ ]
429
+ .join(" ")
430
+ .toLowerCase();
431
+ return searchable.includes(q);
432
+ })
433
+ .map(([name, comp]) => ({
434
+ name,
435
+ title: comp.metadata.title,
436
+ category: comp.metadata.category,
437
+ section: comp.metadata.section,
438
+ tags: comp.metadata.tags ?? [],
439
+ description: comp.metadata.description,
440
+ variantCount: comp.variants ? Object.keys(comp.variants).length : 0,
441
+ exampleCount: collectAllExamples(comp).length,
442
+ }));
443
+ return {
444
+ content: [
445
+ {
446
+ type: "text",
447
+ text: matches.length > 0
448
+ ? JSON.stringify(matches, null, 2)
449
+ : `No components found matching "${query}". Try list_components to see all available components.`,
450
+ },
451
+ ],
452
+ };
453
+ });
454
+ return server;
455
+ }
456
+ // ---------------------------------------------------------------------------
457
+ // CLI entry point
458
+ // ---------------------------------------------------------------------------
459
+ async function main() {
460
+ // Parse --yaml flag
461
+ let yamlPath;
462
+ const args = process.argv.slice(2);
463
+ for (let i = 0; i < args.length; i++) {
464
+ if (args[i] === "--yaml" && args[i + 1]) {
465
+ yamlPath = args[i + 1];
466
+ i++;
467
+ }
468
+ }
469
+ const ds = loadDesignSystem(yamlPath);
470
+ const server = createServer(ds);
471
+ const transport = new StdioServerTransport();
472
+ await server.connect(transport);
473
+ }
474
+ main().catch((err) => {
475
+ console.error("Fatal:", err);
476
+ process.exit(1);
477
+ });
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@deepgram/styles",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "Tailwind-based design system and styles library for Deepgram design system and demos",
5
5
  "author": "Deepgram",
6
6
  "license": "ISC",
7
+ "type": "module",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "https://github.com/deepgram/dx-design.git",
@@ -19,6 +20,9 @@
19
20
  "ui-components",
20
21
  "frontend"
21
22
  ],
23
+ "bin": {
24
+ "mcp": "dist/mcp/server.js"
25
+ },
22
26
  "main": "dist/deepgram.css",
23
27
  "style": "dist/deepgram.css",
24
28
  "files": [
@@ -96,14 +100,19 @@
96
100
  "types": "./dist/react/index.d.ts",
97
101
  "import": "./dist/react/index.js",
98
102
  "default": "./dist/react/index.js"
103
+ },
104
+ "./mcp": {
105
+ "types": "./dist/mcp/server.d.ts",
106
+ "import": "./dist/mcp/server.js",
107
+ "default": "./dist/mcp/server.js"
99
108
  }
100
109
  },
101
110
  "publishConfig": {
102
111
  "access": "public"
103
112
  },
104
113
  "peerDependencies": {
105
- "tailwindcss": "^4.0.0",
106
- "react": "^18.0.0 || ^19.0.0"
114
+ "react": "^18.0.0 || ^19.0.0",
115
+ "tailwindcss": "^4.0.0"
107
116
  },
108
117
  "peerDependenciesMeta": {
109
118
  "tailwindcss": {
@@ -112,5 +121,10 @@
112
121
  "react": {
113
122
  "optional": true
114
123
  }
124
+ },
125
+ "dependencies": {
126
+ "@modelcontextprotocol/sdk": "^1.26.0",
127
+ "yaml": "^2.8.1",
128
+ "zod": "^4.3.6"
115
129
  }
116
130
  }
@@ -0,0 +1,724 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Server for @deepgram/styles
5
+ *
6
+ * Exposes the design system (tokens, components, examples) as MCP tools
7
+ * so AI agents can query BEM classes, rendered HTML examples, and tokens.
8
+ *
9
+ * Usage:
10
+ * npx -p @deepgram/styles mcp # uses bundled design-system.yaml
11
+ * node dist/mcp/server.js --yaml /path/to/design-system.yaml
12
+ */
13
+
14
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import { readFileSync } from "node:fs";
17
+ import { resolve, dirname } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { parseDocument } from "yaml";
20
+ import { z } from "zod";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Inline types (subset of tools/yaml-pipeline/src/schema/types.ts)
24
+ // ---------------------------------------------------------------------------
25
+
26
+ interface CSSRule {
27
+ apply?: string;
28
+ properties?: Record<string, string>;
29
+ }
30
+
31
+ type CSSRuleMap = Record<string, CSSRule>;
32
+
33
+ interface StyleState {
34
+ apply?: string;
35
+ properties?: Record<string, string>;
36
+ }
37
+
38
+ interface ComponentStyles extends StyleState {
39
+ hover?: StyleState;
40
+ focus?: StyleState;
41
+ "focus-visible"?: StyleState;
42
+ "focus-within"?: StyleState;
43
+ active?: StyleState;
44
+ disabled?: StyleState;
45
+ checked?: StyleState;
46
+ "first-child"?: StyleState;
47
+ "last-child"?: StyleState;
48
+ media?: Record<string, StyleState>;
49
+ }
50
+
51
+ interface VariantNode {
52
+ title?: string;
53
+ description?: string;
54
+ styles?: ComponentStyles;
55
+ css?: CSSRuleMap;
56
+ media?: Record<string, CSSRuleMap>;
57
+ examples?: ComponentExample[];
58
+ }
59
+
60
+ interface ComponentNode {
61
+ $ref?: string;
62
+ tag?: string;
63
+ styles?: ComponentStyles;
64
+ variants?: Record<string, VariantNode>;
65
+ components?: Record<string, ComponentNode>;
66
+ css?: CSSRuleMap;
67
+ media?: Record<string, CSSRuleMap>;
68
+ examples?: ComponentExample[];
69
+ "default-props"?: Record<string, string>;
70
+ "class-props"?: Record<string, string>;
71
+ polymorphic?: boolean;
72
+ }
73
+
74
+ interface ASTNode {
75
+ tag: string;
76
+ props?: Record<string, string | boolean | number>;
77
+ children?: (ASTNode | UseRefNode | string)[];
78
+ text?: string;
79
+ }
80
+
81
+ interface UseRefNode {
82
+ $ref: string;
83
+ props?: Record<string, string | boolean | number>;
84
+ children?: (ASTNode | UseRefNode | string)[];
85
+ text?: string;
86
+ }
87
+
88
+ interface ComponentExample {
89
+ title: string;
90
+ description?: string;
91
+ ast: ASTNode | UseRefNode;
92
+ }
93
+
94
+ interface ComponentMetadata {
95
+ title: string;
96
+ description?: string;
97
+ category: string;
98
+ section: string;
99
+ subsection: string;
100
+ tags?: string[];
101
+ }
102
+
103
+ interface ComponentDefinition extends ComponentNode {
104
+ metadata: ComponentMetadata;
105
+ tag?: string;
106
+ variables?: Record<string, string>;
107
+ examples: ComponentExample[];
108
+ }
109
+
110
+ interface ColorToken {
111
+ value: string;
112
+ variable?: string;
113
+ fallback?: string;
114
+ }
115
+
116
+ interface FontToken {
117
+ family: string[];
118
+ "tailwind-key": string;
119
+ }
120
+
121
+ interface DesignTokens {
122
+ variables: Record<string, string>;
123
+ colors: {
124
+ brand: Record<string, ColorToken>;
125
+ background: Record<string, string>;
126
+ border: Record<string, string>;
127
+ text: Record<string, string>;
128
+ status: Record<string, string>;
129
+ gradient: Record<string, string>;
130
+ };
131
+ fonts: Record<string, FontToken>;
132
+ spacing: Record<string, string>;
133
+ "border-radius": Record<string, string>;
134
+ shadows: Record<string, string>;
135
+ }
136
+
137
+ interface DesignSystem {
138
+ version: string;
139
+ tokens: DesignTokens;
140
+ components: Record<string, ComponentDefinition>;
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Type guard
145
+ // ---------------------------------------------------------------------------
146
+
147
+ function isUseRefNode(node: unknown): node is UseRefNode {
148
+ return (
149
+ typeof node === "object" &&
150
+ node !== null &&
151
+ "$ref" in node &&
152
+ typeof (node as UseRefNode).$ref === "string" &&
153
+ !("tag" in node)
154
+ );
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Inline AST → HTML renderer (from generators/html.ts)
159
+ // ---------------------------------------------------------------------------
160
+
161
+ const SELF_CLOSING = new Set(["img", "br", "hr", "input", "meta", "link"]);
162
+
163
+ function formatHTMLProps(
164
+ props: Record<string, string | boolean | number>
165
+ ): string {
166
+ return Object.entries(props)
167
+ .map(([key, value]) => {
168
+ if (typeof value === "boolean") return value ? key : "";
169
+ const attrName = key === "className" ? "class" : key;
170
+ return `${attrName}="${value}"`;
171
+ })
172
+ .filter(Boolean)
173
+ .join(" ");
174
+ }
175
+
176
+ function indentStr(code: string, level = 1): string {
177
+ const spaces = " ".repeat(level);
178
+ return code
179
+ .split("\n")
180
+ .map((line) => (line ? spaces + line : line))
181
+ .join("\n");
182
+ }
183
+
184
+ function astToHTML(node: ASTNode, depth = 0): string {
185
+ const { tag, props = {}, children = [], text } = node;
186
+ const propsStr =
187
+ Object.keys(props).length > 0 ? " " + formatHTMLProps(props) : "";
188
+
189
+ if (SELF_CLOSING.has(tag) && !text && children.length === 0) {
190
+ return `<${tag}${propsStr} />`;
191
+ }
192
+
193
+ if (text) {
194
+ return `<${tag}${propsStr}>${text}</${tag}>`;
195
+ }
196
+
197
+ if (children.length > 0) {
198
+ const childrenStr = children
199
+ .map((child) => {
200
+ if (typeof child === "string") return child;
201
+ if ("tag" in child) return astToHTML(child, depth + 1);
202
+ return "";
203
+ })
204
+ .filter(Boolean)
205
+ .join("\n");
206
+
207
+ if (
208
+ children.length > 1 ||
209
+ (children[0] && typeof children[0] !== "string")
210
+ ) {
211
+ return `<${tag}${propsStr}>\n${indentStr(childrenStr, 1)}\n</${tag}>`;
212
+ }
213
+ return `<${tag}${propsStr}>${childrenStr}</${tag}>`;
214
+ }
215
+
216
+ return `<${tag}${propsStr}></${tag}>`;
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Inline $ref resolver (from parser/resolve-refs.ts)
221
+ // ---------------------------------------------------------------------------
222
+
223
+ function resolveComponentRefs(ds: DesignSystem): DesignSystem {
224
+ const resolved = { ...ds, components: { ...ds.components } };
225
+ for (const [key, component] of Object.entries(resolved.components)) {
226
+ resolved.components[key] = resolveComponentExamples(ds, component);
227
+ }
228
+ return resolved;
229
+ }
230
+
231
+ function resolveComponentExamples(
232
+ ds: DesignSystem,
233
+ component: ComponentDefinition
234
+ ): ComponentDefinition {
235
+ const result = { ...component };
236
+
237
+ if (result.examples) {
238
+ result.examples = result.examples.map((ex) => ({
239
+ ...ex,
240
+ ast: resolveNode(ds, ex.ast),
241
+ }));
242
+ }
243
+
244
+ if (result.variants) {
245
+ result.variants = { ...result.variants };
246
+ for (const [vKey, variant] of Object.entries(result.variants)) {
247
+ if (variant.examples) {
248
+ result.variants[vKey] = {
249
+ ...variant,
250
+ examples: variant.examples.map((ex) => ({
251
+ ...ex,
252
+ ast: resolveNode(ds, ex.ast),
253
+ })),
254
+ };
255
+ }
256
+ }
257
+ }
258
+
259
+ if (result.components) {
260
+ result.components = { ...result.components };
261
+ for (const [cKey, child] of Object.entries(result.components)) {
262
+ if (child.examples) {
263
+ result.components[cKey] = {
264
+ ...child,
265
+ examples: child.examples.map((ex) => ({
266
+ ...ex,
267
+ ast: resolveNode(ds, ex.ast),
268
+ })),
269
+ };
270
+ }
271
+ }
272
+ }
273
+
274
+ return result;
275
+ }
276
+
277
+ function resolveNode(ds: DesignSystem, node: ASTNode | UseRefNode): ASTNode {
278
+ if (isUseRefNode(node)) {
279
+ return resolveUseRef(ds, node);
280
+ }
281
+
282
+ if (!node.children) return node;
283
+
284
+ const resolvedChildren = node.children.map((child) => {
285
+ if (typeof child === "string") return child;
286
+ return resolveNode(ds, child);
287
+ });
288
+
289
+ return { ...node, children: resolvedChildren };
290
+ }
291
+
292
+ function resolveUseRef(ds: DesignSystem, ref: UseRefNode): ASTNode {
293
+ const segments = ref.$ref.split("/");
294
+ const componentKey = segments[0];
295
+
296
+ const component = ds.components[componentKey];
297
+ if (!component) {
298
+ return { tag: "div", props: { class: `dg-${componentKey}` }, text: `[unresolved: ${ref.$ref}]` };
299
+ }
300
+
301
+ const blockClass = `dg-${componentKey}`;
302
+ let tag = component.tag ?? "div";
303
+ const classes: string[] = [blockClass];
304
+
305
+ if (segments.length > 1) {
306
+ const pathSegment = segments.slice(1).join("/");
307
+ const resolved = resolvePathSegment(
308
+ ds,
309
+ component,
310
+ componentKey,
311
+ blockClass,
312
+ pathSegment
313
+ );
314
+ tag = resolved.tag;
315
+ classes.length = 0;
316
+ classes.push(...resolved.classes);
317
+ }
318
+
319
+ const mergedProps: Record<string, string | boolean | number> = {};
320
+ if (classes.length > 0) {
321
+ mergedProps.class = classes.join(" ");
322
+ }
323
+ if (ref.props) {
324
+ for (const [key, value] of Object.entries(ref.props)) {
325
+ if (key === "class" && typeof value === "string") {
326
+ mergedProps.class = mergedProps.class
327
+ ? `${mergedProps.class} ${value}`
328
+ : value;
329
+ } else {
330
+ mergedProps[key] = value;
331
+ }
332
+ }
333
+ }
334
+
335
+ let resolvedChildren: (ASTNode | string)[] | undefined;
336
+ if (ref.children) {
337
+ resolvedChildren = ref.children.map((child) => {
338
+ if (typeof child === "string") return child;
339
+ return resolveNode(ds, child);
340
+ });
341
+ }
342
+
343
+ const result: ASTNode = { tag, props: mergedProps };
344
+ if (resolvedChildren) result.children = resolvedChildren;
345
+ if (ref.text) result.text = ref.text;
346
+
347
+ return result;
348
+ }
349
+
350
+ interface ResolvedPath {
351
+ tag: string;
352
+ classes: string[];
353
+ }
354
+
355
+ function resolvePathSegment(
356
+ ds: DesignSystem,
357
+ component: ComponentDefinition,
358
+ componentKey: string,
359
+ blockClass: string,
360
+ pathSegment: string
361
+ ): ResolvedPath {
362
+ const parts = pathSegment.split("+");
363
+
364
+ if (parts.length > 1) {
365
+ for (const part of parts) {
366
+ if (!component.variants?.[part]) {
367
+ return {
368
+ tag: component.tag ?? "div",
369
+ classes: [blockClass, ...parts.map((p) => `${blockClass}--${p}`)],
370
+ };
371
+ }
372
+ }
373
+ return {
374
+ tag: component.tag ?? "div",
375
+ classes: [blockClass, ...parts.map((p) => `${blockClass}--${p}`)],
376
+ };
377
+ }
378
+
379
+ const name = parts[0];
380
+
381
+ if (component.variants?.[name]) {
382
+ return {
383
+ tag: component.tag ?? "div",
384
+ classes: [blockClass, `${blockClass}--${name}`],
385
+ };
386
+ }
387
+
388
+ if (component.components?.[name]) {
389
+ const child = component.components[name];
390
+ const elementClass = `dg-${componentKey}__${name}`;
391
+
392
+ if (child.$ref) {
393
+ const refComponent = ds.components[child.$ref];
394
+ return {
395
+ tag: child.tag ?? refComponent?.tag ?? "div",
396
+ classes: [`dg-${child.$ref}`, elementClass],
397
+ };
398
+ }
399
+
400
+ return { tag: child.tag ?? "div", classes: [elementClass] };
401
+ }
402
+
403
+ // Graceful fallback for unknown segments
404
+ return {
405
+ tag: component.tag ?? "div",
406
+ classes: [blockClass, `${blockClass}--${name}`],
407
+ };
408
+ }
409
+
410
+ // ---------------------------------------------------------------------------
411
+ // Inline example collector (from generators/collect-examples.ts)
412
+ // ---------------------------------------------------------------------------
413
+
414
+ interface CollectedExample {
415
+ example: ComponentExample;
416
+ variantKey?: string;
417
+ }
418
+
419
+ function collectAllExamples(
420
+ component: ComponentDefinition
421
+ ): CollectedExample[] {
422
+ const results: CollectedExample[] = [];
423
+
424
+ for (const example of component.examples) {
425
+ results.push({ example });
426
+ }
427
+
428
+ if (component.variants) {
429
+ for (const [vKey, variant] of Object.entries(component.variants)) {
430
+ if (variant.examples) {
431
+ for (const example of variant.examples) {
432
+ results.push({ example, variantKey: vKey });
433
+ }
434
+ }
435
+ }
436
+ }
437
+
438
+ return results;
439
+ }
440
+
441
+ // ---------------------------------------------------------------------------
442
+ // CSS class extractor
443
+ // ---------------------------------------------------------------------------
444
+
445
+ function extractBEMClasses(component: ComponentDefinition, name: string): string[] {
446
+ const classes = new Set<string>();
447
+ const blockClass = `dg-${name}`;
448
+ classes.add(blockClass);
449
+
450
+ // From css rules
451
+ if (component.css) {
452
+ for (const selector of Object.keys(component.css)) {
453
+ const matches = selector.match(/\.dg-[\w-]+(?:__[\w-]+)?(?:--[\w-]+)?/g);
454
+ if (matches) matches.forEach((m) => classes.add(m.replace(/^\./, "")));
455
+ }
456
+ }
457
+
458
+ // From variants
459
+ if (component.variants) {
460
+ for (const vKey of Object.keys(component.variants)) {
461
+ classes.add(`${blockClass}--${vKey}`);
462
+ const variant = component.variants[vKey];
463
+ if (variant.css) {
464
+ for (const selector of Object.keys(variant.css)) {
465
+ const matches = selector.match(/\.dg-[\w-]+(?:__[\w-]+)?(?:--[\w-]+)?/g);
466
+ if (matches) matches.forEach((m) => classes.add(m.replace(/^\./, "")));
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ // From elements/components
473
+ if (component.components) {
474
+ for (const cKey of Object.keys(component.components)) {
475
+ classes.add(`${blockClass}__${cKey}`);
476
+ }
477
+ }
478
+
479
+ // From elements (legacy css-based)
480
+ if ((component as any).elements) {
481
+ for (const eKey of Object.keys((component as any).elements)) {
482
+ classes.add(`${blockClass}__${eKey}`);
483
+ }
484
+ }
485
+
486
+ return [...classes].sort();
487
+ }
488
+
489
+ // ---------------------------------------------------------------------------
490
+ // Load YAML
491
+ // ---------------------------------------------------------------------------
492
+
493
+ function loadDesignSystem(yamlPath?: string): DesignSystem {
494
+ const __dirname = dirname(fileURLToPath(import.meta.url));
495
+ const resolvedPath = yamlPath
496
+ ? resolve(yamlPath)
497
+ : resolve(__dirname, "..", "..", "design-system.yaml");
498
+
499
+ const raw = readFileSync(resolvedPath, "utf-8");
500
+ const doc = parseDocument(raw);
501
+ const ds = doc.toJSON() as DesignSystem;
502
+
503
+ return resolveComponentRefs(ds);
504
+ }
505
+
506
+ // ---------------------------------------------------------------------------
507
+ // MCP Server
508
+ // ---------------------------------------------------------------------------
509
+
510
+ function createServer(ds: DesignSystem): McpServer {
511
+ const server = new McpServer({
512
+ name: "deepgram-styles",
513
+ version: ds.version ?? "0.0.0",
514
+ });
515
+
516
+ // ---- list_components ----
517
+ server.tool(
518
+ "list_components",
519
+ "List all components in the Deepgram design system. Returns component names, titles, categories, tags, and counts of variants and examples. Use the optional category filter to narrow results.",
520
+ { category: z.string().optional().describe("Filter by category (e.g. 'application-ui', 'marketing', 'documentation')") },
521
+ async ({ category }) => {
522
+ const entries = Object.entries(ds.components)
523
+ .filter(([, comp]) => !category || comp.metadata.category === category)
524
+ .map(([name, comp]) => ({
525
+ name,
526
+ title: comp.metadata.title,
527
+ category: comp.metadata.category,
528
+ section: comp.metadata.section,
529
+ subsection: comp.metadata.subsection,
530
+ tags: comp.metadata.tags ?? [],
531
+ variantCount: comp.variants ? Object.keys(comp.variants).length : 0,
532
+ exampleCount: collectAllExamples(comp).length,
533
+ }));
534
+
535
+ return {
536
+ content: [
537
+ {
538
+ type: "text" as const,
539
+ text: JSON.stringify(entries, null, 2),
540
+ },
541
+ ],
542
+ };
543
+ }
544
+ );
545
+
546
+ // ---- get_component ----
547
+ server.tool(
548
+ "get_component",
549
+ "Get full details for a specific Deepgram design system component. Returns BEM CSS classes, variant names, and rendered HTML examples you can copy-paste. Use list_components first to discover available component names.",
550
+ { name: z.string().describe("Component key (e.g. 'btn', 'card', 'alert')") },
551
+ async ({ name }) => {
552
+ const component = ds.components[name];
553
+ if (!component) {
554
+ return {
555
+ content: [
556
+ {
557
+ type: "text" as const,
558
+ text: `Component "${name}" not found. Use list_components to see available components.`,
559
+ },
560
+ ],
561
+ };
562
+ }
563
+
564
+ const bemClasses = extractBEMClasses(component, name);
565
+ const variants = component.variants
566
+ ? Object.entries(component.variants).map(([vKey, v]) => ({
567
+ name: vKey,
568
+ class: `dg-${name}--${vKey}`,
569
+ title: v.title,
570
+ description: v.description,
571
+ }))
572
+ : [];
573
+
574
+ const examples = collectAllExamples(component).map(
575
+ ({ example, variantKey }) => ({
576
+ title: example.title,
577
+ description: example.description,
578
+ variant: variantKey,
579
+ html: astToHTML(example.ast as ASTNode),
580
+ })
581
+ );
582
+
583
+ const result = {
584
+ name,
585
+ title: component.metadata.title,
586
+ description: component.metadata.description,
587
+ category: component.metadata.category,
588
+ section: component.metadata.section,
589
+ subsection: component.metadata.subsection,
590
+ tags: component.metadata.tags ?? [],
591
+ tag: component.tag ?? "div",
592
+ bemClasses,
593
+ variants,
594
+ examples,
595
+ };
596
+
597
+ return {
598
+ content: [
599
+ {
600
+ type: "text" as const,
601
+ text: JSON.stringify(result, null, 2),
602
+ },
603
+ ],
604
+ };
605
+ }
606
+ );
607
+
608
+ // ---- get_design_tokens ----
609
+ server.tool(
610
+ "get_design_tokens",
611
+ "Get design tokens (colors, spacing, fonts, shadows, border-radius, CSS variables) from the Deepgram design system. Tokens define the visual language — use these values instead of hardcoded colors or sizes.",
612
+ { group: z.string().optional().describe("Token group to return: 'colors', 'spacing', 'fonts', 'shadows', 'border-radius', 'variables'. Omit for all.") },
613
+ async ({ group }) => {
614
+ const tokens = ds.tokens;
615
+
616
+ if (group) {
617
+ const key = group as keyof DesignTokens;
618
+ if (!(key in tokens)) {
619
+ return {
620
+ content: [
621
+ {
622
+ type: "text" as const,
623
+ text: `Token group "${group}" not found. Available groups: ${Object.keys(tokens).join(", ")}`,
624
+ },
625
+ ],
626
+ };
627
+ }
628
+ return {
629
+ content: [
630
+ {
631
+ type: "text" as const,
632
+ text: JSON.stringify({ [key]: tokens[key] }, null, 2),
633
+ },
634
+ ],
635
+ };
636
+ }
637
+
638
+ return {
639
+ content: [
640
+ {
641
+ type: "text" as const,
642
+ text: JSON.stringify(tokens, null, 2),
643
+ },
644
+ ],
645
+ };
646
+ }
647
+ );
648
+
649
+ // ---- search_components ----
650
+ server.tool(
651
+ "search_components",
652
+ "Search for Deepgram design system components by keyword. Matches against component name, title, tags, description, category, section, and subsection. Returns matching components with metadata.",
653
+ { query: z.string().describe("Search keyword (e.g. 'button', 'navigation', 'form', 'card')") },
654
+ async ({ query }) => {
655
+ const q = query.toLowerCase();
656
+
657
+ const matches = Object.entries(ds.components)
658
+ .filter(([name, comp]) => {
659
+ const searchable = [
660
+ name,
661
+ comp.metadata.title,
662
+ comp.metadata.description ?? "",
663
+ comp.metadata.category,
664
+ comp.metadata.section,
665
+ comp.metadata.subsection,
666
+ ...(comp.metadata.tags ?? []),
667
+ ]
668
+ .join(" ")
669
+ .toLowerCase();
670
+ return searchable.includes(q);
671
+ })
672
+ .map(([name, comp]) => ({
673
+ name,
674
+ title: comp.metadata.title,
675
+ category: comp.metadata.category,
676
+ section: comp.metadata.section,
677
+ tags: comp.metadata.tags ?? [],
678
+ description: comp.metadata.description,
679
+ variantCount: comp.variants ? Object.keys(comp.variants).length : 0,
680
+ exampleCount: collectAllExamples(comp).length,
681
+ }));
682
+
683
+ return {
684
+ content: [
685
+ {
686
+ type: "text" as const,
687
+ text:
688
+ matches.length > 0
689
+ ? JSON.stringify(matches, null, 2)
690
+ : `No components found matching "${query}". Try list_components to see all available components.`,
691
+ },
692
+ ],
693
+ };
694
+ }
695
+ );
696
+
697
+ return server;
698
+ }
699
+
700
+ // ---------------------------------------------------------------------------
701
+ // CLI entry point
702
+ // ---------------------------------------------------------------------------
703
+
704
+ async function main(): Promise<void> {
705
+ // Parse --yaml flag
706
+ let yamlPath: string | undefined;
707
+ const args = process.argv.slice(2);
708
+ for (let i = 0; i < args.length; i++) {
709
+ if (args[i] === "--yaml" && args[i + 1]) {
710
+ yamlPath = args[i + 1];
711
+ i++;
712
+ }
713
+ }
714
+
715
+ const ds = loadDesignSystem(yamlPath);
716
+ const server = createServer(ds);
717
+ const transport = new StdioServerTransport();
718
+ await server.connect(transport);
719
+ }
720
+
721
+ main().catch((err) => {
722
+ console.error("Fatal:", err);
723
+ process.exit(1);
724
+ });