@pudge-ui/mcp-server 0.1.0
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 +63 -0
- package/dist/__tests__/catalog.test.d.ts +1 -0
- package/dist/__tests__/catalog.test.js +50 -0
- package/dist/__tests__/search.test.d.ts +1 -0
- package/dist/__tests__/search.test.js +44 -0
- package/dist/__tests__/validation.test.d.ts +1 -0
- package/dist/__tests__/validation.test.js +34 -0
- package/dist/assembler.d.ts +6 -0
- package/dist/assembler.js +95 -0
- package/dist/catalog.d.ts +22 -0
- package/dist/catalog.js +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +33 -0
- package/dist/search.d.ts +5 -0
- package/dist/search.js +21 -0
- package/dist/tools.d.ts +105 -0
- package/dist/tools.js +129 -0
- package/package.json +55 -0
- package/spec/components/_index.yaml +822 -0
- package/spec/components/buttons/clear-button.md +89 -0
- package/spec/components/buttons/fn-grid.md +104 -0
- package/spec/components/buttons/gel-button.md +125 -0
- package/spec/components/buttons/icon-button.md +108 -0
- package/spec/components/buttons/keypad-button.md +123 -0
- package/spec/components/buttons/push-button.md +139 -0
- package/spec/components/buttons/rec-button.md +105 -0
- package/spec/components/buttons/rubber-button.md +100 -0
- package/spec/components/buttons/segmented-control.md +95 -0
- package/spec/components/data/assembled-panel.md +135 -0
- package/spec/components/data/data-table.md +116 -0
- package/spec/components/data/film-strip.md +110 -0
- package/spec/components/data/media-grid.md +98 -0
- package/spec/components/dials/click-wheel.md +115 -0
- package/spec/components/dials/cylindrical-horizontal.md +130 -0
- package/spec/components/dials/cylindrical-scroll.md +141 -0
- package/spec/components/dials/cylindrical-vertical.md +100 -0
- package/spec/components/dials/mode-dial.md +123 -0
- package/spec/components/dials/radial-knob.md +150 -0
- package/spec/components/dials/rotary-encoder.md +118 -0
- package/spec/components/forms/color-picker.md +99 -0
- package/spec/components/forms/file-input.md +105 -0
- package/spec/components/forms/search-bar.md +96 -0
- package/spec/components/forms/select.md +143 -0
- package/spec/components/forms/text-input.md +114 -0
- package/spec/components/forms/textarea.md +85 -0
- package/spec/components/indicators/accordion.md +137 -0
- package/spec/components/indicators/badges.md +87 -0
- package/spec/components/indicators/chips.md +93 -0
- package/spec/components/indicators/led-dots.md +103 -0
- package/spec/components/indicators/mode-badge.md +97 -0
- package/spec/components/indicators/profile-badge.md +99 -0
- package/spec/components/indicators/skeleton.md +94 -0
- package/spec/components/indicators/spinners.md +95 -0
- package/spec/components/indicators/status-chips.md +85 -0
- package/spec/components/indicators/transport-controls.md +114 -0
- package/spec/components/meters/battery-icon.md +104 -0
- package/spec/components/meters/eq-bars.md +93 -0
- package/spec/components/meters/ev-meter.md +96 -0
- package/spec/components/meters/exposure-scale.md +110 -0
- package/spec/components/meters/gauge-full.md +102 -0
- package/spec/components/meters/gauge-semi.md +113 -0
- package/spec/components/meters/histogram.md +70 -0
- package/spec/components/meters/level-indicator.md +95 -0
- package/spec/components/meters/oscilloscope.md +83 -0
- package/spec/components/meters/progress-bar.md +84 -0
- package/spec/components/meters/signal-bars.md +80 -0
- package/spec/components/meters/signal-meter.md +84 -0
- package/spec/components/meters/vu-meter.md +88 -0
- package/spec/components/meters/waveform.md +70 -0
- package/spec/components/navigation/breadcrumbs.md +94 -0
- package/spec/components/navigation/context-menu.md +94 -0
- package/spec/components/navigation/d-pad.md +121 -0
- package/spec/components/navigation/drawer.md +103 -0
- package/spec/components/navigation/menu-grid.md +113 -0
- package/spec/components/navigation/menu-list.md +134 -0
- package/spec/components/navigation/pagination.md +100 -0
- package/spec/components/navigation/rack-panel.md +124 -0
- package/spec/components/navigation/scrollbar.md +97 -0
- package/spec/components/navigation/status-bar.md +117 -0
- package/spec/components/navigation/tab-bar.md +104 -0
- package/spec/components/overlays/chassis-panel.md +94 -0
- package/spec/components/overlays/device-bezel.md +83 -0
- package/spec/components/overlays/dialog.md +100 -0
- package/spec/components/overlays/focus-brackets.md +124 -0
- package/spec/components/overlays/grid-overlay.md +93 -0
- package/spec/components/overlays/modal.md +89 -0
- package/spec/components/overlays/panel.md +114 -0
- package/spec/components/overlays/plastic-card.md +92 -0
- package/spec/components/overlays/popover.md +75 -0
- package/spec/components/overlays/toast.md +93 -0
- package/spec/components/overlays/tooltip.md +85 -0
- package/spec/components/readouts/camera-readout.md +123 -0
- package/spec/components/readouts/dot-matrix.md +88 -0
- package/spec/components/readouts/lcd-readout.md +116 -0
- package/spec/components/readouts/resource-monitor.md +98 -0
- package/spec/components/readouts/seven-segment.md +110 -0
- package/spec/components/readouts/signal-display.md +93 -0
- package/spec/components/readouts/timecode-display.md +94 -0
- package/spec/components/sliders/crossfader.md +102 -0
- package/spec/components/sliders/dual-range.md +97 -0
- package/spec/components/sliders/range-fader.md +100 -0
- package/spec/components/sliders/scrubber.md +104 -0
- package/spec/components/sliders/stepper.md +106 -0
- package/spec/components/sliders/vertical-fader.md +116 -0
- package/spec/components/sliders/volume-slider.md +107 -0
- package/spec/components/toggles/dip-switch.md +100 -0
- package/spec/components/toggles/led-checkbox.md +108 -0
- package/spec/components/toggles/power-toggle.md +93 -0
- package/spec/components/toggles/radio-button.md +106 -0
- package/spec/components/toggles/rocker-switch.md +92 -0
- package/spec/components/toggles/slide-switch.md +121 -0
- package/spec/components/toggles/toggle-switch.md +135 -0
- package/spec/compositions/audio-mixer-strip.md +62 -0
- package/spec/compositions/camera-viewfinder.md +66 -0
- package/spec/compositions/phone-interface.md +66 -0
- package/spec/foundation/accessibility.md +33 -0
- package/spec/foundation/canvas.md +20 -0
- package/spec/foundation/depth-model.md +82 -0
- package/spec/foundation/layout.md +33 -0
- package/spec/foundation/materials.md +68 -0
- package/spec/foundation/naming.md +33 -0
- package/spec/foundation/philosophy.md +27 -0
- package/spec/foundation/theme.md +39 -0
- package/spec/foundation/tokens.md +148 -0
- package/spec/guides/extension.md +189 -0
- package/spec/guides/for-llms.md +129 -0
- package/spec/guides/prompt-templates.md +143 -0
- package/spec/spec/components/_index.yaml +822 -0
- package/spec/spec/components/buttons/clear-button.md +89 -0
- package/spec/spec/components/buttons/fn-grid.md +104 -0
- package/spec/spec/components/buttons/gel-button.md +125 -0
- package/spec/spec/components/buttons/icon-button.md +108 -0
- package/spec/spec/components/buttons/keypad-button.md +123 -0
- package/spec/spec/components/buttons/push-button.md +139 -0
- package/spec/spec/components/buttons/rec-button.md +105 -0
- package/spec/spec/components/buttons/rubber-button.md +100 -0
- package/spec/spec/components/buttons/segmented-control.md +95 -0
- package/spec/spec/components/data/assembled-panel.md +135 -0
- package/spec/spec/components/data/data-table.md +116 -0
- package/spec/spec/components/data/film-strip.md +110 -0
- package/spec/spec/components/data/media-grid.md +98 -0
- package/spec/spec/components/dials/click-wheel.md +115 -0
- package/spec/spec/components/dials/cylindrical-horizontal.md +130 -0
- package/spec/spec/components/dials/cylindrical-scroll.md +141 -0
- package/spec/spec/components/dials/cylindrical-vertical.md +100 -0
- package/spec/spec/components/dials/mode-dial.md +123 -0
- package/spec/spec/components/dials/radial-knob.md +150 -0
- package/spec/spec/components/dials/rotary-encoder.md +118 -0
- package/spec/spec/components/forms/color-picker.md +99 -0
- package/spec/spec/components/forms/file-input.md +105 -0
- package/spec/spec/components/forms/search-bar.md +96 -0
- package/spec/spec/components/forms/select.md +143 -0
- package/spec/spec/components/forms/text-input.md +114 -0
- package/spec/spec/components/forms/textarea.md +85 -0
- package/spec/spec/components/indicators/accordion.md +137 -0
- package/spec/spec/components/indicators/badges.md +87 -0
- package/spec/spec/components/indicators/chips.md +93 -0
- package/spec/spec/components/indicators/led-dots.md +103 -0
- package/spec/spec/components/indicators/mode-badge.md +97 -0
- package/spec/spec/components/indicators/profile-badge.md +99 -0
- package/spec/spec/components/indicators/skeleton.md +94 -0
- package/spec/spec/components/indicators/spinners.md +95 -0
- package/spec/spec/components/indicators/status-chips.md +85 -0
- package/spec/spec/components/indicators/transport-controls.md +114 -0
- package/spec/spec/components/meters/battery-icon.md +104 -0
- package/spec/spec/components/meters/eq-bars.md +93 -0
- package/spec/spec/components/meters/ev-meter.md +96 -0
- package/spec/spec/components/meters/exposure-scale.md +110 -0
- package/spec/spec/components/meters/gauge-full.md +102 -0
- package/spec/spec/components/meters/gauge-semi.md +113 -0
- package/spec/spec/components/meters/histogram.md +70 -0
- package/spec/spec/components/meters/level-indicator.md +95 -0
- package/spec/spec/components/meters/oscilloscope.md +83 -0
- package/spec/spec/components/meters/progress-bar.md +84 -0
- package/spec/spec/components/meters/signal-bars.md +80 -0
- package/spec/spec/components/meters/signal-meter.md +84 -0
- package/spec/spec/components/meters/vu-meter.md +88 -0
- package/spec/spec/components/meters/waveform.md +70 -0
- package/spec/spec/components/navigation/breadcrumbs.md +94 -0
- package/spec/spec/components/navigation/context-menu.md +94 -0
- package/spec/spec/components/navigation/d-pad.md +121 -0
- package/spec/spec/components/navigation/drawer.md +103 -0
- package/spec/spec/components/navigation/menu-grid.md +113 -0
- package/spec/spec/components/navigation/menu-list.md +134 -0
- package/spec/spec/components/navigation/pagination.md +100 -0
- package/spec/spec/components/navigation/rack-panel.md +124 -0
- package/spec/spec/components/navigation/scrollbar.md +97 -0
- package/spec/spec/components/navigation/status-bar.md +117 -0
- package/spec/spec/components/navigation/tab-bar.md +104 -0
- package/spec/spec/components/overlays/chassis-panel.md +94 -0
- package/spec/spec/components/overlays/device-bezel.md +83 -0
- package/spec/spec/components/overlays/dialog.md +100 -0
- package/spec/spec/components/overlays/focus-brackets.md +124 -0
- package/spec/spec/components/overlays/grid-overlay.md +93 -0
- package/spec/spec/components/overlays/modal.md +89 -0
- package/spec/spec/components/overlays/panel.md +114 -0
- package/spec/spec/components/overlays/plastic-card.md +92 -0
- package/spec/spec/components/overlays/popover.md +75 -0
- package/spec/spec/components/overlays/toast.md +93 -0
- package/spec/spec/components/overlays/tooltip.md +85 -0
- package/spec/spec/components/readouts/camera-readout.md +123 -0
- package/spec/spec/components/readouts/dot-matrix.md +88 -0
- package/spec/spec/components/readouts/lcd-readout.md +116 -0
- package/spec/spec/components/readouts/resource-monitor.md +98 -0
- package/spec/spec/components/readouts/seven-segment.md +110 -0
- package/spec/spec/components/readouts/signal-display.md +93 -0
- package/spec/spec/components/readouts/timecode-display.md +94 -0
- package/spec/spec/components/sliders/crossfader.md +102 -0
- package/spec/spec/components/sliders/dual-range.md +97 -0
- package/spec/spec/components/sliders/range-fader.md +100 -0
- package/spec/spec/components/sliders/scrubber.md +104 -0
- package/spec/spec/components/sliders/stepper.md +106 -0
- package/spec/spec/components/sliders/vertical-fader.md +116 -0
- package/spec/spec/components/sliders/volume-slider.md +107 -0
- package/spec/spec/components/toggles/dip-switch.md +100 -0
- package/spec/spec/components/toggles/led-checkbox.md +108 -0
- package/spec/spec/components/toggles/power-toggle.md +93 -0
- package/spec/spec/components/toggles/radio-button.md +106 -0
- package/spec/spec/components/toggles/rocker-switch.md +92 -0
- package/spec/spec/components/toggles/slide-switch.md +121 -0
- package/spec/spec/components/toggles/toggle-switch.md +135 -0
- package/spec/spec/compositions/audio-mixer-strip.md +62 -0
- package/spec/spec/compositions/camera-viewfinder.md +66 -0
- package/spec/spec/compositions/phone-interface.md +66 -0
- package/spec/spec/foundation/accessibility.md +33 -0
- package/spec/spec/foundation/canvas.md +20 -0
- package/spec/spec/foundation/depth-model.md +82 -0
- package/spec/spec/foundation/layout.md +33 -0
- package/spec/spec/foundation/materials.md +68 -0
- package/spec/spec/foundation/naming.md +33 -0
- package/spec/spec/foundation/philosophy.md +27 -0
- package/spec/spec/foundation/theme.md +39 -0
- package/spec/spec/foundation/tokens.md +148 -0
- package/spec/spec/guides/extension.md +189 -0
- package/spec/spec/guides/for-llms.md +129 -0
- package/spec/spec/guides/prompt-templates.md +143 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @pudge-ui/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for the pudge-ui design system. Lets coding agents query component specs programmatically.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your MCP client config (e.g. Claude Desktop, Claude Code):
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"pudge-ui": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@pudge-ui/mcp-server"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For local development:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"pudge-ui": {
|
|
26
|
+
"command": "npx",
|
|
27
|
+
"args": ["tsx", "src/index.ts"],
|
|
28
|
+
"cwd": "/path/to/pudge-ui/packages/mcp-server"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Available Tools
|
|
35
|
+
|
|
36
|
+
| Tool | Description |
|
|
37
|
+
| ------------------- | ------------------------------------------------------------ |
|
|
38
|
+
| `list_components` | List all components, optionally filtered by category |
|
|
39
|
+
| `search_components` | Fuzzy search components by name, description, or tags |
|
|
40
|
+
| `get_foundation` | Get foundation spec docs (core 5 or extended 9) |
|
|
41
|
+
| `get_component` | Get full spec for a single component (foundation + spec) |
|
|
42
|
+
| `get_components` | Get specs for multiple components (foundation included once) |
|
|
43
|
+
| `get_composition` | Get a composition spec with all referenced component specs |
|
|
44
|
+
|
|
45
|
+
## Examples
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
list_components({ category: "buttons" })
|
|
49
|
+
search_components({ query: "volume slider audio" })
|
|
50
|
+
get_component({ name: "push-button" })
|
|
51
|
+
get_components({ names: ["rotary-encoder", "vu-meter", "vertical-fader"] })
|
|
52
|
+
get_composition({ name: "audio-mixer-strip" })
|
|
53
|
+
get_foundation({ extended: true })
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install
|
|
60
|
+
npm run dev # run with tsx
|
|
61
|
+
npm run build # compile + copy spec
|
|
62
|
+
npm run typecheck # type-check only
|
|
63
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { loadCatalog, findComponent } from "../catalog.js";
|
|
3
|
+
import { resolveSpecDir } from "../assembler.js";
|
|
4
|
+
let catalog;
|
|
5
|
+
let specDir;
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
specDir = resolveSpecDir();
|
|
8
|
+
catalog = await loadCatalog(specDir);
|
|
9
|
+
});
|
|
10
|
+
describe("loadCatalog", () => {
|
|
11
|
+
it("loads all 93 components", () => {
|
|
12
|
+
expect(catalog.components.length).toBe(93);
|
|
13
|
+
});
|
|
14
|
+
it("loads all 11 categories", () => {
|
|
15
|
+
expect(catalog.categories.length).toBe(11);
|
|
16
|
+
});
|
|
17
|
+
it("every component has required fields", () => {
|
|
18
|
+
for (const comp of catalog.components) {
|
|
19
|
+
expect(comp.id).toBeTruthy();
|
|
20
|
+
expect(comp.name).toBeTruthy();
|
|
21
|
+
expect(comp.class).toBeTruthy();
|
|
22
|
+
expect(comp.category).toBeTruthy();
|
|
23
|
+
expect(comp.file).toBeTruthy();
|
|
24
|
+
expect(comp.description).toBeTruthy();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
it("every component belongs to a known category", () => {
|
|
28
|
+
const categoryIds = new Set(catalog.categories.map((c) => c.id));
|
|
29
|
+
for (const comp of catalog.components) {
|
|
30
|
+
expect(categoryIds.has(comp.category)).toBe(true);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("findComponent", () => {
|
|
35
|
+
it("finds a component by id", () => {
|
|
36
|
+
const comp = findComponent(catalog, "push-button");
|
|
37
|
+
expect(comp).toBeDefined();
|
|
38
|
+
expect(comp.name).toBe("Push Button");
|
|
39
|
+
expect(comp.class).toBe(".push-btn");
|
|
40
|
+
});
|
|
41
|
+
it("finds a component by name (case-insensitive)", () => {
|
|
42
|
+
const comp = findComponent(catalog, "push button");
|
|
43
|
+
expect(comp).toBeDefined();
|
|
44
|
+
expect(comp.id).toBe("push-button");
|
|
45
|
+
});
|
|
46
|
+
it("returns undefined for unknown component", () => {
|
|
47
|
+
const comp = findComponent(catalog, "nonexistent-widget");
|
|
48
|
+
expect(comp).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { loadCatalog } from "../catalog.js";
|
|
3
|
+
import { createSearchIndex, searchComponents } from "../search.js";
|
|
4
|
+
import { resolveSpecDir, loadFoundation } from "../assembler.js";
|
|
5
|
+
let catalog;
|
|
6
|
+
let index;
|
|
7
|
+
let specDir;
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
specDir = resolveSpecDir();
|
|
10
|
+
catalog = await loadCatalog(specDir);
|
|
11
|
+
index = createSearchIndex(catalog.components);
|
|
12
|
+
});
|
|
13
|
+
describe("searchComponents", () => {
|
|
14
|
+
it("finds components matching 'button'", () => {
|
|
15
|
+
const results = searchComponents(index, "button");
|
|
16
|
+
expect(results.length).toBeGreaterThan(0);
|
|
17
|
+
expect(results.some((r) => r.id === "push-button")).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
it("finds components matching 'LCD'", () => {
|
|
20
|
+
const results = searchComponents(index, "LCD");
|
|
21
|
+
expect(results.length).toBeGreaterThan(0);
|
|
22
|
+
expect(results.some((r) => r.id === "lcd-readout")).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it("returns empty array for nonsense query", () => {
|
|
25
|
+
const results = searchComponents(index, "xyzzy123nonexistent");
|
|
26
|
+
expect(results.length).toBe(0);
|
|
27
|
+
});
|
|
28
|
+
it("respects limit parameter", () => {
|
|
29
|
+
const results = searchComponents(index, "button", 3);
|
|
30
|
+
expect(results.length).toBeLessThanOrEqual(3);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe("loadFoundation", () => {
|
|
34
|
+
it("loads core foundation (5 files)", async () => {
|
|
35
|
+
const content = await loadFoundation(specDir, false);
|
|
36
|
+
expect(content).toBeTruthy();
|
|
37
|
+
expect(content.length).toBeGreaterThan(1000);
|
|
38
|
+
});
|
|
39
|
+
it("loads extended foundation (9 files)", async () => {
|
|
40
|
+
const content = await loadFoundation(specDir, true);
|
|
41
|
+
expect(content).toBeTruthy();
|
|
42
|
+
expect(content.length).toBeGreaterThan(content.length * 0.5);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { resolveSpecDir, assembleComposition } from "../assembler.js";
|
|
3
|
+
import { loadCatalog } from "../catalog.js";
|
|
4
|
+
import { createSearchIndex } from "../search.js";
|
|
5
|
+
import { handleTool } from "../tools.js";
|
|
6
|
+
let ctx;
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
const specDir = resolveSpecDir();
|
|
9
|
+
const catalog = await loadCatalog(specDir);
|
|
10
|
+
ctx = { specDir, catalog, searchIndex: createSearchIndex(catalog.components) };
|
|
11
|
+
});
|
|
12
|
+
describe("assembleComposition rejects path traversal", () => {
|
|
13
|
+
it("throws on traversal / non-slug names before touching the filesystem", async () => {
|
|
14
|
+
for (const bad of ["../../../etc/passwd", "../secret", "a/b", "foo..bar", "UPPER"]) {
|
|
15
|
+
await expect(assembleComposition(ctx.specDir, ctx.catalog, bad)).rejects.toThrow(/Invalid composition name/);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
it("accepts a valid composition slug", async () => {
|
|
19
|
+
const out = await assembleComposition(ctx.specDir, ctx.catalog, "audio-mixer-strip");
|
|
20
|
+
expect(out).toContain("COMPOSITION: audio-mixer-strip");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe("handleTool validates inputs", () => {
|
|
24
|
+
it("rejects missing or malformed required args", async () => {
|
|
25
|
+
await expect(handleTool("get_component", {}, ctx)).rejects.toThrow(/non-empty string/);
|
|
26
|
+
await expect(handleTool("search_components", { query: "" }, ctx)).rejects.toThrow(/non-empty string/);
|
|
27
|
+
await expect(handleTool("get_components", { names: [] }, ctx)).rejects.toThrow(/non-empty array/);
|
|
28
|
+
await expect(handleTool("get_composition", { name: "../x" }, ctx)).rejects.toThrow(/Invalid composition name/);
|
|
29
|
+
});
|
|
30
|
+
it("clamps an out-of-range search limit", async () => {
|
|
31
|
+
const out = await handleTool("search_components", { query: "button", limit: 999 }, ctx);
|
|
32
|
+
expect(JSON.parse(out).length).toBeLessThanOrEqual(50);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Catalog } from "./catalog.js";
|
|
2
|
+
export declare function resolveSpecDir(): string;
|
|
3
|
+
export declare function loadFoundation(specDir: string, extended?: boolean): Promise<string>;
|
|
4
|
+
export declare function assembleComponent(specDir: string, catalog: Catalog, name: string): Promise<string>;
|
|
5
|
+
export declare function assembleComponents(specDir: string, catalog: Catalog, names: string[]): Promise<string>;
|
|
6
|
+
export declare function assembleComposition(specDir: string, catalog: Catalog, name: string): Promise<string>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { parse } from "yaml";
|
|
6
|
+
import { findComponent } from "./catalog.js";
|
|
7
|
+
const CORE_FOUNDATIONS = ["philosophy", "tokens", "materials", "depth-model", "naming"];
|
|
8
|
+
const ALL_FOUNDATIONS = [...CORE_FOUNDATIONS, "theme", "accessibility", "layout", "canvas"];
|
|
9
|
+
export function resolveSpecDir() {
|
|
10
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
// Monorepo: src/ or dist/ -> package root -> ../../spec
|
|
12
|
+
const monorepo = join(here, "..", "..", "..", "spec");
|
|
13
|
+
if (existsSync(monorepo))
|
|
14
|
+
return monorepo;
|
|
15
|
+
// npm package: dist/ -> package root -> ./spec
|
|
16
|
+
const bundled = join(here, "..", "spec");
|
|
17
|
+
if (existsSync(bundled))
|
|
18
|
+
return bundled;
|
|
19
|
+
throw new Error("Could not find spec directory");
|
|
20
|
+
}
|
|
21
|
+
export async function loadFoundation(specDir, extended = false) {
|
|
22
|
+
const files = extended ? ALL_FOUNDATIONS : CORE_FOUNDATIONS;
|
|
23
|
+
const contents = await Promise.all(files.map((f) => readFile(join(specDir, "foundation", `${f}.md`), "utf8")));
|
|
24
|
+
return contents.join("\n\n");
|
|
25
|
+
}
|
|
26
|
+
export async function assembleComponent(specDir, catalog, name) {
|
|
27
|
+
const comp = findComponent(catalog, name);
|
|
28
|
+
if (!comp)
|
|
29
|
+
throw new Error(`Component not found: ${name}`);
|
|
30
|
+
const [foundation, spec] = await Promise.all([
|
|
31
|
+
loadFoundation(specDir),
|
|
32
|
+
readFile(join(specDir, "components", comp.file), "utf8"),
|
|
33
|
+
]);
|
|
34
|
+
return [
|
|
35
|
+
`<!-- ═══ PUDGE-UI FOUNDATION ═══ -->`,
|
|
36
|
+
foundation,
|
|
37
|
+
"",
|
|
38
|
+
`<!-- ═══ COMPONENT: ${comp.name} ═══ -->`,
|
|
39
|
+
spec,
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
export async function assembleComponents(specDir, catalog, names) {
|
|
43
|
+
const comps = names.map((n) => {
|
|
44
|
+
const c = findComponent(catalog, n);
|
|
45
|
+
if (!c)
|
|
46
|
+
throw new Error(`Component not found: ${n}`);
|
|
47
|
+
return c;
|
|
48
|
+
});
|
|
49
|
+
const [foundation, ...specs] = await Promise.all([
|
|
50
|
+
loadFoundation(specDir),
|
|
51
|
+
...comps.map((c) => readFile(join(specDir, "components", c.file), "utf8")),
|
|
52
|
+
]);
|
|
53
|
+
const parts = [`<!-- ═══ PUDGE-UI FOUNDATION ═══ -->`, foundation, ""];
|
|
54
|
+
comps.forEach((c, i) => {
|
|
55
|
+
parts.push(`<!-- ═══ COMPONENT: ${c.name} ═══ -->`);
|
|
56
|
+
parts.push(specs[i]);
|
|
57
|
+
parts.push("");
|
|
58
|
+
});
|
|
59
|
+
return parts.join("\n");
|
|
60
|
+
}
|
|
61
|
+
export async function assembleComposition(specDir, catalog, name) {
|
|
62
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
63
|
+
throw new Error(`Invalid composition name: ${name}`);
|
|
64
|
+
}
|
|
65
|
+
const compPath = join(specDir, "compositions", `${name}.md`);
|
|
66
|
+
const raw = await readFile(compPath, "utf8");
|
|
67
|
+
// Parse frontmatter for component IDs
|
|
68
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
69
|
+
const fm = fmMatch ? parse(fmMatch[1]) : {};
|
|
70
|
+
const componentIds = fm.components ?? [];
|
|
71
|
+
const comps = componentIds.map((id) => {
|
|
72
|
+
const c = findComponent(catalog, id);
|
|
73
|
+
if (!c)
|
|
74
|
+
throw new Error(`Component not found: ${id}`);
|
|
75
|
+
return c;
|
|
76
|
+
});
|
|
77
|
+
const [foundation, ...specs] = await Promise.all([
|
|
78
|
+
loadFoundation(specDir),
|
|
79
|
+
...comps.map((c) => readFile(join(specDir, "components", c.file), "utf8")),
|
|
80
|
+
]);
|
|
81
|
+
const parts = [
|
|
82
|
+
`<!-- ═══ PUDGE-UI FOUNDATION ═══ -->`,
|
|
83
|
+
foundation,
|
|
84
|
+
"",
|
|
85
|
+
`<!-- ═══ COMPOSITION: ${name} ═══ -->`,
|
|
86
|
+
raw,
|
|
87
|
+
"",
|
|
88
|
+
];
|
|
89
|
+
comps.forEach((c, i) => {
|
|
90
|
+
parts.push(`<!-- ═══ COMPONENT: ${c.name} ═══ -->`);
|
|
91
|
+
parts.push(specs[i]);
|
|
92
|
+
parts.push("");
|
|
93
|
+
});
|
|
94
|
+
return parts.join("\n");
|
|
95
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Category {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
prefix: string;
|
|
5
|
+
count: number;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Component {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
class: string;
|
|
12
|
+
category: string;
|
|
13
|
+
file: string;
|
|
14
|
+
tags: string[];
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
export interface Catalog {
|
|
18
|
+
categories: Category[];
|
|
19
|
+
components: Component[];
|
|
20
|
+
}
|
|
21
|
+
export declare function loadCatalog(specDir: string): Promise<Catalog>;
|
|
22
|
+
export declare function findComponent(catalog: Catalog, id: string): Component | undefined;
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
export async function loadCatalog(specDir) {
|
|
5
|
+
const raw = await readFile(join(specDir, "components", "_index.yaml"), "utf8");
|
|
6
|
+
return parse(raw);
|
|
7
|
+
}
|
|
8
|
+
export function findComponent(catalog, id) {
|
|
9
|
+
return catalog.components.find((c) => c.id === id || c.name.toLowerCase() === id.toLowerCase());
|
|
10
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { loadCatalog } from "./catalog.js";
|
|
9
|
+
import { resolveSpecDir } from "./assembler.js";
|
|
10
|
+
import { createSearchIndex } from "./search.js";
|
|
11
|
+
import { toolDefinitions, handleTool } from "./tools.js";
|
|
12
|
+
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
|
|
13
|
+
const server = new Server({ name: "pudge-ui", version: pkg.version }, { capabilities: { tools: {} } });
|
|
14
|
+
const specDir = resolveSpecDir();
|
|
15
|
+
const catalog = await loadCatalog(specDir);
|
|
16
|
+
const searchIndex = createSearchIndex(catalog.components);
|
|
17
|
+
const ctx = { specDir, catalog, searchIndex };
|
|
18
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
19
|
+
tools: toolDefinitions,
|
|
20
|
+
}));
|
|
21
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
22
|
+
const { name, arguments: args } = request.params;
|
|
23
|
+
try {
|
|
24
|
+
const text = await handleTool(name, args ?? {}, ctx);
|
|
25
|
+
return { content: [{ type: "text", text }] };
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const transport = new StdioServerTransport();
|
|
33
|
+
await server.connect(transport);
|
package/dist/search.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import Fuse from "fuse.js";
|
|
2
|
+
import type { Component } from "./catalog.js";
|
|
3
|
+
export type SearchIndex = Fuse<Component>;
|
|
4
|
+
export declare function createSearchIndex(components: Component[]): SearchIndex;
|
|
5
|
+
export declare function searchComponents(index: SearchIndex, query: string, limit?: number): Component[];
|
package/dist/search.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Fuse from "fuse.js";
|
|
2
|
+
export function createSearchIndex(components) {
|
|
3
|
+
return new Fuse(components, {
|
|
4
|
+
keys: [
|
|
5
|
+
{ name: "name", weight: 2 },
|
|
6
|
+
{ name: "description", weight: 1.5 },
|
|
7
|
+
{ name: "tagsJoined", weight: 1 },
|
|
8
|
+
{ name: "category", weight: 0.5 },
|
|
9
|
+
],
|
|
10
|
+
threshold: 0.4,
|
|
11
|
+
getFn: (obj, path) => {
|
|
12
|
+
const key = Array.isArray(path) ? path[0] : path;
|
|
13
|
+
if (key === "tagsJoined")
|
|
14
|
+
return obj.tags.join(" ");
|
|
15
|
+
return obj[key];
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function searchComponents(index, query, limit = 10) {
|
|
20
|
+
return index.search(query, { limit }).map((r) => r.item);
|
|
21
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Catalog } from "./catalog.js";
|
|
2
|
+
import type { SearchIndex } from "./search.js";
|
|
3
|
+
export interface ToolContext {
|
|
4
|
+
specDir: string;
|
|
5
|
+
catalog: Catalog;
|
|
6
|
+
searchIndex: SearchIndex;
|
|
7
|
+
}
|
|
8
|
+
export declare const toolDefinitions: ({
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object";
|
|
13
|
+
properties: {
|
|
14
|
+
category: {
|
|
15
|
+
type: string;
|
|
16
|
+
description: string;
|
|
17
|
+
};
|
|
18
|
+
query?: undefined;
|
|
19
|
+
limit?: undefined;
|
|
20
|
+
extended?: undefined;
|
|
21
|
+
name?: undefined;
|
|
22
|
+
names?: undefined;
|
|
23
|
+
};
|
|
24
|
+
required?: undefined;
|
|
25
|
+
};
|
|
26
|
+
} | {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object";
|
|
31
|
+
properties: {
|
|
32
|
+
query: {
|
|
33
|
+
type: string;
|
|
34
|
+
description: string;
|
|
35
|
+
};
|
|
36
|
+
limit: {
|
|
37
|
+
type: string;
|
|
38
|
+
description: string;
|
|
39
|
+
};
|
|
40
|
+
category?: undefined;
|
|
41
|
+
extended?: undefined;
|
|
42
|
+
name?: undefined;
|
|
43
|
+
names?: undefined;
|
|
44
|
+
};
|
|
45
|
+
required: string[];
|
|
46
|
+
};
|
|
47
|
+
} | {
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object";
|
|
52
|
+
properties: {
|
|
53
|
+
extended: {
|
|
54
|
+
type: string;
|
|
55
|
+
description: string;
|
|
56
|
+
};
|
|
57
|
+
category?: undefined;
|
|
58
|
+
query?: undefined;
|
|
59
|
+
limit?: undefined;
|
|
60
|
+
name?: undefined;
|
|
61
|
+
names?: undefined;
|
|
62
|
+
};
|
|
63
|
+
required?: undefined;
|
|
64
|
+
};
|
|
65
|
+
} | {
|
|
66
|
+
name: string;
|
|
67
|
+
description: string;
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object";
|
|
70
|
+
properties: {
|
|
71
|
+
name: {
|
|
72
|
+
type: string;
|
|
73
|
+
description: string;
|
|
74
|
+
};
|
|
75
|
+
category?: undefined;
|
|
76
|
+
query?: undefined;
|
|
77
|
+
limit?: undefined;
|
|
78
|
+
extended?: undefined;
|
|
79
|
+
names?: undefined;
|
|
80
|
+
};
|
|
81
|
+
required: string[];
|
|
82
|
+
};
|
|
83
|
+
} | {
|
|
84
|
+
name: string;
|
|
85
|
+
description: string;
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object";
|
|
88
|
+
properties: {
|
|
89
|
+
names: {
|
|
90
|
+
type: string;
|
|
91
|
+
items: {
|
|
92
|
+
type: string;
|
|
93
|
+
};
|
|
94
|
+
description: string;
|
|
95
|
+
};
|
|
96
|
+
category?: undefined;
|
|
97
|
+
query?: undefined;
|
|
98
|
+
limit?: undefined;
|
|
99
|
+
extended?: undefined;
|
|
100
|
+
name?: undefined;
|
|
101
|
+
};
|
|
102
|
+
required: string[];
|
|
103
|
+
};
|
|
104
|
+
})[];
|
|
105
|
+
export declare function handleTool(toolName: string, args: Record<string, unknown>, ctx: ToolContext): Promise<string>;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { searchComponents } from "./search.js";
|
|
2
|
+
import { loadFoundation, assembleComponent, assembleComponents, assembleComposition, } from "./assembler.js";
|
|
3
|
+
export const toolDefinitions = [
|
|
4
|
+
{
|
|
5
|
+
name: "list_components",
|
|
6
|
+
description: "List all pudge-ui components, optionally filtered by category.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
category: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Filter by category id (e.g. 'buttons', 'toggles')",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "search_components",
|
|
19
|
+
description: "Fuzzy search pudge-ui components by name, description, or tags.",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
query: { type: "string", description: "Search query" },
|
|
24
|
+
limit: { type: "number", description: "Max results (default 10)" },
|
|
25
|
+
},
|
|
26
|
+
required: ["query"],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "get_foundation",
|
|
31
|
+
description: "Get pudge-ui foundation spec (philosophy, tokens, materials, depth, naming). Set extended=true for all 9 docs.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
extended: {
|
|
36
|
+
type: "boolean",
|
|
37
|
+
description: "Include all 9 foundation docs (default: core 5)",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "get_component",
|
|
44
|
+
description: "Get full spec for a single pudge-ui component (foundation + component spec).",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
name: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Component id or name (e.g. 'push-button' or 'Push Button')",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ["name"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "get_components",
|
|
58
|
+
description: "Get full specs for multiple pudge-ui components (foundation included once).",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
names: {
|
|
63
|
+
type: "array",
|
|
64
|
+
items: { type: "string" },
|
|
65
|
+
description: "Array of component ids or names",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
required: ["names"],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "get_composition",
|
|
73
|
+
description: "Get a pudge-ui composition spec with all referenced component specs.",
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
name: { type: "string", description: "Composition name (e.g. 'audio-mixer-strip')" },
|
|
78
|
+
},
|
|
79
|
+
required: ["name"],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
function asString(value, field) {
|
|
84
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
85
|
+
throw new Error(`"${field}" must be a non-empty string`);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function asStringArray(value, field) {
|
|
90
|
+
if (!Array.isArray(value) ||
|
|
91
|
+
value.length === 0 ||
|
|
92
|
+
!value.every((v) => typeof v === "string" && v.trim() !== "")) {
|
|
93
|
+
throw new Error(`"${field}" must be a non-empty array of strings`);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function asLimit(value) {
|
|
98
|
+
if (value === undefined)
|
|
99
|
+
return 10;
|
|
100
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
101
|
+
throw new Error(`"limit" must be a number`);
|
|
102
|
+
}
|
|
103
|
+
return Math.max(1, Math.min(50, Math.floor(value)));
|
|
104
|
+
}
|
|
105
|
+
export async function handleTool(toolName, args, ctx) {
|
|
106
|
+
switch (toolName) {
|
|
107
|
+
case "list_components": {
|
|
108
|
+
const category = args.category === undefined ? undefined : asString(args.category, "category");
|
|
109
|
+
const list = category
|
|
110
|
+
? ctx.catalog.components.filter((c) => c.category === category)
|
|
111
|
+
: ctx.catalog.components;
|
|
112
|
+
return JSON.stringify(list.map(({ id, name, category, description }) => ({ id, name, category, description })), null, 2);
|
|
113
|
+
}
|
|
114
|
+
case "search_components": {
|
|
115
|
+
const results = searchComponents(ctx.searchIndex, asString(args.query, "query"), asLimit(args.limit));
|
|
116
|
+
return JSON.stringify(results.map(({ id, name, category, description }) => ({ id, name, category, description })), null, 2);
|
|
117
|
+
}
|
|
118
|
+
case "get_foundation":
|
|
119
|
+
return loadFoundation(ctx.specDir, args.extended === true);
|
|
120
|
+
case "get_component":
|
|
121
|
+
return assembleComponent(ctx.specDir, ctx.catalog, asString(args.name, "name"));
|
|
122
|
+
case "get_components":
|
|
123
|
+
return assembleComponents(ctx.specDir, ctx.catalog, asStringArray(args.names, "names"));
|
|
124
|
+
case "get_composition":
|
|
125
|
+
return assembleComposition(ctx.specDir, ctx.catalog, asString(args.name, "name"));
|
|
126
|
+
default:
|
|
127
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
128
|
+
}
|
|
129
|
+
}
|