@unpunnyfuns/swatchbook-mcp 0.10.2

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 ADDED
@@ -0,0 +1,75 @@
1
+ # MCP
2
+
3
+ Published as `@unpunnyfuns/swatchbook-mcp`. Model Context Protocol server for swatchbook — exposes a DTCG project's tokens, axes, and diagnostics to AI agents without running Storybook.
4
+
5
+ > **Documentation:** [unpunnyfuns.github.io/swatchbook](https://unpunnyfuns.github.io/swatchbook/). Token parsing powered by [Terrazzo](https://terrazzo.app/) via `@unpunnyfuns/swatchbook-core`.
6
+
7
+ ## What it's for
8
+
9
+ Agents that need to reason about your design tokens — figma-to-token round-trips, alias-chain navigation, CI lint hooks, AI-assisted authoring — without spinning up a Storybook iframe. Point it at a `swatchbook.config.{ts,mts,js,mjs}` or a bare DTCG `resolver.json` and it parses the project on startup, then answers MCP tool calls against the resolved graph.
10
+
11
+ ## Install & run
12
+
13
+ ```sh
14
+ npx @unpunnyfuns/swatchbook-mcp --config swatchbook.config.ts
15
+ ```
16
+
17
+ Or wire it into an MCP client's config. Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "swatchbook": {
23
+ "command": "npx",
24
+ "args": ["-y", "@unpunnyfuns/swatchbook-mcp", "--config", "/absolute/path/to/swatchbook.config.ts"]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ CLI flags:
31
+
32
+ | Flag | What |
33
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- |
34
+ | `--config <path>` | Required. Either a `swatchbook.config.{ts,mts,js,mjs}` (full config) or a DTCG `resolver.json` (bare — other options at defaults). |
35
+ | `--cwd <path>` | Override the working directory for resolving relative `resolver` / `tokens` paths. |
36
+ | `--no-watch` | Disable live-reload. By default the server watches the config + resolved source files and swaps in fresh data on edits. |
37
+ | `--help` | Print usage and exit. |
38
+
39
+ ## Tools
40
+
41
+ | Tool | Inputs | Returns |
42
+ | --------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
43
+ | `describe_project` | (none) | High-level overview — token counts, axes, themes, presets, diagnostic counts, `$type`s present. |
44
+ | `list_tokens` | `filter?` path glob, `type?` DTCG `$type`, `theme?` name | Array of `{ path, type?, value }` from the named theme (or default). Use first to discover paths. |
45
+ | `search_tokens` | `query`, `theme?`, `limit?` | Case-insensitive substring search across paths, descriptions, and values. Returns matches + `matchedIn` hint. |
46
+ | `resolve_theme` | `tuple`, `filter?`, `type?` | Resolved token map for an axis tuple (`{ mode: "Dark", brand: "…" }`). Fills omitted axes from defaults. |
47
+ | `get_token` | `path` | Full detail: per-theme value, alias chain, aliased-by list, CSS var reference. |
48
+ | `get_alias_chain` | `path` | Forward alias chain per theme (`path → ... → primitive`). Empty when the token is a primitive. |
49
+ | `get_aliased_by` | `path`, `maxDepth?` | Backward alias tree — every token that resolves through this path. Breadth-first with cycle protection; default max depth 6. |
50
+ | `get_consumer_output` | `path`, `tuple?` | CSS var, resolved value, compound `[data-…]` selector + HTML attrs needed to pin the tuple on `<html>`. |
51
+ | `get_color_formats` | `path`, `theme?` | Color token rendered in `hex` / `rgb` / `hsl` / `oklch` / `raw`, each with an `outOfGamut` flag. |
52
+ | `list_axes` | (none) | Axes + contexts + themes + presets from the project config. |
53
+ | `get_diagnostics` | `severity?` `'error' \| 'warn' \| 'info'` | Parser / resolver / validation diagnostics. |
54
+ | `emit_css` | (none) | Full project stylesheet — `:root` default + per-tuple compound-selector blocks. |
55
+
56
+ Path globs accept `*` (one segment), `**` (any number of segments trailing or mid-path), or exact dot-paths.
57
+
58
+ ## Programmatic use
59
+
60
+ You can also construct the server in-process — useful if you're embedding the MCP handler in a larger toolchain:
61
+
62
+ ```ts
63
+ import { createServer, loadFromConfig } from '@unpunnyfuns/swatchbook-mcp';
64
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
65
+
66
+ const { project } = await loadFromConfig('swatchbook.config.ts');
67
+ const server = createServer(project);
68
+ await server.connect(new StdioServerTransport());
69
+ ```
70
+
71
+ ## See also
72
+
73
+ - [`@unpunnyfuns/swatchbook-core`](../core) — the loader this server wraps.
74
+ - [Project README](../../README.md) — install and wiring flow for the whole toolchain.
75
+ - [Model Context Protocol](https://modelcontextprotocol.io/) — the upstream spec.
package/dist/bin.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/bin.mjs ADDED
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ import { n as createServer, t as loadFromConfig } from "./load-config-DQQ0BK3J.mjs";
3
+ import { basename, dirname, isAbsolute, resolve } from "node:path";
4
+ import { watch } from "node:fs";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ //#region src/bin.ts
7
+ /**
8
+ * Stdio entry for `npx @unpunnyfuns/swatchbook-mcp --config <path>`.
9
+ *
10
+ * Parses `--config <path>`, optional `--cwd <path>`, loads the project, and
11
+ * binds an MCP server to stdio. Watches the resolved source files + config
12
+ * file so token edits land in subsequent tool calls without restarting the
13
+ * transport. Pass `--no-watch` to opt out (e.g. CI). Loader / watcher errors
14
+ * print to stderr — stdout is reserved for MCP protocol frames.
15
+ */
16
+ function parseArgs(argv) {
17
+ const out = { watch: true };
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const arg = argv[i];
20
+ const next = argv[i + 1];
21
+ if ((arg === "--config" || arg === "-c") && next) {
22
+ out.config = next;
23
+ i++;
24
+ } else if (arg === "--cwd" && next) {
25
+ out.cwd = next;
26
+ i++;
27
+ } else if (arg === "--no-watch") out.watch = false;
28
+ else if (arg === "--help" || arg === "-h") {
29
+ console.log(`swatchbook-mcp — Model Context Protocol server for swatchbook projects
30
+
31
+ Usage:
32
+ swatchbook-mcp --config <path> Point at a swatchbook.config.{ts,mts,js,mjs}
33
+ or a DTCG resolver.json directly. Bare
34
+ resolvers boot with every other config
35
+ option at defaults.
36
+ swatchbook-mcp --config <path> --cwd <path> Override the project cwd for relative paths.
37
+ swatchbook-mcp --config <path> --no-watch Disable live-reload on token / config edits.
38
+
39
+ Tools exposed:
40
+ describe_project Orientation — counts, axes, themes, presets, diagnostics.
41
+ list_tokens List tokens by path glob / \`$type\`.
42
+ search_tokens Substring search across paths, descriptions, values.
43
+ resolve_theme Full resolved token map for an axis tuple.
44
+ get_token Full detail for one token path.
45
+ get_alias_chain Forward alias chain per theme.
46
+ get_aliased_by Backward alias tree.
47
+ get_consumer_output CSS var + data-attribute activation for a tuple.
48
+ get_color_formats hex / rgb / hsl / oklch / raw for a color token.
49
+ list_axes Axes + contexts + themes + presets.
50
+ get_diagnostics Project diagnostics (optional severity filter).
51
+ emit_css Full project stylesheet.`);
52
+ process.exit(0);
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+ /**
58
+ * Watch the project's source files + config path for edits. Debounces
59
+ * filesystem events (editors fire 2-3 per save) into a single reload per
60
+ * 100 ms burst; on each settle, calls `loadFromConfig` again and swaps the
61
+ * fresh project into the already-connected MCP server.
62
+ *
63
+ * Watches parent directories rather than files themselves. Atomic-save
64
+ * editors unlink + recreate the target inode, which kills file-level
65
+ * watchers on the first save; a dir watcher with filename filtering
66
+ * survives that dance. Mirrors the addon's plugin strategy.
67
+ */
68
+ function setupReload(initialSourceFiles, configPath, reload) {
69
+ const dirs = /* @__PURE__ */ new Map();
70
+ const add = (file) => {
71
+ const dir = dirname(file);
72
+ const set = dirs.get(dir) ?? /* @__PURE__ */ new Set();
73
+ set.add(basename(file));
74
+ dirs.set(dir, set);
75
+ };
76
+ for (const file of initialSourceFiles) add(file);
77
+ add(configPath);
78
+ let pending = null;
79
+ const schedule = () => {
80
+ if (pending) clearTimeout(pending);
81
+ pending = setTimeout(() => {
82
+ pending = null;
83
+ reload().catch((err) => {
84
+ console.error("swatchbook-mcp: reload failed —", err);
85
+ });
86
+ }, 100);
87
+ };
88
+ const watchers = [];
89
+ for (const [dir, names] of dirs) try {
90
+ const w = watch(dir, { persistent: false }, (eventType, filename) => {
91
+ if (!filename) return;
92
+ if (!names.has(filename)) return;
93
+ if (eventType === "change" || eventType === "rename") schedule();
94
+ });
95
+ watchers.push(w);
96
+ } catch {}
97
+ return () => {
98
+ if (pending) clearTimeout(pending);
99
+ for (const w of watchers) w.close();
100
+ };
101
+ }
102
+ async function main() {
103
+ const args = parseArgs(process.argv.slice(2));
104
+ if (!args.config) {
105
+ console.error("swatchbook-mcp: --config <path> is required. Use --help for usage.");
106
+ process.exit(1);
107
+ }
108
+ const configAbsolute = isAbsolute(args.config) ? args.config : resolve(process.cwd(), args.config);
109
+ const { project } = await loadFromConfig(configAbsolute, args.cwd);
110
+ const server = createServer(project);
111
+ const transport = new StdioServerTransport();
112
+ await server.connect(transport);
113
+ if (args.watch) {
114
+ let stopWatchers = setupReload(project.sourceFiles, configAbsolute, () => reload());
115
+ async function reload() {
116
+ const { project: next } = await loadFromConfig(configAbsolute, args.cwd);
117
+ server.setProject(next);
118
+ const themeCount = next.themes.length;
119
+ const tokenCount = Object.keys(next.themesResolved[next.themes[0]?.name ?? ""] ?? {}).length;
120
+ console.error(`swatchbook-mcp: project reloaded — ${tokenCount} tokens across ${themeCount} theme${themeCount === 1 ? "" : "s"}.`);
121
+ stopWatchers();
122
+ stopWatchers = setupReload(next.sourceFiles, configAbsolute, () => reload());
123
+ }
124
+ }
125
+ }
126
+ main().catch((err) => {
127
+ console.error("swatchbook-mcp failed to start:", err);
128
+ process.exit(1);
129
+ });
130
+ //#endregion
131
+ export {};
132
+
133
+ //# sourceMappingURL=bin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.mjs","names":["fsWatch"],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Stdio entry for `npx @unpunnyfuns/swatchbook-mcp --config <path>`.\n *\n * Parses `--config <path>`, optional `--cwd <path>`, loads the project, and\n * binds an MCP server to stdio. Watches the resolved source files + config\n * file so token edits land in subsequent tool calls without restarting the\n * transport. Pass `--no-watch` to opt out (e.g. CI). Loader / watcher errors\n * print to stderr — stdout is reserved for MCP protocol frames.\n */\nimport { type FSWatcher, watch as fsWatch } from 'node:fs';\nimport { basename, dirname, isAbsolute, resolve } from 'node:path';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { loadFromConfig } from '#/load-config.ts';\nimport { createServer } from '#/server.ts';\n\ninterface CliArgs {\n config?: string;\n cwd?: string;\n watch: boolean;\n}\n\nfunction parseArgs(argv: readonly string[]): CliArgs {\n const out: CliArgs = { watch: true };\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n const next = argv[i + 1];\n if ((arg === '--config' || arg === '-c') && next) {\n out.config = next;\n i++;\n } else if (arg === '--cwd' && next) {\n out.cwd = next;\n i++;\n } else if (arg === '--no-watch') {\n out.watch = false;\n } else if (arg === '--help' || arg === '-h') {\n console.log(`swatchbook-mcp — Model Context Protocol server for swatchbook projects\n\nUsage:\n swatchbook-mcp --config <path> Point at a swatchbook.config.{ts,mts,js,mjs}\n or a DTCG resolver.json directly. Bare\n resolvers boot with every other config\n option at defaults.\n swatchbook-mcp --config <path> --cwd <path> Override the project cwd for relative paths.\n swatchbook-mcp --config <path> --no-watch Disable live-reload on token / config edits.\n\nTools exposed:\n describe_project Orientation — counts, axes, themes, presets, diagnostics.\n list_tokens List tokens by path glob / \\`$type\\`.\n search_tokens Substring search across paths, descriptions, values.\n resolve_theme Full resolved token map for an axis tuple.\n get_token Full detail for one token path.\n get_alias_chain Forward alias chain per theme.\n get_aliased_by Backward alias tree.\n get_consumer_output CSS var + data-attribute activation for a tuple.\n get_color_formats hex / rgb / hsl / oklch / raw for a color token.\n list_axes Axes + contexts + themes + presets.\n get_diagnostics Project diagnostics (optional severity filter).\n emit_css Full project stylesheet.`);\n process.exit(0);\n }\n }\n return out;\n}\n\n/**\n * Watch the project's source files + config path for edits. Debounces\n * filesystem events (editors fire 2-3 per save) into a single reload per\n * 100 ms burst; on each settle, calls `loadFromConfig` again and swaps the\n * fresh project into the already-connected MCP server.\n *\n * Watches parent directories rather than files themselves. Atomic-save\n * editors unlink + recreate the target inode, which kills file-level\n * watchers on the first save; a dir watcher with filename filtering\n * survives that dance. Mirrors the addon's plugin strategy.\n */\nfunction setupReload(\n initialSourceFiles: readonly string[],\n configPath: string,\n reload: () => Promise<void>,\n): () => void {\n const dirs = new Map<string, Set<string>>();\n const add = (file: string): void => {\n const dir = dirname(file);\n const set = dirs.get(dir) ?? new Set<string>();\n set.add(basename(file));\n dirs.set(dir, set);\n };\n for (const file of initialSourceFiles) add(file);\n add(configPath);\n\n let pending: ReturnType<typeof setTimeout> | null = null;\n const schedule = (): void => {\n if (pending) clearTimeout(pending);\n pending = setTimeout(() => {\n pending = null;\n reload().catch((err) => {\n console.error('swatchbook-mcp: reload failed —', err);\n });\n }, 100);\n };\n\n const watchers: FSWatcher[] = [];\n for (const [dir, names] of dirs) {\n try {\n const w = fsWatch(dir, { persistent: false }, (eventType, filename) => {\n if (!filename) return;\n if (!names.has(filename)) return;\n if (eventType === 'change' || eventType === 'rename') schedule();\n });\n watchers.push(w);\n } catch {\n // unwatchable dir — skip. The initial load already succeeded, so the\n // server keeps serving the current snapshot.\n }\n }\n return () => {\n if (pending) clearTimeout(pending);\n for (const w of watchers) w.close();\n };\n}\n\nasync function main(): Promise<void> {\n const args = parseArgs(process.argv.slice(2));\n if (!args.config) {\n console.error('swatchbook-mcp: --config <path> is required. Use --help for usage.');\n process.exit(1);\n }\n const configAbsolute = isAbsolute(args.config)\n ? args.config\n : resolve(process.cwd(), args.config);\n\n const { project } = await loadFromConfig(configAbsolute, args.cwd);\n const server = createServer(project);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n if (args.watch) {\n let stopWatchers = setupReload(project.sourceFiles, configAbsolute, () => reload());\n async function reload(): Promise<void> {\n const { project: next } = await loadFromConfig(configAbsolute, args.cwd);\n server.setProject(next);\n const themeCount = next.themes.length;\n const tokenCount = Object.keys(next.themesResolved[next.themes[0]?.name ?? ''] ?? {}).length;\n console.error(\n `swatchbook-mcp: project reloaded — ${tokenCount} tokens across ${themeCount} theme${themeCount === 1 ? '' : 's'}.`,\n );\n // Rebind watchers against the fresh source-file set so newly-referenced\n // tokens / resolvers pick up edits from here on.\n stopWatchers();\n stopWatchers = setupReload(next.sourceFiles, configAbsolute, () => reload());\n }\n }\n}\n\nmain().catch((err) => {\n console.error('swatchbook-mcp failed to start:', err);\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;;;;;;AAsBA,SAAS,UAAU,MAAkC;CACnD,MAAM,MAAe,EAAE,OAAO,MAAM;AACpC,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,MAAM,OAAO,KAAK,IAAI;AACtB,OAAK,QAAQ,cAAc,QAAQ,SAAS,MAAM;AAChD,OAAI,SAAS;AACb;aACS,QAAQ,WAAW,MAAM;AAClC,OAAI,MAAM;AACV;aACS,QAAQ,aACjB,KAAI,QAAQ;WACH,QAAQ,YAAY,QAAQ,MAAM;AAC3C,WAAQ,IAAI;;;;;;;;;;;;;;;;;;;;;;gDAsB8B;AAC1C,WAAQ,KAAK,EAAE;;;AAGnB,QAAO;;;;;;;;;;;;;AAcT,SAAS,YACP,oBACA,YACA,QACY;CACZ,MAAM,uBAAO,IAAI,KAA0B;CAC3C,MAAM,OAAO,SAAuB;EAClC,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,MAAM,KAAK,IAAI,IAAI,oBAAI,IAAI,KAAa;AAC9C,MAAI,IAAI,SAAS,KAAK,CAAC;AACvB,OAAK,IAAI,KAAK,IAAI;;AAEpB,MAAK,MAAM,QAAQ,mBAAoB,KAAI,KAAK;AAChD,KAAI,WAAW;CAEf,IAAI,UAAgD;CACpD,MAAM,iBAAuB;AAC3B,MAAI,QAAS,cAAa,QAAQ;AAClC,YAAU,iBAAiB;AACzB,aAAU;AACV,WAAQ,CAAC,OAAO,QAAQ;AACtB,YAAQ,MAAM,mCAAmC,IAAI;KACrD;KACD,IAAI;;CAGT,MAAM,WAAwB,EAAE;AAChC,MAAK,MAAM,CAAC,KAAK,UAAU,KACzB,KAAI;EACF,MAAM,IAAIA,MAAQ,KAAK,EAAE,YAAY,OAAO,GAAG,WAAW,aAAa;AACrE,OAAI,CAAC,SAAU;AACf,OAAI,CAAC,MAAM,IAAI,SAAS,CAAE;AAC1B,OAAI,cAAc,YAAY,cAAc,SAAU,WAAU;IAChE;AACF,WAAS,KAAK,EAAE;SACV;AAKV,cAAa;AACX,MAAI,QAAS,cAAa,QAAQ;AAClC,OAAK,MAAM,KAAK,SAAU,GAAE,OAAO;;;AAIvC,eAAe,OAAsB;CACnC,MAAM,OAAO,UAAU,QAAQ,KAAK,MAAM,EAAE,CAAC;AAC7C,KAAI,CAAC,KAAK,QAAQ;AAChB,UAAQ,MAAM,qEAAqE;AACnF,UAAQ,KAAK,EAAE;;CAEjB,MAAM,iBAAiB,WAAW,KAAK,OAAO,GAC1C,KAAK,SACL,QAAQ,QAAQ,KAAK,EAAE,KAAK,OAAO;CAEvC,MAAM,EAAE,YAAY,MAAM,eAAe,gBAAgB,KAAK,IAAI;CAClE,MAAM,SAAS,aAAa,QAAQ;CACpC,MAAM,YAAY,IAAI,sBAAsB;AAC5C,OAAM,OAAO,QAAQ,UAAU;AAE/B,KAAI,KAAK,OAAO;EACd,IAAI,eAAe,YAAY,QAAQ,aAAa,sBAAsB,QAAQ,CAAC;EACnF,eAAe,SAAwB;GACrC,MAAM,EAAE,SAAS,SAAS,MAAM,eAAe,gBAAgB,KAAK,IAAI;AACxE,UAAO,WAAW,KAAK;GACvB,MAAM,aAAa,KAAK,OAAO;GAC/B,MAAM,aAAa,OAAO,KAAK,KAAK,eAAe,KAAK,OAAO,IAAI,QAAQ,OAAO,EAAE,CAAC,CAAC;AACtF,WAAQ,MACN,sCAAsC,WAAW,iBAAiB,WAAW,QAAQ,eAAe,IAAI,KAAK,IAAI,GAClH;AAGD,iBAAc;AACd,kBAAe,YAAY,KAAK,aAAa,sBAAsB,QAAQ,CAAC;;;;AAKlF,MAAM,CAAC,OAAO,QAAQ;AACpB,SAAQ,MAAM,mCAAmC,IAAI;AACrD,SAAQ,KAAK,EAAE;EACf"}
@@ -0,0 +1,40 @@
1
+ import { Config, Project } from "@unpunnyfuns/swatchbook-core";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+
4
+ //#region src/server.d.ts
5
+ /**
6
+ * Build a swatchbook MCP server bound to a loaded project. Tools expose the
7
+ * project's tokens, axes, and diagnostics so an AI agent can query them
8
+ * without running Storybook. Tool handlers close over a live `project`
9
+ * reference — call the returned `setProject` to swap in a freshly loaded
10
+ * project (e.g. after token edits) without re-binding the transport.
11
+ */
12
+ declare function createServer(initial: Project): McpServer & {
13
+ setProject: (next: Project) => void;
14
+ };
15
+ //#endregion
16
+ //#region src/load-config.d.ts
17
+ /**
18
+ * Load a swatchbook project from either a full config module or a bare
19
+ * DTCG resolver JSON.
20
+ *
21
+ * `.ts` / `.mts` / `.js` / `.mjs` → jiti imports the module and uses
22
+ * its default export as the swatchbook {@link Config}.
23
+ *
24
+ * `.json` → treated as a DTCG resolver file; the CLI constructs a
25
+ * minimal `{ resolver: path }` config so agents can point at a raw
26
+ * resolver without authoring a wrapper. Every other config option
27
+ * (presets, chrome map, disabled axes, css-var prefix) falls back to
28
+ * `loadProject` defaults.
29
+ *
30
+ * The `cwd` returned is the directory the config / resolver lives in.
31
+ * `loadProject` uses it to resolve relative token references.
32
+ */
33
+ declare function loadFromConfig(configPath: string, cwdOverride?: string): Promise<{
34
+ project: Project;
35
+ cwd: string;
36
+ config: Config;
37
+ }>;
38
+ //#endregion
39
+ export { createServer, loadFromConfig };
40
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { n as createServer, t as loadFromConfig } from "./load-config-DQQ0BK3J.mjs";
2
+ export { createServer, loadFromConfig };
@@ -0,0 +1,540 @@
1
+ import { loadProject, projectCss } from "@unpunnyfuns/swatchbook-core";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { z } from "zod";
4
+ import Color from "colorjs.io";
5
+ import { extname, isAbsolute, resolve } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { createJiti } from "jiti";
8
+ //#region src/format-color.ts
9
+ function formatColor(raw, format) {
10
+ if (!raw || typeof raw !== "object") return null;
11
+ const normalized = raw;
12
+ if (format === "raw") return {
13
+ format,
14
+ value: JSON.stringify(raw),
15
+ outOfGamut: false
16
+ };
17
+ const components = normalized.components ?? normalized.channels;
18
+ if (!components || !normalized.colorSpace) return null;
19
+ let color;
20
+ try {
21
+ const [r = 0, g = 0, b = 0] = components.map((c) => c == null ? 0 : c);
22
+ color = new Color(normalized.colorSpace, [
23
+ r,
24
+ g,
25
+ b
26
+ ], normalized.alpha ?? 1);
27
+ } catch {
28
+ return null;
29
+ }
30
+ if (format === "hex") try {
31
+ const inSrgb = color.to("srgb");
32
+ if (!inSrgb.inGamut()) return {
33
+ format,
34
+ value: inSrgb.toString({ format: "rgb" }),
35
+ outOfGamut: true
36
+ };
37
+ return {
38
+ format,
39
+ value: inSrgb.toString({ format: "hex" }),
40
+ outOfGamut: false
41
+ };
42
+ } catch {
43
+ return null;
44
+ }
45
+ if (format === "rgb") try {
46
+ const inSrgb = color.to("srgb");
47
+ return {
48
+ format,
49
+ value: inSrgb.toString({ format: "rgb" }),
50
+ outOfGamut: !inSrgb.inGamut()
51
+ };
52
+ } catch {
53
+ return null;
54
+ }
55
+ if (format === "hsl") try {
56
+ return {
57
+ format,
58
+ value: color.to("hsl").toString(),
59
+ outOfGamut: !color.to("srgb").inGamut()
60
+ };
61
+ } catch {
62
+ return null;
63
+ }
64
+ if (format === "oklch") try {
65
+ return {
66
+ format,
67
+ value: color.to("oklch").toString(),
68
+ outOfGamut: false
69
+ };
70
+ } catch {
71
+ return null;
72
+ }
73
+ return null;
74
+ }
75
+ const ALL_COLOR_FORMATS = [
76
+ "hex",
77
+ "rgb",
78
+ "hsl",
79
+ "oklch",
80
+ "raw"
81
+ ];
82
+ function formatColorEveryWay(raw) {
83
+ const out = {};
84
+ for (const format of ALL_COLOR_FORMATS) {
85
+ const result = formatColor(raw, format);
86
+ if (result) out[format] = result;
87
+ }
88
+ return out;
89
+ }
90
+ //#endregion
91
+ //#region src/match.ts
92
+ /**
93
+ * Minimal DTCG-flavoured path matcher. Accepts exact paths (`color.bg`), single-
94
+ * segment globs (`color.*`), multi-segment globs (`color.**`), or a trailing `*`
95
+ * mid-segment (`color.palette.blue.*`). No brace expansion, no regex — DTCG
96
+ * paths are dot-delimited and this matches the parity the blocks' `globMatch`
97
+ * ships. Case-sensitive.
98
+ */
99
+ function matchPath(path, filter) {
100
+ if (!filter) return true;
101
+ if (filter === "**" || filter === "*") return true;
102
+ const pathSegments = path.split(".");
103
+ const filterSegments = filter.split(".");
104
+ let pi = 0;
105
+ let fi = 0;
106
+ while (fi < filterSegments.length) {
107
+ const fseg = filterSegments[fi];
108
+ if (fseg === "**") {
109
+ if (fi === filterSegments.length - 1) return true;
110
+ const remaining = filterSegments.slice(fi + 1);
111
+ for (let k = pi; k <= pathSegments.length; k++) if (matchPath(pathSegments.slice(k).join("."), remaining.join("."))) return true;
112
+ return false;
113
+ }
114
+ if (pi >= pathSegments.length) return false;
115
+ const pseg = pathSegments[pi];
116
+ if (fseg === "*") {} else if (fseg !== pseg) return false;
117
+ pi++;
118
+ fi++;
119
+ }
120
+ return pi === pathSegments.length;
121
+ }
122
+ //#endregion
123
+ //#region src/server.ts
124
+ /**
125
+ * Build a swatchbook MCP server bound to a loaded project. Tools expose the
126
+ * project's tokens, axes, and diagnostics so an AI agent can query them
127
+ * without running Storybook. Tool handlers close over a live `project`
128
+ * reference — call the returned `setProject` to swap in a freshly loaded
129
+ * project (e.g. after token edits) without re-binding the transport.
130
+ */
131
+ function createServer(initial) {
132
+ let project = initial;
133
+ const server = new McpServer({
134
+ name: "@unpunnyfuns/swatchbook-mcp",
135
+ version: project.config.cssVarPrefix ? `project:${project.config.cssVarPrefix}` : "project"
136
+ }, { instructions: "Query a swatchbook DTCG project: list tokens by path glob, inspect individual tokens (value, $type, alias chain, per-theme resolved values), read axes / presets, and inspect diagnostics." });
137
+ server.setProject = (next) => {
138
+ project = next;
139
+ };
140
+ server.registerTool("describe_project", {
141
+ description: "High-level summary of the project — total token count per theme, axes (with contexts) and how they compose, preset list, diagnostic counts by severity, css-var prefix, and the DTCG `$type`s present. Good first call for an agent that needs an orientation before querying specifics.",
142
+ inputSchema: {}
143
+ }, () => {
144
+ const typeCounts = {};
145
+ const tokensPerTheme = {};
146
+ for (const theme of project.themes) {
147
+ const tokens = project.themesResolved[theme.name] ?? {};
148
+ tokensPerTheme[theme.name] = Object.keys(tokens).length;
149
+ for (const token of Object.values(tokens)) if (token.$type) typeCounts[token.$type] = (typeCounts[token.$type] ?? 0) + 1;
150
+ }
151
+ const diagBySeverity = {
152
+ error: 0,
153
+ warn: 0,
154
+ info: 0
155
+ };
156
+ for (const d of project.diagnostics) diagBySeverity[d.severity] = (diagBySeverity[d.severity] ?? 0) + 1;
157
+ return jsonResult({
158
+ cssVarPrefix: project.config.cssVarPrefix ?? "",
159
+ axes: project.axes.map((a) => ({
160
+ name: a.name,
161
+ contexts: a.contexts,
162
+ default: a.default
163
+ })),
164
+ themes: project.themes.map((t) => t.name),
165
+ defaultTheme: project.themes[0]?.name ?? null,
166
+ presets: project.presets.map((p) => p.name),
167
+ tokensPerTheme,
168
+ types: typeCounts,
169
+ diagnostics: {
170
+ counts: diagBySeverity,
171
+ total: project.diagnostics.length
172
+ }
173
+ });
174
+ });
175
+ server.registerTool("emit_css", {
176
+ description: "Return the full project CSS — a `:root` block with the default tuple plus one compound-selector block per non-default axis combination. Same output the addon injects into Storybook and the docs-site chrome pipeline writes to disk. Useful when an agent needs to inline the stylesheet into a generated artifact.",
177
+ inputSchema: {}
178
+ }, () => textResult(projectCss(project)));
179
+ server.registerTool("list_tokens", {
180
+ description: "List token paths in the project, optionally filtered by path glob (`color.*`, `color.palette.**`) and/or DTCG `$type` (color, dimension, typography, …). Returns path + $type + stringified value from the default theme. Use this first to discover what tokens exist; follow with get_token for details.",
181
+ inputSchema: {
182
+ filter: z.string().optional().describe("Dot-path glob, e.g. `color.*` or `color.palette.**`. Omit for all tokens."),
183
+ type: z.string().optional().describe("DTCG `$type` to scope the result, e.g. `color`, `dimension`, `typography`."),
184
+ theme: z.string().optional().describe("Theme name to read values from. Defaults to the project default theme.")
185
+ }
186
+ }, ({ filter, type, theme }) => {
187
+ const themeName = theme ?? project.themes[0]?.name;
188
+ if (!themeName) return textResult("No themes in project.");
189
+ const tokens = project.themesResolved[themeName] ?? {};
190
+ const rows = [];
191
+ for (const [path, token] of Object.entries(tokens)) {
192
+ if (type && token.$type !== type) continue;
193
+ if (!matchPath(path, filter)) continue;
194
+ rows.push({
195
+ path,
196
+ ...token.$type !== void 0 && { type: token.$type },
197
+ value: stringifyValue(token.$value)
198
+ });
199
+ }
200
+ rows.sort((a, b) => a.path.localeCompare(b.path, void 0, { numeric: true }));
201
+ return jsonResult({
202
+ theme: themeName,
203
+ count: rows.length,
204
+ tokens: rows
205
+ });
206
+ });
207
+ server.registerTool("get_token", {
208
+ description: "Get full details for a single token: resolved value in every theme, DTCG `$type`, `$description`, alias chain, aliased-by list, and CSS var reference. Use after `list_tokens` to inspect a specific path.",
209
+ inputSchema: { path: z.string().describe("Dot-path of the token, e.g. `color.accent.bg`.") }
210
+ }, ({ path }) => {
211
+ const perTheme = {};
212
+ let type;
213
+ let description;
214
+ let aliasedBy;
215
+ let found = false;
216
+ for (const theme of project.themes) {
217
+ const token = project.themesResolved[theme.name]?.[path];
218
+ if (!token) continue;
219
+ found = true;
220
+ type ??= token.$type;
221
+ description ??= token.$description;
222
+ aliasedBy ??= token.aliasedBy;
223
+ perTheme[theme.name] = {
224
+ value: stringifyValue(token.$value),
225
+ ...token.aliasOf !== void 0 && { aliasOf: token.aliasOf },
226
+ ...token.aliasChain !== void 0 && { aliasChain: token.aliasChain }
227
+ };
228
+ }
229
+ if (!found) return textResult(`Token not found: ${path}`);
230
+ const prefix = project.config.cssVarPrefix ?? "";
231
+ const cssVar = `var(--${prefix ? `${prefix}-` : ""}${path.replaceAll(".", "-")})`;
232
+ return jsonResult({
233
+ path,
234
+ type,
235
+ description,
236
+ cssVar,
237
+ aliasedBy,
238
+ perTheme
239
+ });
240
+ });
241
+ server.registerTool("list_axes", {
242
+ description: "List the project axes — each axis has a name, its contexts (discrete values like `Light` / `Dark`), a default, and a source (`resolver` for DTCG-resolver-driven, `layered` for authored layered axes, `synthetic` for single-theme projects). Also returns the named themes (every axis tuple combination) and any presets defined in the project config.",
243
+ inputSchema: {}
244
+ }, () => jsonResult({
245
+ axes: project.axes.map((axis) => ({
246
+ name: axis.name,
247
+ contexts: axis.contexts,
248
+ default: axis.default,
249
+ description: axis.description,
250
+ source: axis.source
251
+ })),
252
+ disabledAxes: project.disabledAxes,
253
+ themes: project.themes.map((t) => ({
254
+ name: t.name,
255
+ input: t.input
256
+ })),
257
+ presets: project.presets.map((p) => ({
258
+ name: p.name,
259
+ axes: p.axes,
260
+ description: p.description
261
+ }))
262
+ }));
263
+ server.registerTool("get_alias_chain", {
264
+ description: "Forward alias chain for a token — the sequence of paths it resolves through on the way to a primitive value (e.g. `color.accent.bg → color.brand.blue.700 → color.palette.blue.700`). Returns the chain per theme because aliases can resolve through different paths per axis context. Empty chain when the token is a primitive (no aliases) or missing.",
265
+ inputSchema: { path: z.string().describe("Dot-path of the token, e.g. `color.accent.bg`.") }
266
+ }, ({ path }) => {
267
+ const perTheme = {};
268
+ let found = false;
269
+ for (const theme of project.themes) {
270
+ const token = project.themesResolved[theme.name]?.[path];
271
+ if (!token) continue;
272
+ found = true;
273
+ const chain = [path];
274
+ if (token.aliasChain && token.aliasChain.length > 0) chain.push(...token.aliasChain);
275
+ else if (token.aliasOf) chain.push(token.aliasOf);
276
+ perTheme[theme.name] = {
277
+ ...token.aliasOf !== void 0 && { aliasOf: token.aliasOf },
278
+ chain
279
+ };
280
+ }
281
+ if (!found) return textResult(`Token not found: ${path}`);
282
+ return jsonResult({
283
+ path,
284
+ perTheme
285
+ });
286
+ });
287
+ server.registerTool("get_aliased_by", {
288
+ description: "Backward alias tree for a token — every token that resolves through this path at any depth. Breadth-first walk with cycle protection; `maxDepth` caps recursion (default 6). Empty when nothing aliases the token.",
289
+ inputSchema: {
290
+ path: z.string().describe("Dot-path of the token, e.g. `color.palette.blue.500`."),
291
+ maxDepth: z.number().int().positive().optional().describe("Maximum recursion depth. Default 6.")
292
+ }
293
+ }, ({ path, maxDepth }) => {
294
+ const depth = maxDepth ?? 6;
295
+ const themeName = project.themes[0]?.name;
296
+ if (!themeName) return textResult("No themes in project.");
297
+ const tokens = project.themesResolved[themeName] ?? {};
298
+ if (!tokens[path]) return textResult(`Token not found: ${path}`);
299
+ const visited = new Set([path]);
300
+ const walk = (current, d) => {
301
+ const direct = tokens[current]?.aliasedBy ?? [];
302
+ if (direct.length === 0) return {
303
+ path: current,
304
+ depth: d,
305
+ children: []
306
+ };
307
+ if (d >= depth) return {
308
+ path: current,
309
+ depth: d,
310
+ children: [],
311
+ truncated: true
312
+ };
313
+ const children = [];
314
+ for (const p of direct) {
315
+ if (visited.has(p)) continue;
316
+ visited.add(p);
317
+ children.push(walk(p, d + 1));
318
+ }
319
+ return {
320
+ path: current,
321
+ depth: d,
322
+ children
323
+ };
324
+ };
325
+ return jsonResult(walk(path, 0));
326
+ });
327
+ server.registerTool("get_color_formats", {
328
+ description: "For a color token, return its value rendered in every format the addon toolbar exposes — `hex`, `rgb`, `hsl`, `oklch`, and the raw JSON. Each entry carries an `outOfGamut` flag when the chosen colorspace can't losslessly represent the token (wide-gamut tokens rendered in sRGB, for example). Skips non-color tokens.",
329
+ inputSchema: {
330
+ path: z.string().describe("Dot-path of a color token, e.g. `color.accent.bg`."),
331
+ theme: z.string().optional().describe("Theme name to read the value from. Defaults to the project default theme.")
332
+ }
333
+ }, ({ path, theme }) => {
334
+ const themeName = theme ?? project.themes[0]?.name;
335
+ if (!themeName) return textResult("No themes in project.");
336
+ const token = project.themesResolved[themeName]?.[path];
337
+ if (!token) return textResult(`Token not found: ${path}`);
338
+ if (token.$type !== "color") return textResult(`Token ${path} is not a color (got $type=${token.$type ?? "unknown"}).`);
339
+ return jsonResult({
340
+ path,
341
+ theme: themeName,
342
+ formats: formatColorEveryWay(token.$value)
343
+ });
344
+ });
345
+ server.registerTool("search_tokens", {
346
+ description: "Case-insensitive substring search across token paths, `$description`, and stringified values. Returns matches with a short snippet pointing at where the match hit. Use when you know what you want but not the path — `search_tokens(\"radius\")` finds every token whose path / description / value mentions radius. Scopes to a single theme (default: project default).",
347
+ inputSchema: {
348
+ query: z.string().min(1).describe("Substring to search for (case-insensitive)."),
349
+ theme: z.string().optional().describe("Theme name to search within. Defaults to the project default."),
350
+ limit: z.number().int().positive().optional().describe("Cap the result count. Default 50.")
351
+ }
352
+ }, ({ query, theme, limit }) => {
353
+ const themeName = theme ?? project.themes[0]?.name;
354
+ if (!themeName) return textResult("No themes in project.");
355
+ const tokens = project.themesResolved[themeName] ?? {};
356
+ const needle = query.toLowerCase();
357
+ const max = limit ?? 50;
358
+ const hits = [];
359
+ for (const [path, token] of Object.entries(tokens)) {
360
+ const matchedIn = [];
361
+ if (path.toLowerCase().includes(needle)) matchedIn.push("path");
362
+ if ((token.$description?.toLowerCase())?.includes(needle)) matchedIn.push("description");
363
+ const value = stringifyValue(token.$value);
364
+ if (value.toLowerCase().includes(needle)) matchedIn.push("value");
365
+ if (matchedIn.length === 0) continue;
366
+ const entry = {
367
+ path,
368
+ matchedIn,
369
+ snippet: matchedIn.includes("description") ? token.$description ?? path : matchedIn.includes("value") ? `${path} = ${value}` : path
370
+ };
371
+ if (token.$type !== void 0) entry.type = token.$type;
372
+ hits.push(entry);
373
+ if (hits.length >= max) break;
374
+ }
375
+ hits.sort((a, b) => a.path.localeCompare(b.path, void 0, { numeric: true }));
376
+ return jsonResult({
377
+ query,
378
+ theme: themeName,
379
+ count: hits.length,
380
+ truncated: hits.length === max,
381
+ hits
382
+ });
383
+ });
384
+ server.registerTool("resolve_theme", {
385
+ description: "Resolve the full token map for a given axis tuple. Agent passes a partial tuple (`{ mode: \"Dark\", brand: \"Brand A\" }`); any axis omitted falls back to that axis's default. Returns the matching theme name, the complete tuple after filling defaults, and the resolved `{ path: { value, type, aliasOf?, aliasChain? } }` map — effectively \"what do all tokens look like if I pin this combination\".",
386
+ inputSchema: {
387
+ tuple: z.record(z.string(), z.string()).describe("Partial axis tuple, e.g. `{ mode: \"Dark\", brand: \"Brand A\" }`."),
388
+ filter: z.string().optional().describe("Optional path glob to scope the returned map."),
389
+ type: z.string().optional().describe("Optional DTCG `$type` to scope the returned map.")
390
+ }
391
+ }, ({ tuple, filter, type }) => {
392
+ const active = {};
393
+ for (const axis of project.axes) {
394
+ const candidate = tuple[axis.name];
395
+ active[axis.name] = candidate && axis.contexts.includes(candidate) ? candidate : axis.default;
396
+ }
397
+ const themeName = project.themes.find((t) => {
398
+ for (const axis of project.axes) if (t.input[axis.name] !== active[axis.name]) return false;
399
+ return true;
400
+ })?.name ?? project.themes[0]?.name;
401
+ if (!themeName) return textResult("No matching theme.");
402
+ const tokens = project.themesResolved[themeName] ?? {};
403
+ const resolved = {};
404
+ let count = 0;
405
+ for (const [path, token] of Object.entries(tokens)) {
406
+ if (type && token.$type !== type) continue;
407
+ if (!matchPath(path, filter)) continue;
408
+ resolved[path] = {
409
+ value: stringifyValue(token.$value),
410
+ ...token.$type !== void 0 && { type: token.$type },
411
+ ...token.aliasOf !== void 0 && { aliasOf: token.aliasOf },
412
+ ...token.aliasChain !== void 0 && { aliasChain: token.aliasChain }
413
+ };
414
+ count++;
415
+ }
416
+ return jsonResult({
417
+ theme: themeName,
418
+ tuple: active,
419
+ count,
420
+ tokens: resolved
421
+ });
422
+ });
423
+ server.registerTool("get_consumer_output", {
424
+ description: "CSS var reference + resolved value + HTML data-attribute activation for a token under an optional axis tuple. Tells an agent everything it needs to write a stylesheet or JSX snippet that pins a particular theme combination — `selector` is the compound CSS selector that matches the tuple on `<html>`, `attrs` is the same information as HTML attributes, `cssVar` is the `var(--…)` reference. Tuple defaults to the project default when omitted.",
425
+ inputSchema: {
426
+ path: z.string().describe("Dot-path of the token, e.g. `color.accent.bg`."),
427
+ tuple: z.record(z.string(), z.string()).optional().describe("Optional axis tuple (e.g. `{ mode: \"Dark\", brand: \"Brand A\" }`). Defaults to each axis's own default.")
428
+ }
429
+ }, ({ path, tuple }) => {
430
+ const prefix = project.config.cssVarPrefix ?? "";
431
+ const activeTuple = {};
432
+ for (const axis of project.axes) {
433
+ const candidate = tuple?.[axis.name];
434
+ activeTuple[axis.name] = candidate && axis.contexts.includes(candidate) ? candidate : axis.default;
435
+ }
436
+ const themeName = project.themes.find((t) => {
437
+ for (const axis of project.axes) if (t.input[axis.name] !== activeTuple[axis.name]) return false;
438
+ return true;
439
+ })?.name ?? project.themes[0]?.name ?? "";
440
+ const token = themeName ? project.themesResolved[themeName]?.[path] : void 0;
441
+ if (!token) return textResult(`Token not found: ${path}`);
442
+ const cssVar = `var(--${prefix ? `${prefix}-` : ""}${path.replaceAll(".", "-")})`;
443
+ const attrName = (axis) => prefix ? `data-${prefix}-${axis}` : `data-${axis}`;
444
+ const attrs = {};
445
+ const selectorParts = [];
446
+ for (const axis of project.axes) {
447
+ const value = activeTuple[axis.name];
448
+ if (value !== void 0) {
449
+ attrs[attrName(axis.name)] = value;
450
+ selectorParts.push(`[${attrName(axis.name)}="${value}"]`);
451
+ }
452
+ }
453
+ return jsonResult({
454
+ path,
455
+ cssVar,
456
+ value: stringifyValue(token.$value),
457
+ type: token.$type,
458
+ theme: themeName,
459
+ tuple: activeTuple,
460
+ attrs,
461
+ selector: selectorParts.join("") || ":root",
462
+ usageSnippet: `color: ${cssVar};`
463
+ });
464
+ });
465
+ server.registerTool("get_diagnostics", {
466
+ description: "List parser / resolver / validation diagnostics for the project. Each entry carries a severity (`error`, `warn`, `info`), group, message, and optional filename / line / column for locating the issue.",
467
+ inputSchema: { severity: z.enum([
468
+ "error",
469
+ "warn",
470
+ "info"
471
+ ]).optional().describe("Optional severity filter. Omit for all diagnostics.") }
472
+ }, ({ severity }) => {
473
+ const rows = severity ? project.diagnostics.filter((d) => d.severity === severity) : project.diagnostics;
474
+ return jsonResult({
475
+ count: rows.length,
476
+ diagnostics: rows
477
+ });
478
+ });
479
+ return server;
480
+ }
481
+ function stringifyValue(value) {
482
+ if (value === null || value === void 0) return "—";
483
+ if (typeof value === "string") return value;
484
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
485
+ try {
486
+ return JSON.stringify(value);
487
+ } catch {
488
+ return String(value);
489
+ }
490
+ }
491
+ function textResult(text) {
492
+ return { content: [{
493
+ type: "text",
494
+ text
495
+ }] };
496
+ }
497
+ function jsonResult(data) {
498
+ return { content: [{
499
+ type: "text",
500
+ text: JSON.stringify(data, null, 2)
501
+ }] };
502
+ }
503
+ //#endregion
504
+ //#region src/load-config.ts
505
+ /**
506
+ * Load a swatchbook project from either a full config module or a bare
507
+ * DTCG resolver JSON.
508
+ *
509
+ * `.ts` / `.mts` / `.js` / `.mjs` → jiti imports the module and uses
510
+ * its default export as the swatchbook {@link Config}.
511
+ *
512
+ * `.json` → treated as a DTCG resolver file; the CLI constructs a
513
+ * minimal `{ resolver: path }` config so agents can point at a raw
514
+ * resolver without authoring a wrapper. Every other config option
515
+ * (presets, chrome map, disabled axes, css-var prefix) falls back to
516
+ * `loadProject` defaults.
517
+ *
518
+ * The `cwd` returned is the directory the config / resolver lives in.
519
+ * `loadProject` uses it to resolve relative token references.
520
+ */
521
+ async function loadFromConfig(configPath, cwdOverride) {
522
+ const absolute = isAbsolute(configPath) ? configPath : resolve(process.cwd(), configPath);
523
+ const cwd = cwdOverride ?? resolve(absolute, "..");
524
+ const config = extname(absolute).toLowerCase() === ".json" ? { resolver: absolute } : await loadTsConfig(absolute);
525
+ return {
526
+ project: await loadProject(config, cwd),
527
+ cwd,
528
+ config
529
+ };
530
+ }
531
+ async function loadTsConfig(absolute) {
532
+ return await createJiti(new URL("./", pathToFileURL(absolute)).href, {
533
+ interopDefault: true,
534
+ moduleCache: false
535
+ }).import(absolute, { default: true });
536
+ }
537
+ //#endregion
538
+ export { createServer as n, loadFromConfig as t };
539
+
540
+ //# sourceMappingURL=load-config-DQQ0BK3J.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-config-DQQ0BK3J.mjs","names":[],"sources":["../src/format-color.ts","../src/match.ts","../src/server.ts","../src/load-config.ts"],"sourcesContent":["import Color from 'colorjs.io';\n\n/**\n * Convert a DTCG color `$value` into the same format menu the\n * addon's toolbar exposes — hex / rgb / hsl / oklch / raw JSON. Mirrors\n * the narrower version in `@unpunnyfuns/swatchbook-blocks/format-color.ts`\n * without pulling blocks (and React) into the MCP server.\n *\n * Out-of-gamut colours still stringify — the caller gets both the\n * rendered string and an `outOfGamut` flag so agents can warn.\n */\n\nexport type ColorFormat = 'hex' | 'rgb' | 'hsl' | 'oklch' | 'raw';\n\nexport interface FormatColorResult {\n format: ColorFormat;\n value: string;\n outOfGamut: boolean;\n}\n\ninterface NormalizedColor {\n colorSpace?: string;\n components?: readonly (number | null)[];\n channels?: readonly (number | null)[];\n alpha?: number;\n hex?: string;\n}\n\nexport function formatColor(raw: unknown, format: ColorFormat): FormatColorResult | null {\n if (!raw || typeof raw !== 'object') return null;\n const normalized = raw as NormalizedColor;\n\n if (format === 'raw') {\n return { format, value: JSON.stringify(raw), outOfGamut: false };\n }\n\n const components = normalized.components ?? normalized.channels;\n if (!components || !normalized.colorSpace) return null;\n\n let color: Color;\n try {\n const coords = components.map((c) => (c == null ? 0 : c));\n const [r = 0, g = 0, b = 0] = coords;\n color = new Color(normalized.colorSpace, [r, g, b], normalized.alpha ?? 1);\n } catch {\n return null;\n }\n\n if (format === 'hex') {\n try {\n const inSrgb = color.to('srgb');\n const outOfGamut = !inSrgb.inGamut();\n if (outOfGamut) {\n return { format, value: inSrgb.toString({ format: 'rgb' }), outOfGamut: true };\n }\n return { format, value: inSrgb.toString({ format: 'hex' }), outOfGamut: false };\n } catch {\n return null;\n }\n }\n\n if (format === 'rgb') {\n try {\n const inSrgb = color.to('srgb');\n return {\n format,\n value: inSrgb.toString({ format: 'rgb' }),\n outOfGamut: !inSrgb.inGamut(),\n };\n } catch {\n return null;\n }\n }\n\n if (format === 'hsl') {\n try {\n const inHsl = color.to('hsl');\n return {\n format,\n value: inHsl.toString(),\n outOfGamut: !color.to('srgb').inGamut(),\n };\n } catch {\n return null;\n }\n }\n\n if (format === 'oklch') {\n try {\n const inOklch = color.to('oklch');\n return { format, value: inOklch.toString(), outOfGamut: false };\n } catch {\n return null;\n }\n }\n\n return null;\n}\n\nexport const ALL_COLOR_FORMATS: readonly ColorFormat[] = ['hex', 'rgb', 'hsl', 'oklch', 'raw'];\n\nexport function formatColorEveryWay(raw: unknown): Partial<Record<ColorFormat, FormatColorResult>> {\n const out: Partial<Record<ColorFormat, FormatColorResult>> = {};\n for (const format of ALL_COLOR_FORMATS) {\n const result = formatColor(raw, format);\n if (result) out[format] = result;\n }\n return out;\n}\n","/**\n * Minimal DTCG-flavoured path matcher. Accepts exact paths (`color.bg`), single-\n * segment globs (`color.*`), multi-segment globs (`color.**`), or a trailing `*`\n * mid-segment (`color.palette.blue.*`). No brace expansion, no regex — DTCG\n * paths are dot-delimited and this matches the parity the blocks' `globMatch`\n * ships. Case-sensitive.\n */\nexport function matchPath(path: string, filter: string | undefined): boolean {\n if (!filter) return true;\n if (filter === '**' || filter === '*') return true;\n\n const pathSegments = path.split('.');\n const filterSegments = filter.split('.');\n\n let pi = 0;\n let fi = 0;\n while (fi < filterSegments.length) {\n const fseg = filterSegments[fi];\n if (fseg === '**') {\n // Match zero or more path segments. The next filter segment must match\n // somewhere in the remaining path, or we accept the tail.\n if (fi === filterSegments.length - 1) return true;\n const remaining = filterSegments.slice(fi + 1);\n for (let k = pi; k <= pathSegments.length; k++) {\n if (matchPath(pathSegments.slice(k).join('.'), remaining.join('.'))) {\n return true;\n }\n }\n return false;\n }\n if (pi >= pathSegments.length) return false;\n const pseg = pathSegments[pi];\n if (fseg === '*') {\n // pass\n } else if (fseg !== pseg) {\n return false;\n }\n pi++;\n fi++;\n }\n return pi === pathSegments.length;\n}\n","import type { Project } from '@unpunnyfuns/swatchbook-core';\nimport { projectCss } from '@unpunnyfuns/swatchbook-core';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\nimport { formatColorEveryWay } from '#/format-color.ts';\nimport { matchPath } from '#/match.ts';\n\n/**\n * Build a swatchbook MCP server bound to a loaded project. Tools expose the\n * project's tokens, axes, and diagnostics so an AI agent can query them\n * without running Storybook. Tool handlers close over a live `project`\n * reference — call the returned `setProject` to swap in a freshly loaded\n * project (e.g. after token edits) without re-binding the transport.\n */\nexport function createServer(initial: Project): McpServer & {\n setProject: (next: Project) => void;\n} {\n let project = initial;\n const server = new McpServer(\n {\n name: '@unpunnyfuns/swatchbook-mcp',\n version: project.config.cssVarPrefix ? `project:${project.config.cssVarPrefix}` : 'project',\n },\n {\n instructions:\n 'Query a swatchbook DTCG project: list tokens by path glob, inspect individual tokens (value, $type, alias chain, per-theme resolved values), read axes / presets, and inspect diagnostics.',\n },\n ) as McpServer & { setProject: (next: Project) => void };\n server.setProject = (next: Project) => {\n project = next;\n };\n\n server.registerTool(\n 'describe_project',\n {\n description:\n 'High-level summary of the project — total token count per theme, axes (with contexts) and how they compose, preset list, diagnostic counts by severity, css-var prefix, and the DTCG `$type`s present. Good first call for an agent that needs an orientation before querying specifics.',\n inputSchema: {},\n },\n () => {\n const typeCounts: Record<string, number> = {};\n const tokensPerTheme: Record<string, number> = {};\n for (const theme of project.themes) {\n const tokens = project.themesResolved[theme.name] ?? {};\n tokensPerTheme[theme.name] = Object.keys(tokens).length;\n for (const token of Object.values(tokens)) {\n if (token.$type) typeCounts[token.$type] = (typeCounts[token.$type] ?? 0) + 1;\n }\n }\n const diagBySeverity = { error: 0, warn: 0, info: 0 } as Record<string, number>;\n for (const d of project.diagnostics) {\n diagBySeverity[d.severity] = (diagBySeverity[d.severity] ?? 0) + 1;\n }\n return jsonResult({\n cssVarPrefix: project.config.cssVarPrefix ?? '',\n axes: project.axes.map((a) => ({ name: a.name, contexts: a.contexts, default: a.default })),\n themes: project.themes.map((t) => t.name),\n defaultTheme: project.themes[0]?.name ?? null,\n presets: project.presets.map((p) => p.name),\n tokensPerTheme,\n types: typeCounts,\n diagnostics: {\n counts: diagBySeverity,\n total: project.diagnostics.length,\n },\n });\n },\n );\n\n server.registerTool(\n 'emit_css',\n {\n description:\n 'Return the full project CSS — a `:root` block with the default tuple plus one compound-selector block per non-default axis combination. Same output the addon injects into Storybook and the docs-site chrome pipeline writes to disk. Useful when an agent needs to inline the stylesheet into a generated artifact.',\n inputSchema: {},\n },\n () => textResult(projectCss(project)),\n );\n\n server.registerTool(\n 'list_tokens',\n {\n description:\n 'List token paths in the project, optionally filtered by path glob (`color.*`, `color.palette.**`) and/or DTCG `$type` (color, dimension, typography, …). Returns path + $type + stringified value from the default theme. Use this first to discover what tokens exist; follow with get_token for details.',\n inputSchema: {\n filter: z\n .string()\n .optional()\n .describe('Dot-path glob, e.g. `color.*` or `color.palette.**`. Omit for all tokens.'),\n type: z\n .string()\n .optional()\n .describe('DTCG `$type` to scope the result, e.g. `color`, `dimension`, `typography`.'),\n theme: z\n .string()\n .optional()\n .describe('Theme name to read values from. Defaults to the project default theme.'),\n },\n },\n ({ filter, type, theme }) => {\n const themeName = theme ?? project.themes[0]?.name;\n if (!themeName) {\n return textResult('No themes in project.');\n }\n const tokens = project.themesResolved[themeName] ?? {};\n const rows: { path: string; type?: string; value: string }[] = [];\n for (const [path, token] of Object.entries(tokens)) {\n if (type && token.$type !== type) continue;\n if (!matchPath(path, filter)) continue;\n rows.push({\n path,\n ...(token.$type !== undefined && { type: token.$type }),\n value: stringifyValue(token.$value),\n });\n }\n rows.sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true }));\n return jsonResult({ theme: themeName, count: rows.length, tokens: rows });\n },\n );\n\n server.registerTool(\n 'get_token',\n {\n description:\n 'Get full details for a single token: resolved value in every theme, DTCG `$type`, `$description`, alias chain, aliased-by list, and CSS var reference. Use after `list_tokens` to inspect a specific path.',\n inputSchema: {\n path: z.string().describe('Dot-path of the token, e.g. `color.accent.bg`.'),\n },\n },\n ({ path }) => {\n const perTheme: Record<\n string,\n { value: string; aliasOf?: string; aliasChain?: readonly string[] }\n > = {};\n let type: string | undefined;\n let description: string | undefined;\n let aliasedBy: readonly string[] | undefined;\n let found = false;\n\n for (const theme of project.themes) {\n const token = project.themesResolved[theme.name]?.[path];\n if (!token) continue;\n found = true;\n type ??= token.$type;\n description ??= token.$description;\n aliasedBy ??= token.aliasedBy;\n perTheme[theme.name] = {\n value: stringifyValue(token.$value),\n ...(token.aliasOf !== undefined && { aliasOf: token.aliasOf }),\n ...(token.aliasChain !== undefined && { aliasChain: token.aliasChain }),\n };\n }\n\n if (!found) return textResult(`Token not found: ${path}`);\n\n const prefix = project.config.cssVarPrefix ?? '';\n const cssVar = `var(--${prefix ? `${prefix}-` : ''}${path.replaceAll('.', '-')})`;\n\n return jsonResult({\n path,\n type,\n description,\n cssVar,\n aliasedBy,\n perTheme,\n });\n },\n );\n\n server.registerTool(\n 'list_axes',\n {\n description:\n 'List the project axes — each axis has a name, its contexts (discrete values like `Light` / `Dark`), a default, and a source (`resolver` for DTCG-resolver-driven, `layered` for authored layered axes, `synthetic` for single-theme projects). Also returns the named themes (every axis tuple combination) and any presets defined in the project config.',\n inputSchema: {},\n },\n () =>\n jsonResult({\n axes: project.axes.map((axis) => ({\n name: axis.name,\n contexts: axis.contexts,\n default: axis.default,\n description: axis.description,\n source: axis.source,\n })),\n disabledAxes: project.disabledAxes,\n themes: project.themes.map((t) => ({ name: t.name, input: t.input })),\n presets: project.presets.map((p) => ({\n name: p.name,\n axes: p.axes,\n description: p.description,\n })),\n }),\n );\n\n server.registerTool(\n 'get_alias_chain',\n {\n description:\n 'Forward alias chain for a token — the sequence of paths it resolves through on the way to a primitive value (e.g. `color.accent.bg → color.brand.blue.700 → color.palette.blue.700`). Returns the chain per theme because aliases can resolve through different paths per axis context. Empty chain when the token is a primitive (no aliases) or missing.',\n inputSchema: {\n path: z.string().describe('Dot-path of the token, e.g. `color.accent.bg`.'),\n },\n },\n ({ path }) => {\n const perTheme: Record<string, { aliasOf?: string; chain: readonly string[] }> = {};\n let found = false;\n for (const theme of project.themes) {\n const token = project.themesResolved[theme.name]?.[path];\n if (!token) continue;\n found = true;\n const chain: string[] = [path];\n if (token.aliasChain && token.aliasChain.length > 0) chain.push(...token.aliasChain);\n else if (token.aliasOf) chain.push(token.aliasOf);\n perTheme[theme.name] = {\n ...(token.aliasOf !== undefined && { aliasOf: token.aliasOf }),\n chain,\n };\n }\n if (!found) return textResult(`Token not found: ${path}`);\n return jsonResult({ path, perTheme });\n },\n );\n\n server.registerTool(\n 'get_aliased_by',\n {\n description:\n 'Backward alias tree for a token — every token that resolves through this path at any depth. Breadth-first walk with cycle protection; `maxDepth` caps recursion (default 6). Empty when nothing aliases the token.',\n inputSchema: {\n path: z.string().describe('Dot-path of the token, e.g. `color.palette.blue.500`.'),\n maxDepth: z\n .number()\n .int()\n .positive()\n .optional()\n .describe('Maximum recursion depth. Default 6.'),\n },\n },\n ({ path, maxDepth }) => {\n const depth = maxDepth ?? 6;\n const themeName = project.themes[0]?.name;\n if (!themeName) return textResult('No themes in project.');\n const tokens = project.themesResolved[themeName] ?? {};\n if (!tokens[path]) return textResult(`Token not found: ${path}`);\n\n interface Node {\n path: string;\n depth: number;\n children: Node[];\n truncated?: boolean;\n }\n const visited = new Set<string>([path]);\n const walk = (current: string, d: number): Node => {\n const tok = tokens[current];\n const direct = tok?.aliasedBy ?? [];\n if (direct.length === 0) return { path: current, depth: d, children: [] };\n if (d >= depth) return { path: current, depth: d, children: [], truncated: true };\n const children: Node[] = [];\n for (const p of direct) {\n if (visited.has(p)) continue;\n visited.add(p);\n children.push(walk(p, d + 1));\n }\n return { path: current, depth: d, children };\n };\n const root = walk(path, 0);\n return jsonResult(root);\n },\n );\n\n server.registerTool(\n 'get_color_formats',\n {\n description:\n \"For a color token, return its value rendered in every format the addon toolbar exposes — `hex`, `rgb`, `hsl`, `oklch`, and the raw JSON. Each entry carries an `outOfGamut` flag when the chosen colorspace can't losslessly represent the token (wide-gamut tokens rendered in sRGB, for example). Skips non-color tokens.\",\n inputSchema: {\n path: z.string().describe('Dot-path of a color token, e.g. `color.accent.bg`.'),\n theme: z\n .string()\n .optional()\n .describe('Theme name to read the value from. Defaults to the project default theme.'),\n },\n },\n ({ path, theme }) => {\n const themeName = theme ?? project.themes[0]?.name;\n if (!themeName) return textResult('No themes in project.');\n const token = project.themesResolved[themeName]?.[path];\n if (!token) return textResult(`Token not found: ${path}`);\n if (token.$type !== 'color') {\n return textResult(`Token ${path} is not a color (got $type=${token.$type ?? 'unknown'}).`);\n }\n return jsonResult({\n path,\n theme: themeName,\n formats: formatColorEveryWay(token.$value),\n });\n },\n );\n\n server.registerTool(\n 'search_tokens',\n {\n description:\n 'Case-insensitive substring search across token paths, `$description`, and stringified values. Returns matches with a short snippet pointing at where the match hit. Use when you know what you want but not the path — `search_tokens(\"radius\")` finds every token whose path / description / value mentions radius. Scopes to a single theme (default: project default).',\n inputSchema: {\n query: z.string().min(1).describe('Substring to search for (case-insensitive).'),\n theme: z\n .string()\n .optional()\n .describe('Theme name to search within. Defaults to the project default.'),\n limit: z.number().int().positive().optional().describe('Cap the result count. Default 50.'),\n },\n },\n ({ query, theme, limit }) => {\n const themeName = theme ?? project.themes[0]?.name;\n if (!themeName) return textResult('No themes in project.');\n const tokens = project.themesResolved[themeName] ?? {};\n const needle = query.toLowerCase();\n const max = limit ?? 50;\n const hits: {\n path: string;\n type?: string;\n matchedIn: ('path' | 'description' | 'value')[];\n snippet: string;\n }[] = [];\n\n for (const [path, token] of Object.entries(tokens)) {\n const matchedIn: ('path' | 'description' | 'value')[] = [];\n if (path.toLowerCase().includes(needle)) matchedIn.push('path');\n const desc = token.$description?.toLowerCase();\n if (desc?.includes(needle)) matchedIn.push('description');\n const value = stringifyValue(token.$value);\n if (value.toLowerCase().includes(needle)) matchedIn.push('value');\n if (matchedIn.length === 0) continue;\n const snippet = matchedIn.includes('description')\n ? (token.$description ?? path)\n : matchedIn.includes('value')\n ? `${path} = ${value}`\n : path;\n const entry: (typeof hits)[number] = { path, matchedIn, snippet };\n if (token.$type !== undefined) entry.type = token.$type;\n hits.push(entry);\n if (hits.length >= max) break;\n }\n hits.sort((a, b) => a.path.localeCompare(b.path, undefined, { numeric: true }));\n return jsonResult({\n query,\n theme: themeName,\n count: hits.length,\n truncated: hits.length === max,\n hits,\n });\n },\n );\n\n server.registerTool(\n 'resolve_theme',\n {\n description:\n 'Resolve the full token map for a given axis tuple. Agent passes a partial tuple (`{ mode: \"Dark\", brand: \"Brand A\" }`); any axis omitted falls back to that axis\\'s default. Returns the matching theme name, the complete tuple after filling defaults, and the resolved `{ path: { value, type, aliasOf?, aliasChain? } }` map — effectively \"what do all tokens look like if I pin this combination\".',\n inputSchema: {\n tuple: z\n .record(z.string(), z.string())\n .describe('Partial axis tuple, e.g. `{ mode: \"Dark\", brand: \"Brand A\" }`.'),\n filter: z.string().optional().describe('Optional path glob to scope the returned map.'),\n type: z.string().optional().describe('Optional DTCG `$type` to scope the returned map.'),\n },\n },\n ({ tuple, filter, type }) => {\n const active: Record<string, string> = {};\n for (const axis of project.axes) {\n const candidate = tuple[axis.name];\n active[axis.name] =\n candidate && axis.contexts.includes(candidate) ? candidate : axis.default;\n }\n const themeName =\n project.themes.find((t) => {\n for (const axis of project.axes) {\n if ((t.input as Record<string, string>)[axis.name] !== active[axis.name]) {\n return false;\n }\n }\n return true;\n })?.name ?? project.themes[0]?.name;\n if (!themeName) return textResult('No matching theme.');\n const tokens = project.themesResolved[themeName] ?? {};\n const resolved: Record<\n string,\n { value: string; type?: string; aliasOf?: string; aliasChain?: readonly string[] }\n > = {};\n let count = 0;\n for (const [path, token] of Object.entries(tokens)) {\n if (type && token.$type !== type) continue;\n if (!matchPath(path, filter)) continue;\n resolved[path] = {\n value: stringifyValue(token.$value),\n ...(token.$type !== undefined && { type: token.$type }),\n ...(token.aliasOf !== undefined && { aliasOf: token.aliasOf }),\n ...(token.aliasChain !== undefined && { aliasChain: token.aliasChain }),\n };\n count++;\n }\n return jsonResult({ theme: themeName, tuple: active, count, tokens: resolved });\n },\n );\n\n server.registerTool(\n 'get_consumer_output',\n {\n description:\n 'CSS var reference + resolved value + HTML data-attribute activation for a token under an optional axis tuple. Tells an agent everything it needs to write a stylesheet or JSX snippet that pins a particular theme combination — `selector` is the compound CSS selector that matches the tuple on `<html>`, `attrs` is the same information as HTML attributes, `cssVar` is the `var(--…)` reference. Tuple defaults to the project default when omitted.',\n inputSchema: {\n path: z.string().describe('Dot-path of the token, e.g. `color.accent.bg`.'),\n tuple: z\n .record(z.string(), z.string())\n .optional()\n .describe(\n 'Optional axis tuple (e.g. `{ mode: \"Dark\", brand: \"Brand A\" }`). Defaults to each axis\\'s own default.',\n ),\n },\n },\n ({ path, tuple }) => {\n const prefix = project.config.cssVarPrefix ?? '';\n const activeTuple: Record<string, string> = {};\n for (const axis of project.axes) {\n const candidate = tuple?.[axis.name];\n activeTuple[axis.name] =\n candidate && axis.contexts.includes(candidate) ? candidate : axis.default;\n }\n const themeName =\n project.themes.find((t) => {\n for (const axis of project.axes) {\n if ((t.input as Record<string, string>)[axis.name] !== activeTuple[axis.name]) {\n return false;\n }\n }\n return true;\n })?.name ??\n project.themes[0]?.name ??\n '';\n const token = themeName ? project.themesResolved[themeName]?.[path] : undefined;\n if (!token) return textResult(`Token not found: ${path}`);\n\n const cssVar = `var(--${prefix ? `${prefix}-` : ''}${path.replaceAll('.', '-')})`;\n const attrName = (axis: string): string =>\n prefix ? `data-${prefix}-${axis}` : `data-${axis}`;\n const attrs: Record<string, string> = {};\n const selectorParts: string[] = [];\n for (const axis of project.axes) {\n const value = activeTuple[axis.name];\n if (value !== undefined) {\n attrs[attrName(axis.name)] = value;\n selectorParts.push(`[${attrName(axis.name)}=\"${value}\"]`);\n }\n }\n\n return jsonResult({\n path,\n cssVar,\n value: stringifyValue(token.$value),\n type: token.$type,\n theme: themeName,\n tuple: activeTuple,\n attrs,\n selector: selectorParts.join('') || ':root',\n usageSnippet: `color: ${cssVar};`,\n });\n },\n );\n\n server.registerTool(\n 'get_diagnostics',\n {\n description:\n 'List parser / resolver / validation diagnostics for the project. Each entry carries a severity (`error`, `warn`, `info`), group, message, and optional filename / line / column for locating the issue.',\n inputSchema: {\n severity: z\n .enum(['error', 'warn', 'info'])\n .optional()\n .describe('Optional severity filter. Omit for all diagnostics.'),\n },\n },\n ({ severity }) => {\n const rows = severity\n ? project.diagnostics.filter((d) => d.severity === severity)\n : project.diagnostics;\n return jsonResult({ count: rows.length, diagnostics: rows });\n },\n );\n\n return server;\n}\n\nfunction stringifyValue(value: unknown): string {\n if (value === null || value === undefined) return '—';\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nfunction textResult(text: string): { content: { type: 'text'; text: string }[] } {\n return { content: [{ type: 'text', text }] };\n}\n\nfunction jsonResult(data: unknown): { content: { type: 'text'; text: string }[] } {\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(data, null, 2),\n },\n ],\n };\n}\n","import { extname, isAbsolute, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport type { Config, Project } from '@unpunnyfuns/swatchbook-core';\nimport { loadProject } from '@unpunnyfuns/swatchbook-core';\nimport { createJiti } from 'jiti';\n\n/**\n * Load a swatchbook project from either a full config module or a bare\n * DTCG resolver JSON.\n *\n * `.ts` / `.mts` / `.js` / `.mjs` → jiti imports the module and uses\n * its default export as the swatchbook {@link Config}.\n *\n * `.json` → treated as a DTCG resolver file; the CLI constructs a\n * minimal `{ resolver: path }` config so agents can point at a raw\n * resolver without authoring a wrapper. Every other config option\n * (presets, chrome map, disabled axes, css-var prefix) falls back to\n * `loadProject` defaults.\n *\n * The `cwd` returned is the directory the config / resolver lives in.\n * `loadProject` uses it to resolve relative token references.\n */\nexport async function loadFromConfig(\n configPath: string,\n cwdOverride?: string,\n): Promise<{ project: Project; cwd: string; config: Config }> {\n const absolute = isAbsolute(configPath) ? configPath : resolve(process.cwd(), configPath);\n const cwd = cwdOverride ?? resolve(absolute, '..');\n const ext = extname(absolute).toLowerCase();\n\n const config =\n ext === '.json' ? ({ resolver: absolute } satisfies Config) : await loadTsConfig(absolute);\n\n const project = await loadProject(config, cwd);\n return { project, cwd, config };\n}\n\nasync function loadTsConfig(absolute: string): Promise<Config> {\n /**\n * jiti's first arg is a directory-shaped \"from\" URL it uses to resolve\n * the target's relative imports. Passing a file URL leaves jiti\n * unsure whether to treat the path as a dir, which on some Node\n * versions falls through to a plain JSON read and surfaces as\n * `Unexpected token 'i', \"import { d\"...`. A trailing slash on a\n * directory URL avoids the ambiguity.\n */\n const fromUrl = new URL('./', pathToFileURL(absolute));\n const jiti = createJiti(fromUrl.href, {\n interopDefault: true,\n moduleCache: false,\n });\n return (await jiti.import(absolute, { default: true })) as Config;\n}\n"],"mappings":";;;;;;;;AA4BA,SAAgB,YAAY,KAAc,QAA+C;AACvF,KAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;CAC5C,MAAM,aAAa;AAEnB,KAAI,WAAW,MACb,QAAO;EAAE;EAAQ,OAAO,KAAK,UAAU,IAAI;EAAE,YAAY;EAAO;CAGlE,MAAM,aAAa,WAAW,cAAc,WAAW;AACvD,KAAI,CAAC,cAAc,CAAC,WAAW,WAAY,QAAO;CAElD,IAAI;AACJ,KAAI;EAEF,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,KADV,WAAW,KAAK,MAAO,KAAK,OAAO,IAAI,EAAG;AAEzD,UAAQ,IAAI,MAAM,WAAW,YAAY;GAAC;GAAG;GAAG;GAAE,EAAE,WAAW,SAAS,EAAE;SACpE;AACN,SAAO;;AAGT,KAAI,WAAW,MACb,KAAI;EACF,MAAM,SAAS,MAAM,GAAG,OAAO;AAE/B,MADmB,CAAC,OAAO,SAAS,CAElC,QAAO;GAAE;GAAQ,OAAO,OAAO,SAAS,EAAE,QAAQ,OAAO,CAAC;GAAE,YAAY;GAAM;AAEhF,SAAO;GAAE;GAAQ,OAAO,OAAO,SAAS,EAAE,QAAQ,OAAO,CAAC;GAAE,YAAY;GAAO;SACzE;AACN,SAAO;;AAIX,KAAI,WAAW,MACb,KAAI;EACF,MAAM,SAAS,MAAM,GAAG,OAAO;AAC/B,SAAO;GACL;GACA,OAAO,OAAO,SAAS,EAAE,QAAQ,OAAO,CAAC;GACzC,YAAY,CAAC,OAAO,SAAS;GAC9B;SACK;AACN,SAAO;;AAIX,KAAI,WAAW,MACb,KAAI;AAEF,SAAO;GACL;GACA,OAHY,MAAM,GAAG,MAAM,CAGd,UAAU;GACvB,YAAY,CAAC,MAAM,GAAG,OAAO,CAAC,SAAS;GACxC;SACK;AACN,SAAO;;AAIX,KAAI,WAAW,QACb,KAAI;AAEF,SAAO;GAAE;GAAQ,OADD,MAAM,GAAG,QAAQ,CACD,UAAU;GAAE,YAAY;GAAO;SACzD;AACN,SAAO;;AAIX,QAAO;;AAGT,MAAa,oBAA4C;CAAC;CAAO;CAAO;CAAO;CAAS;CAAM;AAE9F,SAAgB,oBAAoB,KAA+D;CACjG,MAAM,MAAuD,EAAE;AAC/D,MAAK,MAAM,UAAU,mBAAmB;EACtC,MAAM,SAAS,YAAY,KAAK,OAAO;AACvC,MAAI,OAAQ,KAAI,UAAU;;AAE5B,QAAO;;;;;;;;;;;ACpGT,SAAgB,UAAU,MAAc,QAAqC;AAC3E,KAAI,CAAC,OAAQ,QAAO;AACpB,KAAI,WAAW,QAAQ,WAAW,IAAK,QAAO;CAE9C,MAAM,eAAe,KAAK,MAAM,IAAI;CACpC,MAAM,iBAAiB,OAAO,MAAM,IAAI;CAExC,IAAI,KAAK;CACT,IAAI,KAAK;AACT,QAAO,KAAK,eAAe,QAAQ;EACjC,MAAM,OAAO,eAAe;AAC5B,MAAI,SAAS,MAAM;AAGjB,OAAI,OAAO,eAAe,SAAS,EAAG,QAAO;GAC7C,MAAM,YAAY,eAAe,MAAM,KAAK,EAAE;AAC9C,QAAK,IAAI,IAAI,IAAI,KAAK,aAAa,QAAQ,IACzC,KAAI,UAAU,aAAa,MAAM,EAAE,CAAC,KAAK,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC,CACjE,QAAO;AAGX,UAAO;;AAET,MAAI,MAAM,aAAa,OAAQ,QAAO;EACtC,MAAM,OAAO,aAAa;AAC1B,MAAI,SAAS,KAAK,YAEP,SAAS,KAClB,QAAO;AAET;AACA;;AAEF,QAAO,OAAO,aAAa;;;;;;;;;;;AC1B7B,SAAgB,aAAa,SAE3B;CACA,IAAI,UAAU;CACd,MAAM,SAAS,IAAI,UACjB;EACE,MAAM;EACN,SAAS,QAAQ,OAAO,eAAe,WAAW,QAAQ,OAAO,iBAAiB;EACnF,EACD,EACE,cACE,8LACH,CACF;AACD,QAAO,cAAc,SAAkB;AACrC,YAAU;;AAGZ,QAAO,aACL,oBACA;EACE,aACE;EACF,aAAa,EAAE;EAChB,QACK;EACJ,MAAM,aAAqC,EAAE;EAC7C,MAAM,iBAAyC,EAAE;AACjD,OAAK,MAAM,SAAS,QAAQ,QAAQ;GAClC,MAAM,SAAS,QAAQ,eAAe,MAAM,SAAS,EAAE;AACvD,kBAAe,MAAM,QAAQ,OAAO,KAAK,OAAO,CAAC;AACjD,QAAK,MAAM,SAAS,OAAO,OAAO,OAAO,CACvC,KAAI,MAAM,MAAO,YAAW,MAAM,UAAU,WAAW,MAAM,UAAU,KAAK;;EAGhF,MAAM,iBAAiB;GAAE,OAAO;GAAG,MAAM;GAAG,MAAM;GAAG;AACrD,OAAK,MAAM,KAAK,QAAQ,YACtB,gBAAe,EAAE,aAAa,eAAe,EAAE,aAAa,KAAK;AAEnE,SAAO,WAAW;GAChB,cAAc,QAAQ,OAAO,gBAAgB;GAC7C,MAAM,QAAQ,KAAK,KAAK,OAAO;IAAE,MAAM,EAAE;IAAM,UAAU,EAAE;IAAU,SAAS,EAAE;IAAS,EAAE;GAC3F,QAAQ,QAAQ,OAAO,KAAK,MAAM,EAAE,KAAK;GACzC,cAAc,QAAQ,OAAO,IAAI,QAAQ;GACzC,SAAS,QAAQ,QAAQ,KAAK,MAAM,EAAE,KAAK;GAC3C;GACA,OAAO;GACP,aAAa;IACX,QAAQ;IACR,OAAO,QAAQ,YAAY;IAC5B;GACF,CAAC;GAEL;AAED,QAAO,aACL,YACA;EACE,aACE;EACF,aAAa,EAAE;EAChB,QACK,WAAW,WAAW,QAAQ,CAAC,CACtC;AAED,QAAO,aACL,eACA;EACE,aACE;EACF,aAAa;GACX,QAAQ,EACL,QAAQ,CACR,UAAU,CACV,SAAS,4EAA4E;GACxF,MAAM,EACH,QAAQ,CACR,UAAU,CACV,SAAS,6EAA6E;GACzF,OAAO,EACJ,QAAQ,CACR,UAAU,CACV,SAAS,yEAAyE;GACtF;EACF,GACA,EAAE,QAAQ,MAAM,YAAY;EAC3B,MAAM,YAAY,SAAS,QAAQ,OAAO,IAAI;AAC9C,MAAI,CAAC,UACH,QAAO,WAAW,wBAAwB;EAE5C,MAAM,SAAS,QAAQ,eAAe,cAAc,EAAE;EACtD,MAAM,OAAyD,EAAE;AACjE,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,EAAE;AAClD,OAAI,QAAQ,MAAM,UAAU,KAAM;AAClC,OAAI,CAAC,UAAU,MAAM,OAAO,CAAE;AAC9B,QAAK,KAAK;IACR;IACA,GAAI,MAAM,UAAU,KAAA,KAAa,EAAE,MAAM,MAAM,OAAO;IACtD,OAAO,eAAe,MAAM,OAAO;IACpC,CAAC;;AAEJ,OAAK,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,MAAM,KAAA,GAAW,EAAE,SAAS,MAAM,CAAC,CAAC;AAC/E,SAAO,WAAW;GAAE,OAAO;GAAW,OAAO,KAAK;GAAQ,QAAQ;GAAM,CAAC;GAE5E;AAED,QAAO,aACL,aACA;EACE,aACE;EACF,aAAa,EACX,MAAM,EAAE,QAAQ,CAAC,SAAS,iDAAiD,EAC5E;EACF,GACA,EAAE,WAAW;EACZ,MAAM,WAGF,EAAE;EACN,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI,QAAQ;AAEZ,OAAK,MAAM,SAAS,QAAQ,QAAQ;GAClC,MAAM,QAAQ,QAAQ,eAAe,MAAM,QAAQ;AACnD,OAAI,CAAC,MAAO;AACZ,WAAQ;AACR,YAAS,MAAM;AACf,mBAAgB,MAAM;AACtB,iBAAc,MAAM;AACpB,YAAS,MAAM,QAAQ;IACrB,OAAO,eAAe,MAAM,OAAO;IACnC,GAAI,MAAM,YAAY,KAAA,KAAa,EAAE,SAAS,MAAM,SAAS;IAC7D,GAAI,MAAM,eAAe,KAAA,KAAa,EAAE,YAAY,MAAM,YAAY;IACvE;;AAGH,MAAI,CAAC,MAAO,QAAO,WAAW,oBAAoB,OAAO;EAEzD,MAAM,SAAS,QAAQ,OAAO,gBAAgB;EAC9C,MAAM,SAAS,SAAS,SAAS,GAAG,OAAO,KAAK,KAAK,KAAK,WAAW,KAAK,IAAI,CAAC;AAE/E,SAAO,WAAW;GAChB;GACA;GACA;GACA;GACA;GACA;GACD,CAAC;GAEL;AAED,QAAO,aACL,aACA;EACE,aACE;EACF,aAAa,EAAE;EAChB,QAEC,WAAW;EACT,MAAM,QAAQ,KAAK,KAAK,UAAU;GAChC,MAAM,KAAK;GACX,UAAU,KAAK;GACf,SAAS,KAAK;GACd,aAAa,KAAK;GAClB,QAAQ,KAAK;GACd,EAAE;EACH,cAAc,QAAQ;EACtB,QAAQ,QAAQ,OAAO,KAAK,OAAO;GAAE,MAAM,EAAE;GAAM,OAAO,EAAE;GAAO,EAAE;EACrE,SAAS,QAAQ,QAAQ,KAAK,OAAO;GACnC,MAAM,EAAE;GACR,MAAM,EAAE;GACR,aAAa,EAAE;GAChB,EAAE;EACJ,CAAC,CACL;AAED,QAAO,aACL,mBACA;EACE,aACE;EACF,aAAa,EACX,MAAM,EAAE,QAAQ,CAAC,SAAS,iDAAiD,EAC5E;EACF,GACA,EAAE,WAAW;EACZ,MAAM,WAA2E,EAAE;EACnF,IAAI,QAAQ;AACZ,OAAK,MAAM,SAAS,QAAQ,QAAQ;GAClC,MAAM,QAAQ,QAAQ,eAAe,MAAM,QAAQ;AACnD,OAAI,CAAC,MAAO;AACZ,WAAQ;GACR,MAAM,QAAkB,CAAC,KAAK;AAC9B,OAAI,MAAM,cAAc,MAAM,WAAW,SAAS,EAAG,OAAM,KAAK,GAAG,MAAM,WAAW;YAC3E,MAAM,QAAS,OAAM,KAAK,MAAM,QAAQ;AACjD,YAAS,MAAM,QAAQ;IACrB,GAAI,MAAM,YAAY,KAAA,KAAa,EAAE,SAAS,MAAM,SAAS;IAC7D;IACD;;AAEH,MAAI,CAAC,MAAO,QAAO,WAAW,oBAAoB,OAAO;AACzD,SAAO,WAAW;GAAE;GAAM;GAAU,CAAC;GAExC;AAED,QAAO,aACL,kBACA;EACE,aACE;EACF,aAAa;GACX,MAAM,EAAE,QAAQ,CAAC,SAAS,wDAAwD;GAClF,UAAU,EACP,QAAQ,CACR,KAAK,CACL,UAAU,CACV,UAAU,CACV,SAAS,sCAAsC;GACnD;EACF,GACA,EAAE,MAAM,eAAe;EACtB,MAAM,QAAQ,YAAY;EAC1B,MAAM,YAAY,QAAQ,OAAO,IAAI;AACrC,MAAI,CAAC,UAAW,QAAO,WAAW,wBAAwB;EAC1D,MAAM,SAAS,QAAQ,eAAe,cAAc,EAAE;AACtD,MAAI,CAAC,OAAO,MAAO,QAAO,WAAW,oBAAoB,OAAO;EAQhE,MAAM,UAAU,IAAI,IAAY,CAAC,KAAK,CAAC;EACvC,MAAM,QAAQ,SAAiB,MAAoB;GAEjD,MAAM,SADM,OAAO,UACC,aAAa,EAAE;AACnC,OAAI,OAAO,WAAW,EAAG,QAAO;IAAE,MAAM;IAAS,OAAO;IAAG,UAAU,EAAE;IAAE;AACzE,OAAI,KAAK,MAAO,QAAO;IAAE,MAAM;IAAS,OAAO;IAAG,UAAU,EAAE;IAAE,WAAW;IAAM;GACjF,MAAM,WAAmB,EAAE;AAC3B,QAAK,MAAM,KAAK,QAAQ;AACtB,QAAI,QAAQ,IAAI,EAAE,CAAE;AACpB,YAAQ,IAAI,EAAE;AACd,aAAS,KAAK,KAAK,GAAG,IAAI,EAAE,CAAC;;AAE/B,UAAO;IAAE,MAAM;IAAS,OAAO;IAAG;IAAU;;AAG9C,SAAO,WADM,KAAK,MAAM,EAAE,CACH;GAE1B;AAED,QAAO,aACL,qBACA;EACE,aACE;EACF,aAAa;GACX,MAAM,EAAE,QAAQ,CAAC,SAAS,qDAAqD;GAC/E,OAAO,EACJ,QAAQ,CACR,UAAU,CACV,SAAS,4EAA4E;GACzF;EACF,GACA,EAAE,MAAM,YAAY;EACnB,MAAM,YAAY,SAAS,QAAQ,OAAO,IAAI;AAC9C,MAAI,CAAC,UAAW,QAAO,WAAW,wBAAwB;EAC1D,MAAM,QAAQ,QAAQ,eAAe,aAAa;AAClD,MAAI,CAAC,MAAO,QAAO,WAAW,oBAAoB,OAAO;AACzD,MAAI,MAAM,UAAU,QAClB,QAAO,WAAW,SAAS,KAAK,6BAA6B,MAAM,SAAS,UAAU,IAAI;AAE5F,SAAO,WAAW;GAChB;GACA,OAAO;GACP,SAAS,oBAAoB,MAAM,OAAO;GAC3C,CAAC;GAEL;AAED,QAAO,aACL,iBACA;EACE,aACE;EACF,aAAa;GACX,OAAO,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,SAAS,8CAA8C;GAChF,OAAO,EACJ,QAAQ,CACR,UAAU,CACV,SAAS,gEAAgE;GAC5E,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,oCAAoC;GAC5F;EACF,GACA,EAAE,OAAO,OAAO,YAAY;EAC3B,MAAM,YAAY,SAAS,QAAQ,OAAO,IAAI;AAC9C,MAAI,CAAC,UAAW,QAAO,WAAW,wBAAwB;EAC1D,MAAM,SAAS,QAAQ,eAAe,cAAc,EAAE;EACtD,MAAM,SAAS,MAAM,aAAa;EAClC,MAAM,MAAM,SAAS;EACrB,MAAM,OAKA,EAAE;AAER,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,EAAE;GAClD,MAAM,YAAkD,EAAE;AAC1D,OAAI,KAAK,aAAa,CAAC,SAAS,OAAO,CAAE,WAAU,KAAK,OAAO;AAE/D,QADa,MAAM,cAAc,aAAa,GACpC,SAAS,OAAO,CAAE,WAAU,KAAK,cAAc;GACzD,MAAM,QAAQ,eAAe,MAAM,OAAO;AAC1C,OAAI,MAAM,aAAa,CAAC,SAAS,OAAO,CAAE,WAAU,KAAK,QAAQ;AACjE,OAAI,UAAU,WAAW,EAAG;GAM5B,MAAM,QAA+B;IAAE;IAAM;IAAW,SALxC,UAAU,SAAS,cAAc,GAC5C,MAAM,gBAAgB,OACvB,UAAU,SAAS,QAAQ,GACzB,GAAG,KAAK,KAAK,UACb;IAC2D;AACjE,OAAI,MAAM,UAAU,KAAA,EAAW,OAAM,OAAO,MAAM;AAClD,QAAK,KAAK,MAAM;AAChB,OAAI,KAAK,UAAU,IAAK;;AAE1B,OAAK,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,MAAM,KAAA,GAAW,EAAE,SAAS,MAAM,CAAC,CAAC;AAC/E,SAAO,WAAW;GAChB;GACA,OAAO;GACP,OAAO,KAAK;GACZ,WAAW,KAAK,WAAW;GAC3B;GACD,CAAC;GAEL;AAED,QAAO,aACL,iBACA;EACE,aACE;EACF,aAAa;GACX,OAAO,EACJ,OAAO,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAC9B,SAAS,qEAAiE;GAC7E,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,gDAAgD;GACvF,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,SAAS,mDAAmD;GACzF;EACF,GACA,EAAE,OAAO,QAAQ,WAAW;EAC3B,MAAM,SAAiC,EAAE;AACzC,OAAK,MAAM,QAAQ,QAAQ,MAAM;GAC/B,MAAM,YAAY,MAAM,KAAK;AAC7B,UAAO,KAAK,QACV,aAAa,KAAK,SAAS,SAAS,UAAU,GAAG,YAAY,KAAK;;EAEtE,MAAM,YACJ,QAAQ,OAAO,MAAM,MAAM;AACzB,QAAK,MAAM,QAAQ,QAAQ,KACzB,KAAK,EAAE,MAAiC,KAAK,UAAU,OAAO,KAAK,MACjE,QAAO;AAGX,UAAO;IACP,EAAE,QAAQ,QAAQ,OAAO,IAAI;AACjC,MAAI,CAAC,UAAW,QAAO,WAAW,qBAAqB;EACvD,MAAM,SAAS,QAAQ,eAAe,cAAc,EAAE;EACtD,MAAM,WAGF,EAAE;EACN,IAAI,QAAQ;AACZ,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,EAAE;AAClD,OAAI,QAAQ,MAAM,UAAU,KAAM;AAClC,OAAI,CAAC,UAAU,MAAM,OAAO,CAAE;AAC9B,YAAS,QAAQ;IACf,OAAO,eAAe,MAAM,OAAO;IACnC,GAAI,MAAM,UAAU,KAAA,KAAa,EAAE,MAAM,MAAM,OAAO;IACtD,GAAI,MAAM,YAAY,KAAA,KAAa,EAAE,SAAS,MAAM,SAAS;IAC7D,GAAI,MAAM,eAAe,KAAA,KAAa,EAAE,YAAY,MAAM,YAAY;IACvE;AACD;;AAEF,SAAO,WAAW;GAAE,OAAO;GAAW,OAAO;GAAQ;GAAO,QAAQ;GAAU,CAAC;GAElF;AAED,QAAO,aACL,uBACA;EACE,aACE;EACF,aAAa;GACX,MAAM,EAAE,QAAQ,CAAC,SAAS,iDAAiD;GAC3E,OAAO,EACJ,OAAO,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAC9B,UAAU,CACV,SACC,4GACD;GACJ;EACF,GACA,EAAE,MAAM,YAAY;EACnB,MAAM,SAAS,QAAQ,OAAO,gBAAgB;EAC9C,MAAM,cAAsC,EAAE;AAC9C,OAAK,MAAM,QAAQ,QAAQ,MAAM;GAC/B,MAAM,YAAY,QAAQ,KAAK;AAC/B,eAAY,KAAK,QACf,aAAa,KAAK,SAAS,SAAS,UAAU,GAAG,YAAY,KAAK;;EAEtE,MAAM,YACJ,QAAQ,OAAO,MAAM,MAAM;AACzB,QAAK,MAAM,QAAQ,QAAQ,KACzB,KAAK,EAAE,MAAiC,KAAK,UAAU,YAAY,KAAK,MACtE,QAAO;AAGX,UAAO;IACP,EAAE,QACJ,QAAQ,OAAO,IAAI,QACnB;EACF,MAAM,QAAQ,YAAY,QAAQ,eAAe,aAAa,QAAQ,KAAA;AACtE,MAAI,CAAC,MAAO,QAAO,WAAW,oBAAoB,OAAO;EAEzD,MAAM,SAAS,SAAS,SAAS,GAAG,OAAO,KAAK,KAAK,KAAK,WAAW,KAAK,IAAI,CAAC;EAC/E,MAAM,YAAY,SAChB,SAAS,QAAQ,OAAO,GAAG,SAAS,QAAQ;EAC9C,MAAM,QAAgC,EAAE;EACxC,MAAM,gBAA0B,EAAE;AAClC,OAAK,MAAM,QAAQ,QAAQ,MAAM;GAC/B,MAAM,QAAQ,YAAY,KAAK;AAC/B,OAAI,UAAU,KAAA,GAAW;AACvB,UAAM,SAAS,KAAK,KAAK,IAAI;AAC7B,kBAAc,KAAK,IAAI,SAAS,KAAK,KAAK,CAAC,IAAI,MAAM,IAAI;;;AAI7D,SAAO,WAAW;GAChB;GACA;GACA,OAAO,eAAe,MAAM,OAAO;GACnC,MAAM,MAAM;GACZ,OAAO;GACP,OAAO;GACP;GACA,UAAU,cAAc,KAAK,GAAG,IAAI;GACpC,cAAc,UAAU,OAAO;GAChC,CAAC;GAEL;AAED,QAAO,aACL,mBACA;EACE,aACE;EACF,aAAa,EACX,UAAU,EACP,KAAK;GAAC;GAAS;GAAQ;GAAO,CAAC,CAC/B,UAAU,CACV,SAAS,sDAAsD,EACnE;EACF,GACA,EAAE,eAAe;EAChB,MAAM,OAAO,WACT,QAAQ,YAAY,QAAQ,MAAM,EAAE,aAAa,SAAS,GAC1D,QAAQ;AACZ,SAAO,WAAW;GAAE,OAAO,KAAK;GAAQ,aAAa;GAAM,CAAC;GAE/D;AAED,QAAO;;AAGT,SAAS,eAAe,OAAwB;AAC9C,KAAI,UAAU,QAAQ,UAAU,KAAA,EAAW,QAAO;AAClD,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAAW,QAAO,OAAO,MAAM;AACjF,KAAI;AACF,SAAO,KAAK,UAAU,MAAM;SACtB;AACN,SAAO,OAAO,MAAM;;;AAIxB,SAAS,WAAW,MAA6D;AAC/E,QAAO,EAAE,SAAS,CAAC;EAAE,MAAM;EAAQ;EAAM,CAAC,EAAE;;AAG9C,SAAS,WAAW,MAA8D;AAChF,QAAO,EACL,SAAS,CACP;EACE,MAAM;EACN,MAAM,KAAK,UAAU,MAAM,MAAM,EAAE;EACpC,CACF,EACF;;;;;;;;;;;;;;;;;;;;AC/eH,eAAsB,eACpB,YACA,aAC4D;CAC5D,MAAM,WAAW,WAAW,WAAW,GAAG,aAAa,QAAQ,QAAQ,KAAK,EAAE,WAAW;CACzF,MAAM,MAAM,eAAe,QAAQ,UAAU,KAAK;CAGlD,MAAM,SAFM,QAAQ,SAAS,CAAC,aAAa,KAGjC,UAAW,EAAE,UAAU,UAAU,GAAqB,MAAM,aAAa,SAAS;AAG5F,QAAO;EAAE,SADO,MAAM,YAAY,QAAQ,IAAI;EAC5B;EAAK;EAAQ;;AAGjC,eAAe,aAAa,UAAmC;AAc7D,QAAQ,MAJK,WADG,IAAI,IAAI,MAAM,cAAc,SAAS,CAAC,CACtB,MAAM;EACpC,gBAAgB;EAChB,aAAa;EACd,CAAC,CACiB,OAAO,UAAU,EAAE,SAAS,MAAM,CAAC"}
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@unpunnyfuns/swatchbook-mcp",
3
+ "version": "0.10.2",
4
+ "description": "Model Context Protocol server for swatchbook — exposes a DTCG project's tokens, axes, and diagnostics to AI agents without running Storybook.",
5
+ "license": "MIT",
6
+ "author": "unpunnyfuns <unpunnyfuns@gmail.com>",
7
+ "homepage": "https://unpunnyfuns.github.io/swatchbook/",
8
+ "bugs": "https://github.com/unpunnyfuns/swatchbook/issues",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/unpunnyfuns/swatchbook.git",
12
+ "directory": "packages/mcp"
13
+ },
14
+ "keywords": [
15
+ "swatchbook",
16
+ "design-tokens",
17
+ "dtcg",
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "ai",
21
+ "llm"
22
+ ],
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=24.14.0"
26
+ },
27
+ "main": "./dist/index.mjs",
28
+ "types": "./dist/index.d.mts",
29
+ "bin": {
30
+ "swatchbook-mcp": "./dist/bin.mjs"
31
+ },
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.mts",
35
+ "import": "./dist/index.mjs"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "imports": {
40
+ "#/*": "./src/*"
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "sideEffects": false,
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "start": "node dist/bin.mjs --config ../../apps/storybook/swatchbook.config.ts",
52
+ "inspect": "mcp-inspector node dist/bin.mjs -- --config ../../apps/storybook/swatchbook.config.ts",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "lint": "oxlint --deny-warnings -c ../../.oxlintrc.json src test",
57
+ "format": "oxfmt -c ../../.oxfmtrc.json src test",
58
+ "format:check": "oxfmt --check -c ../../.oxfmtrc.json src test"
59
+ },
60
+ "dependencies": {
61
+ "@modelcontextprotocol/sdk": "^1.29.0",
62
+ "@unpunnyfuns/swatchbook-core": "workspace:*",
63
+ "colorjs.io": "0.6.1",
64
+ "jiti": "^2.4.0",
65
+ "zod": "^3.23.8"
66
+ },
67
+ "devDependencies": {
68
+ "@modelcontextprotocol/inspector": "^0.21.2",
69
+ "@types/node": "^25.6.0",
70
+ "tsdown": "^0.21.9",
71
+ "typescript": "^6.0.0",
72
+ "vitest": "^4.1.4"
73
+ }
74
+ }