@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 +62 -0
- package/design-system.yaml +7 -0
- package/dist/mcp/server.d.ts +12 -0
- package/dist/mcp/server.js +477 -0
- package/package.json +17 -3
- package/src/mcp/server.ts +724 -0
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
|
---
|
package/design-system.yaml
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
106
|
-
"
|
|
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
|
+
});
|