designsource 0.2.1 → 0.3.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 +78 -4
- package/dist/index.js +356 -14
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
# designsource
|
|
6
6
|
|
|
7
|
-
The command line for a [DS source](https://designsource.app) design system.
|
|
7
|
+
The command line for a [DS source](https://designsource.app) design system. Three things it does:
|
|
8
8
|
|
|
9
9
|
- **`integrate`** — add the design system to the app you already have: a `design-system/` folder with your tokens as CSS variables, a prompt that tells your coding agent how to wire them in, and the component kit as reference. Vue, Svelte, Angular or plain CSS.
|
|
10
10
|
- **`create`** — start a new Next.js project from it: tokens, all 28 components and your mockup page, running after `pnpm install && pnpm dev`.
|
|
11
|
+
- **`mcp`** — serve the design system to your coding agent over MCP: tokens, every component's source, your mockup page and a step-by-step integration guide, read on demand instead of pasted. Claude Code, Cursor, Codex — any client that speaks MCP over stdio.
|
|
11
12
|
|
|
12
13
|
```sh
|
|
13
14
|
npx designsource integrate
|
|
@@ -62,22 +63,92 @@ Writes a runnable Next.js app into a new or empty folder (default `my-design-sys
|
|
|
62
63
|
<img src="https://designsource.app/designsource/starter-light-components.png" alt="Component gallery, light" width="49%">
|
|
63
64
|
</p>
|
|
64
65
|
|
|
66
|
+
## `mcp` — serve it to a coding agent
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npx designsource mcp [--token <project token>] [--api <origin>]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Not a command you run in a terminal: put it in your MCP client's config and the agent runs it. The server is plain stdio MCP, so every client takes the same command and env — only the config file's shape differs.
|
|
73
|
+
|
|
74
|
+
| Client | Config | Shape |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| Claude Code | `.mcp.json` in the repo, or `claude mcp add designsource -e DS_SYNC_TOKEN=<project token> -- npx -y designsource mcp` | `mcpServers` JSON |
|
|
77
|
+
| Cursor | `.cursor/mcp.json` | `mcpServers` JSON |
|
|
78
|
+
| Windsurf, Cline, Gemini CLI | their `mcp_config.json` / `settings.json` | `mcpServers` JSON |
|
|
79
|
+
| Codex | `~/.codex/config.toml` | `mcp_servers` TOML |
|
|
80
|
+
| VS Code (Copilot) | `.vscode/mcp.json` | `servers` JSON with `type: "stdio"` |
|
|
81
|
+
| Anything else | its MCP config | command `npx -y designsource mcp`, env `DS_SYNC_TOKEN` |
|
|
82
|
+
|
|
83
|
+
`mcpServers` JSON:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"mcpServers": {
|
|
88
|
+
"designsource": {
|
|
89
|
+
"command": "npx",
|
|
90
|
+
"args": ["-y", "designsource", "mcp"],
|
|
91
|
+
"env": { "DS_SYNC_TOKEN": "<project token>" }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Codex TOML:
|
|
98
|
+
|
|
99
|
+
```toml
|
|
100
|
+
[mcp_servers.designsource]
|
|
101
|
+
command = "npx"
|
|
102
|
+
args = ["-y", "designsource", "mcp"]
|
|
103
|
+
env = { DS_SYNC_TOKEN = "<project token>" }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
VS Code:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"servers": {
|
|
111
|
+
"designsource": {
|
|
112
|
+
"type": "stdio",
|
|
113
|
+
"command": "npx",
|
|
114
|
+
"args": ["-y", "designsource", "mcp"],
|
|
115
|
+
"env": { "DS_SYNC_TOKEN": "<project token>" }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The workbench's Copy dialog hands out all three. Then ask the agent to integrate the design system: in Claude Code, `/mcp__designsource__integrate-design-system` does it; elsewhere, "integrate the design system using the designsource MCP" — the `integrate-design-system` prompt is there for clients that expose MCP prompts, and `get_integration_guide` carries the same steps as a tool for those that don't. Running the agent headless (`claude -p`) needs its MCP tools allowed, e.g. `--allowedTools "mcp__designsource__*"`; interactive sessions prompt instead.
|
|
122
|
+
|
|
123
|
+
| Tool | Returns |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `get_project` | Name, token count, corner-radius strategy, font families, whether there is a mockup page. |
|
|
126
|
+
| `get_tokens` | Tokens as `{path, $type, $value: {light, dark}, ref}`; filter by `tier`, `category`, `component`. |
|
|
127
|
+
| `get_css_variables` | Every token as plain CSS custom properties, light and dark — for any stack. |
|
|
128
|
+
| `get_shadcn_theme` | The shadcn/ui theme stylesheet for Tailwind v4. |
|
|
129
|
+
| `list_components` | The 28 components and the CSS variables each reads. |
|
|
130
|
+
| `get_component_source` | One component's React source (`utils` for the `cn` helper): verbatim in React + Tailwind, the spec to port from elsewhere. |
|
|
131
|
+
| `get_mockup_page` | Your mockup page as a layout spec plus the renderer that paints it. |
|
|
132
|
+
| `get_integration_guide` | The steps: wire the tokens, bring in the components, migrate existing styles, document. |
|
|
133
|
+
|
|
134
|
+
Read-only: nothing an agent does here changes the design system. The project is fetched on the first tool call and refreshed every 60 seconds, so a Save in the workbench shows on the next call. Like every export, it needs the project's account on Pro; on the free plan every tool answers with one line saying so.
|
|
135
|
+
|
|
65
136
|
## Options
|
|
66
137
|
|
|
67
138
|
| Flag | What it does |
|
|
68
139
|
| --- | --- |
|
|
69
|
-
| `folder` | Target folder. `integrate` defaults to `.`, `create` to `my-design-system
|
|
140
|
+
| `folder` | Target folder. `integrate` defaults to `.`, `create` to `my-design-system`; `mcp` takes none. |
|
|
70
141
|
| `--framework` | `integrate` only. Skip the picker: `vue`, `svelte`, `angular` or `css`. |
|
|
71
142
|
| `--token` | The project token, or set `DS_SYNC_TOKEN` for scripts. Asked hidden when neither is given. |
|
|
72
143
|
| `--api` | API origin. Default `https://designsource.app`; point it at a self-hosted instance. |
|
|
73
|
-
| `--force` | Overwrite files that already exist. Without it the command refuses rather than touching them. |
|
|
144
|
+
| `--force` | `integrate` and `create` only. Overwrite files that already exist. Without it the command refuses rather than touching them. |
|
|
74
145
|
|
|
75
146
|
Outside a terminal (CI, pipes) the pickers become numbered prompts, and `designsource` with no command prints usage instead of the menu. Ctrl+C or Esc in a picker exits 130 with nothing written.
|
|
76
147
|
|
|
77
148
|
## Safety
|
|
78
149
|
|
|
79
150
|
- Every write is contained to the target folder. On any failure the command removes what it created, restores what it overwrote, and says so in one line.
|
|
80
|
-
- The project token authorises read-only access to one project. Regenerate it in Project settings to revoke it; the command never stores it.
|
|
151
|
+
- The project token authorises read-only access to one project. Regenerate it in Project settings to revoke it; the command never stores it; `mcp` reads it from the environment your MCP client passes and prints it nowhere.
|
|
81
152
|
- Zero runtime dependencies. Node 20 or newer.
|
|
82
153
|
|
|
83
154
|
## Local development
|
|
@@ -88,8 +159,11 @@ From the [design-system](https://github.com/KaroMourad/design-system) repo, agai
|
|
|
88
159
|
pnpm --filter designsource build
|
|
89
160
|
pnpm --filter designsource start -- integrate --framework vue --api http://localhost:3001
|
|
90
161
|
pnpm --filter designsource start -- create my-app --api http://localhost:3001
|
|
162
|
+
DS_SYNC_TOKEN=<project token> pnpm --filter designsource start -- mcp --api http://localhost:3001
|
|
91
163
|
```
|
|
92
164
|
|
|
165
|
+
For the MCP server, add it to Claude Code with `claude mcp add designsource -e DS_SYNC_TOKEN=… -- node apps/designsource/dist/index.js mcp --api http://localhost:3001` and run `/mcp__designsource__integrate-design-system` in a scratch app.
|
|
166
|
+
|
|
93
167
|
`pnpm --filter designsource test` builds and runs the unit tests plus an end-to-end run of the binary against a throwaway local HTTP server, asserting the token never reaches stdout or stderr.
|
|
94
168
|
|
|
95
169
|
## Publishing
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ var USAGE = `designsource <command> [options] (also installed as: dss)
|
|
|
11
11
|
Commands
|
|
12
12
|
integrate [folder] add the design system to an existing app \u2014 writes design-system/ (default folder: .)
|
|
13
13
|
create [folder] new Next.js project with the tokens, every component and your mockup page (default folder: ${CREATE_DEFAULT_FOLDER})
|
|
14
|
+
mcp serve the design system to a coding agent over MCP (stdio) \u2014 for .mcp.json, not for a terminal
|
|
14
15
|
|
|
15
16
|
Run a command with --help for its options. In a terminal, no command shows a menu.
|
|
16
17
|
`;
|
|
@@ -33,8 +34,18 @@ ${TOKEN_LINE}
|
|
|
33
34
|
${API_LINE}
|
|
34
35
|
--force overwrite files the bundle writes when design-system/ already exists
|
|
35
36
|
`;
|
|
37
|
+
var MCP_USAGE = `designsource mcp [--token <project token>] [--api <origin>]
|
|
38
|
+
|
|
39
|
+
Serves the design system to a coding agent over MCP on stdin/stdout. Put it in your MCP
|
|
40
|
+
client's config (Claude Code, Cursor, Codex\u2026); the token comes from --token or DS_SYNC_TOKEN.
|
|
41
|
+
Read-only: tokens, component source, the mockup page and an integration guide.
|
|
42
|
+
|
|
43
|
+
Options
|
|
44
|
+
${TOKEN_LINE}
|
|
45
|
+
${API_LINE}
|
|
46
|
+
`;
|
|
36
47
|
function usageFor(verb) {
|
|
37
|
-
return verb === "create" ? CREATE_USAGE : verb === "integrate" ? INTEGRATE_USAGE : USAGE;
|
|
48
|
+
return verb === "create" ? CREATE_USAGE : verb === "integrate" ? INTEGRATE_USAGE : verb === "mcp" ? MCP_USAGE : USAGE;
|
|
38
49
|
}
|
|
39
50
|
var ArgsError = class extends Error {
|
|
40
51
|
constructor(message, usage) {
|
|
@@ -53,8 +64,36 @@ function parseArgs(argv, env) {
|
|
|
53
64
|
if (first === void 0) return { kind: "menu" };
|
|
54
65
|
if (HELP.has(first)) return { kind: "help", verb: void 0 };
|
|
55
66
|
if (first === "create" || first === "integrate") return parseVerb(first, rest, env);
|
|
56
|
-
if (first
|
|
57
|
-
throw new ArgsError(
|
|
67
|
+
if (first === "mcp") return parseMcp(rest, env);
|
|
68
|
+
if (first.startsWith("-")) throw new ArgsError("Missing command. Use integrate, create or mcp.", USAGE);
|
|
69
|
+
throw new ArgsError(`Unknown command "${first}". Use integrate, create or mcp.`, USAGE);
|
|
70
|
+
}
|
|
71
|
+
function parseApiOrigin(value, usage) {
|
|
72
|
+
const origin = value.replace(/\/+$/, "");
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = new URL(origin);
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
if (!parsed || !parsed.host) throw new ArgsError(`Option --api needs a full origin such as http://localhost:3001 (got "${value}")`, usage);
|
|
79
|
+
return origin;
|
|
80
|
+
}
|
|
81
|
+
function parseMcp(argv, env) {
|
|
82
|
+
const args = { api: DEFAULT_API_ORIGIN };
|
|
83
|
+
if (env.DS_SYNC_TOKEN) args.token = env.DS_SYNC_TOKEN;
|
|
84
|
+
for (let i = 0; i < argv.length; i++) {
|
|
85
|
+
const arg = argv[i];
|
|
86
|
+
if (arg === "--help" || arg === "-h") return { kind: "help", verb: "mcp" };
|
|
87
|
+
if (!arg.startsWith("--")) throw new ArgsError(arg.startsWith("-") ? `Unknown option ${arg}` : "mcp takes no folder", MCP_USAGE);
|
|
88
|
+
const eq = arg.indexOf("=");
|
|
89
|
+
const key = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
90
|
+
const value = eq === -1 ? argv[++i] : arg.slice(eq + 1);
|
|
91
|
+
if (key !== "token" && key !== "api") throw new ArgsError(`Unknown option --${key}`, MCP_USAGE);
|
|
92
|
+
if (value === void 0) throw new ArgsError(`Option --${key} needs a value`, MCP_USAGE);
|
|
93
|
+
if (key === "token") args.token = value;
|
|
94
|
+
else args.api = parseApiOrigin(value, MCP_USAGE);
|
|
95
|
+
}
|
|
96
|
+
return { kind: "mcp", args };
|
|
58
97
|
}
|
|
59
98
|
function parseVerb(verb, argv, env) {
|
|
60
99
|
const usage = usageFor(verb);
|
|
@@ -84,16 +123,7 @@ function parseVerb(verb, argv, env) {
|
|
|
84
123
|
args.token = value;
|
|
85
124
|
} else if (key === "api") {
|
|
86
125
|
needs();
|
|
87
|
-
|
|
88
|
-
let parsed;
|
|
89
|
-
try {
|
|
90
|
-
parsed = new URL(origin);
|
|
91
|
-
} catch {
|
|
92
|
-
}
|
|
93
|
-
if (!parsed || !parsed.host) {
|
|
94
|
-
throw new ArgsError(`Option --api needs a full origin such as http://localhost:3001 (got "${value}")`, usage);
|
|
95
|
-
}
|
|
96
|
-
args.api = origin;
|
|
126
|
+
args.api = parseApiOrigin(value, usage);
|
|
97
127
|
} else throw new ArgsError(`Unknown option --${key}`, usage);
|
|
98
128
|
continue;
|
|
99
129
|
}
|
|
@@ -119,8 +149,10 @@ var CANCELLED = "Cancelled.";
|
|
|
119
149
|
var TOKEN_MESSAGE = "Project token not recognised. In the workbench open Project settings and copy the project token.";
|
|
120
150
|
var UNREACHABLE = (api) => `Could not reach ${api}.`;
|
|
121
151
|
var UPDATE_MESSAGE = "This design system needs a newer designsource CLI. Update it (for example `npx designsource@latest`) and run again.";
|
|
152
|
+
var PLAN_MESSAGE = "This project's account is on the free plan. Exports need Pro: https://designsource.app/pricing";
|
|
122
153
|
function messageForResponse(status, body, api) {
|
|
123
154
|
if (status === 401) return TOKEN_MESSAGE;
|
|
155
|
+
if (status === 402) return PLAN_MESSAGE;
|
|
124
156
|
const serverMessage = typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
|
|
125
157
|
if (status === 400) return serverMessage ?? `${api} rejected the request (400).`;
|
|
126
158
|
return `${api} answered ${status}.`;
|
|
@@ -468,6 +500,312 @@ async function runIntegrate(args, env, io) {
|
|
|
468
500
|
await scaffold({ folder, framework, token, api: args.api, force: args.force }, env, io);
|
|
469
501
|
}
|
|
470
502
|
|
|
503
|
+
// src/mcp/server.ts
|
|
504
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
505
|
+
|
|
506
|
+
// src/version.ts
|
|
507
|
+
var VERSION = true ? "0.3.0" : "0.0.0-dev";
|
|
508
|
+
|
|
509
|
+
// src/mcp/jsonrpc.ts
|
|
510
|
+
var PARSE_ERROR = -32700;
|
|
511
|
+
var INVALID_REQUEST = -32600;
|
|
512
|
+
var METHOD_NOT_FOUND = -32601;
|
|
513
|
+
var INVALID_PARAMS = -32602;
|
|
514
|
+
function respond(id, result) {
|
|
515
|
+
return { jsonrpc: "2.0", id, result };
|
|
516
|
+
}
|
|
517
|
+
function fail(id, code2, message) {
|
|
518
|
+
return { jsonrpc: "2.0", id, error: { code: code2, message } };
|
|
519
|
+
}
|
|
520
|
+
function encode(response) {
|
|
521
|
+
return `${JSON.stringify(response).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
var isId = (v) => typeof v === "string" || typeof v === "number";
|
|
525
|
+
function parseLine(line) {
|
|
526
|
+
let json;
|
|
527
|
+
try {
|
|
528
|
+
json = JSON.parse(line);
|
|
529
|
+
} catch {
|
|
530
|
+
return { kind: "error", response: fail(null, PARSE_ERROR, "Parse error") };
|
|
531
|
+
}
|
|
532
|
+
if (typeof json !== "object" || json === null || Array.isArray(json)) {
|
|
533
|
+
return { kind: "error", response: fail(null, INVALID_REQUEST, "Invalid Request") };
|
|
534
|
+
}
|
|
535
|
+
const m = json;
|
|
536
|
+
const hasId = "id" in m;
|
|
537
|
+
const id = hasId && isId(m.id) ? m.id : null;
|
|
538
|
+
if (m.jsonrpc !== "2.0" || typeof m.method !== "string" || hasId && !isId(m.id)) {
|
|
539
|
+
return { kind: "error", response: fail(id, INVALID_REQUEST, "Invalid Request") };
|
|
540
|
+
}
|
|
541
|
+
const params = "params" in m ? m.params : void 0;
|
|
542
|
+
if (hasId) {
|
|
543
|
+
const message2 = { jsonrpc: "2.0", id, method: m.method };
|
|
544
|
+
if (params !== void 0) message2.params = params;
|
|
545
|
+
return { kind: "request", message: message2 };
|
|
546
|
+
}
|
|
547
|
+
const message = { jsonrpc: "2.0", method: m.method };
|
|
548
|
+
if (params !== void 0) message.params = params;
|
|
549
|
+
return { kind: "notification", message };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/mcp/prompts.ts
|
|
553
|
+
var INTEGRATE_PROMPT_NAME = "integrate-design-system";
|
|
554
|
+
var INTEGRATE_TEXT = "Integrate the design system this MCP server serves into this repository. Call `get_integration_guide` first and follow it step by step. Detect the stack from the repository itself; do not ask which framework this is.";
|
|
555
|
+
function promptList() {
|
|
556
|
+
return { prompts: [{ name: INTEGRATE_PROMPT_NAME, description: "Integrate the design system into the current repository, following the guide the server provides.", arguments: [] }] };
|
|
557
|
+
}
|
|
558
|
+
function getPrompt(name) {
|
|
559
|
+
if (name !== INTEGRATE_PROMPT_NAME) return null;
|
|
560
|
+
return { description: "Integrate the design system into the current repository.", messages: [{ role: "user", content: { type: "text", text: INTEGRATE_TEXT } }] };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/mcp/tools.ts
|
|
564
|
+
var textResult = (text) => ({ content: [{ type: "text", text }] });
|
|
565
|
+
var errorResult = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
566
|
+
var TIERS = ["design", "semantic", "component"];
|
|
567
|
+
var CATEGORIES = ["color", "spacing", "radius", "typography", "shadow", "zIndex", "border"];
|
|
568
|
+
var NO_INPUT = { type: "object", properties: {}, additionalProperties: false };
|
|
569
|
+
var pretty = (v) => JSON.stringify(v, null, 2);
|
|
570
|
+
var SOURCE_HEADER = "// Reference implementation. Use verbatim in React + Tailwind; port to your stack otherwise, keeping every var() chain.";
|
|
571
|
+
var UTILS_HEADER = "// The `cn` helper every component imports from `@/lib/utils`. Save as lib/utils.ts in React; port or drop elsewhere.";
|
|
572
|
+
var componentsIn = (snapshot) => [...new Set(snapshot.canonical.tokens.map((t) => t.component).filter((c) => c !== null))].sort();
|
|
573
|
+
function optionalEnum(args, key, allowed) {
|
|
574
|
+
const v = args[key];
|
|
575
|
+
if (v === void 0) return { ok: true, value: void 0 };
|
|
576
|
+
if (typeof v !== "string" || !allowed.includes(v)) {
|
|
577
|
+
return { ok: false, error: errorResult(`"${key}" must be one of: ${allowed.join(", ")}.`) };
|
|
578
|
+
}
|
|
579
|
+
return { ok: true, value: v };
|
|
580
|
+
}
|
|
581
|
+
var TOOLS = [
|
|
582
|
+
{
|
|
583
|
+
name: "get_project",
|
|
584
|
+
description: "The design system's name, token count, corner-radius strategy, font families and whether it has a mockup page. Call this first.",
|
|
585
|
+
inputSchema: NO_INPUT,
|
|
586
|
+
run: (snapshot, fetchedAt) => textResult(pretty({ ...snapshot.project, fetchedAt: new Date(fetchedAt).toISOString() }))
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
name: "get_tokens",
|
|
590
|
+
description: "Design tokens as {path, $type, $value: {light, dark}, ref}. Filter by tier (design | semantic | component), category and component; every filter is optional and they combine.",
|
|
591
|
+
inputSchema: {
|
|
592
|
+
type: "object",
|
|
593
|
+
properties: {
|
|
594
|
+
tier: { type: "string", enum: [...TIERS] },
|
|
595
|
+
category: { type: "string", enum: [...CATEGORIES] },
|
|
596
|
+
component: { type: "string", description: "A component name such as button or card (component tier only)." }
|
|
597
|
+
},
|
|
598
|
+
additionalProperties: false
|
|
599
|
+
},
|
|
600
|
+
run: (snapshot, _at, args) => {
|
|
601
|
+
const tier = optionalEnum(args, "tier", TIERS);
|
|
602
|
+
if (!tier.ok) return tier.error;
|
|
603
|
+
const category = optionalEnum(args, "category", CATEGORIES);
|
|
604
|
+
if (!category.ok) return category.error;
|
|
605
|
+
const components = componentsIn(snapshot);
|
|
606
|
+
if (args.component !== void 0 && components.length === 0) return errorResult("This project has no component tokens.");
|
|
607
|
+
const component = optionalEnum(args, "component", components);
|
|
608
|
+
if (!component.ok) return component.error;
|
|
609
|
+
const rows = snapshot.canonical.tokens.filter((t) => (tier.value === void 0 || t.tier === tier.value) && (category.value === void 0 || t.category === category.value) && (component.value === void 0 || t.component === component.value)).map(({ path, $type, $value, ref }) => ({ path, $type, $value, ref }));
|
|
610
|
+
return textResult(rows.length ? pretty(rows) : "No tokens match.");
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
name: "get_css_variables",
|
|
615
|
+
description: "The whole design system as plain CSS custom properties for any stack: design tokens in :root, semantic and component tokens as var() aliases per theme, light and dark via [data-theme] and prefers-color-scheme.",
|
|
616
|
+
inputSchema: NO_INPUT,
|
|
617
|
+
run: (snapshot) => textResult(snapshot.cssVars)
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: "get_shadcn_theme",
|
|
621
|
+
description: "The design system as a shadcn/ui theme stylesheet for Tailwind v4: :root and .dark blocks plus @theme inline. Use on the React + Tailwind path.",
|
|
622
|
+
inputSchema: NO_INPUT,
|
|
623
|
+
run: (snapshot) => textResult(snapshot.shadcnTheme)
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "list_components",
|
|
627
|
+
description: "Every component in the kit with the CSS variables its source reads, plus the helper files. Follow with get_component_source.",
|
|
628
|
+
inputSchema: NO_INPUT,
|
|
629
|
+
run: (snapshot) => textResult(
|
|
630
|
+
pretty({
|
|
631
|
+
components: Object.entries(snapshot.kit.variables).sort(([a], [b]) => a.localeCompare(b)).map(([name, variables]) => ({ name, variables })),
|
|
632
|
+
helpers: Object.keys(snapshot.kit.sources).filter((n) => !(n in snapshot.kit.variables)).sort()
|
|
633
|
+
})
|
|
634
|
+
)
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: "get_component_source",
|
|
638
|
+
description: 'The React + Tailwind source of one component (see list_components), or "utils" for the cn helper. Use verbatim in React; port to any other stack keeping every var() chain.',
|
|
639
|
+
inputSchema: { type: "object", properties: { name: { type: "string", description: "A name from list_components, or utils." } }, required: ["name"], additionalProperties: false },
|
|
640
|
+
run: (snapshot, _at, args) => {
|
|
641
|
+
const names = Object.keys(snapshot.kit.sources).sort();
|
|
642
|
+
const name = args.name;
|
|
643
|
+
if (typeof name !== "string" || !names.includes(name)) return errorResult(`"name" must be one of: ${names.join(", ")}.`);
|
|
644
|
+
const header = name in snapshot.kit.variables ? SOURCE_HEADER : UTILS_HEADER;
|
|
645
|
+
return textResult(`${header}
|
|
646
|
+
${snapshot.kit.sources[name]}`);
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
name: "get_mockup_page",
|
|
651
|
+
description: "The project's own mockup page as a layout spec, followed by the React renderer and schema that paint it. Render it as the first route.",
|
|
652
|
+
inputSchema: NO_INPUT,
|
|
653
|
+
run: (snapshot) => {
|
|
654
|
+
if (!snapshot.mockup) return textResult("This project has no mockup page.");
|
|
655
|
+
const files = Object.entries(snapshot.mockup.renderer).sort(([a], [b]) => a.localeCompare(b)).map(([file, source]) => `
|
|
656
|
+
--- file: ${file}
|
|
657
|
+
${source}`);
|
|
658
|
+
return textResult(`${pretty(snapshot.mockup.spec)}
|
|
659
|
+
${files.join("\n")}`);
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: "get_integration_guide",
|
|
664
|
+
description: "Step-by-step instructions for integrating this design system into the current repository: wire the tokens, bring in the components, migrate existing styles, document. Read this before changing anything.",
|
|
665
|
+
inputSchema: NO_INPUT,
|
|
666
|
+
run: (snapshot) => textResult(snapshot.guide)
|
|
667
|
+
}
|
|
668
|
+
];
|
|
669
|
+
var TOOL_NAMES = TOOLS.map((t) => t.name);
|
|
670
|
+
function toolList() {
|
|
671
|
+
return { tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) };
|
|
672
|
+
}
|
|
673
|
+
function findTool(name) {
|
|
674
|
+
return TOOLS.find((t) => t.name === name);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/mcp/protocol.ts
|
|
678
|
+
var SUPPORTED_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
679
|
+
var asObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
680
|
+
async function handle(parsed, deps) {
|
|
681
|
+
if (parsed.kind === "error") return parsed.response;
|
|
682
|
+
if (parsed.kind === "notification") return null;
|
|
683
|
+
return request(parsed.message, deps);
|
|
684
|
+
}
|
|
685
|
+
async function request(msg, deps) {
|
|
686
|
+
const { id } = msg;
|
|
687
|
+
const params = asObject(msg.params);
|
|
688
|
+
switch (msg.method) {
|
|
689
|
+
case "initialize": {
|
|
690
|
+
const asked = params.protocolVersion;
|
|
691
|
+
const protocolVersion = typeof asked === "string" && SUPPORTED_VERSIONS.includes(asked) ? asked : SUPPORTED_VERSIONS[0];
|
|
692
|
+
return respond(id, { protocolVersion, capabilities: { tools: {}, prompts: {} }, serverInfo: { name: "designsource", version: deps.version } });
|
|
693
|
+
}
|
|
694
|
+
case "ping":
|
|
695
|
+
return respond(id, {});
|
|
696
|
+
case "tools/list":
|
|
697
|
+
return respond(id, toolList());
|
|
698
|
+
case "tools/call": {
|
|
699
|
+
if (typeof params.name !== "string") return fail(id, INVALID_PARAMS, 'tools/call needs a string "name"');
|
|
700
|
+
const tool = findTool(params.name);
|
|
701
|
+
if (!tool) return fail(id, INVALID_PARAMS, `Unknown tool: ${params.name}`);
|
|
702
|
+
let cached;
|
|
703
|
+
try {
|
|
704
|
+
cached = await deps.snapshot();
|
|
705
|
+
} catch (e) {
|
|
706
|
+
if (e instanceof CliError) return respond(id, errorResult(e.message));
|
|
707
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
708
|
+
deps.warn(`designsource mcp: loading the design system failed: ${message}`);
|
|
709
|
+
return respond(id, errorResult(`Could not load the design system: ${message}`));
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
return respond(id, tool.run(cached.snapshot, cached.fetchedAt, asObject(params.arguments)));
|
|
713
|
+
} catch (e) {
|
|
714
|
+
if (e instanceof CliError) return respond(id, errorResult(e.message));
|
|
715
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
716
|
+
deps.warn(`designsource mcp: tool ${params.name} failed: ${message}`);
|
|
717
|
+
return respond(id, errorResult(`Tool ${params.name} failed: ${message}`));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
case "prompts/list":
|
|
721
|
+
return respond(id, promptList());
|
|
722
|
+
case "prompts/get": {
|
|
723
|
+
const prompt = typeof params.name === "string" ? getPrompt(params.name) : null;
|
|
724
|
+
if (!prompt) return fail(id, INVALID_PARAMS, `Unknown prompt: ${String(params.name)}`);
|
|
725
|
+
return respond(id, prompt);
|
|
726
|
+
}
|
|
727
|
+
default:
|
|
728
|
+
return fail(id, METHOD_NOT_FOUND, `Method not found: ${msg.method}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/mcp/snapshot-client.ts
|
|
733
|
+
var malformed2 = () => new CliError("Unexpected response from the API \u2014 not a design system snapshot.");
|
|
734
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
735
|
+
var isStringRecord = (v) => isObject(v) && Object.values(v).every((x) => typeof x === "string");
|
|
736
|
+
var isStringListRecord = (v) => isObject(v) && Object.values(v).every((x) => Array.isArray(x) && x.every((s) => typeof s === "string"));
|
|
737
|
+
function validateSnapshot(json) {
|
|
738
|
+
if (!isObject(json)) throw malformed2();
|
|
739
|
+
if (typeof json.version !== "number") throw malformed2();
|
|
740
|
+
if (json.version !== 1) throw new CliError(UPDATE_MESSAGE);
|
|
741
|
+
const { project, canonical, cssVars, shadcnTheme, kit, mockup, guide } = json;
|
|
742
|
+
if (!isObject(project) || typeof project.name !== "string" || typeof project.radiusStrategy !== "string" || typeof project.tokenCount !== "number" || typeof project.hasMockup !== "boolean" || !isStringRecord(project.fonts)) throw malformed2();
|
|
743
|
+
if (!isObject(canonical) || !Array.isArray(canonical.tokens) || !canonical.tokens.every(isObject)) throw malformed2();
|
|
744
|
+
if (typeof cssVars !== "string" || typeof shadcnTheme !== "string" || typeof guide !== "string") throw malformed2();
|
|
745
|
+
if (!isObject(kit) || !isStringRecord(kit.sources) || !isStringListRecord(kit.variables)) throw malformed2();
|
|
746
|
+
if (mockup !== null && !(isObject(mockup) && isObject(mockup.spec) && isStringRecord(mockup.renderer))) throw malformed2();
|
|
747
|
+
return json;
|
|
748
|
+
}
|
|
749
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
750
|
+
async function fetchSnapshot(opts, fetchImpl = fetch) {
|
|
751
|
+
const url = new URL("/api/snapshot", `${opts.api}/`);
|
|
752
|
+
let res;
|
|
753
|
+
try {
|
|
754
|
+
res = await fetchImpl(url.toString(), {
|
|
755
|
+
headers: { Authorization: `Bearer ${opts.token}`, Accept: "application/json" },
|
|
756
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
757
|
+
});
|
|
758
|
+
} catch {
|
|
759
|
+
throw new CliError(UNREACHABLE(opts.api));
|
|
760
|
+
}
|
|
761
|
+
const body = await res.json().catch(() => null);
|
|
762
|
+
if (!res.ok) throw new CliError(messageForResponse(res.status, body, opts.api));
|
|
763
|
+
return validateSnapshot(body);
|
|
764
|
+
}
|
|
765
|
+
function createSnapshotCache(fetcher, opts = {}) {
|
|
766
|
+
const ttl = opts.ttlMs ?? 6e4;
|
|
767
|
+
const now = opts.now ?? Date.now;
|
|
768
|
+
let cached = null;
|
|
769
|
+
let inflight = null;
|
|
770
|
+
return {
|
|
771
|
+
get() {
|
|
772
|
+
if (cached && now() - cached.fetchedAt < ttl) return Promise.resolve(cached);
|
|
773
|
+
if (inflight) return inflight;
|
|
774
|
+
inflight = fetcher().then((snapshot) => {
|
|
775
|
+
cached = { snapshot, fetchedAt: now() };
|
|
776
|
+
return cached;
|
|
777
|
+
}).catch((e) => {
|
|
778
|
+
if (!cached) throw e;
|
|
779
|
+
opts.warn?.(`Could not refresh the design system; serving the snapshot from ${new Date(cached.fetchedAt).toISOString()}: ${e instanceof Error ? e.message : String(e)}`);
|
|
780
|
+
return cached;
|
|
781
|
+
}).finally(() => {
|
|
782
|
+
inflight = null;
|
|
783
|
+
});
|
|
784
|
+
return inflight;
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/mcp/server.ts
|
|
790
|
+
async function runMcp(args, io, streams = { input: process.stdin, output: process.stdout }) {
|
|
791
|
+
const token = args.token;
|
|
792
|
+
if (!token) throw new CliError("A project token is required: pass --token or set DS_SYNC_TOKEN (workbench \u2192 Project settings).");
|
|
793
|
+
const warn = (line) => io.stderr(`${line}
|
|
794
|
+
`);
|
|
795
|
+
const cache = createSnapshotCache(() => fetchSnapshot({ api: args.api, token }, io.fetchImpl), { warn });
|
|
796
|
+
const deps = { version: VERSION, snapshot: () => cache.get(), warn };
|
|
797
|
+
const lines = createInterface2({ input: streams.input, crlfDelay: Infinity });
|
|
798
|
+
streams.output.on("error", () => {
|
|
799
|
+
lines.close();
|
|
800
|
+
});
|
|
801
|
+
for await (const raw of lines) {
|
|
802
|
+
const line = raw.trim();
|
|
803
|
+
if (!line) continue;
|
|
804
|
+
const response = await handle(parseLine(line), deps);
|
|
805
|
+
if (response) streams.output.write(encode(response));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
471
809
|
// src/index.ts
|
|
472
810
|
var MENU = [
|
|
473
811
|
{ value: "integrate", label: "Add to an existing project", hint: "writes design-system/ \u2014 tokens, prompt, reference kit" },
|
|
@@ -493,7 +831,7 @@ ${e.usage}`);
|
|
|
493
831
|
try {
|
|
494
832
|
if (command.kind === "menu") {
|
|
495
833
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
496
|
-
io.stderr(`Missing command. Use integrate or
|
|
834
|
+
io.stderr(`Missing command. Use integrate, create or mcp.
|
|
497
835
|
|
|
498
836
|
${USAGE}`);
|
|
499
837
|
return 1;
|
|
@@ -501,6 +839,10 @@ ${USAGE}`);
|
|
|
501
839
|
const verb = await pick("What do you want to do?", MENU, 0);
|
|
502
840
|
command = verb === "create" ? { kind: "create", args: baseArgs(env) } : { kind: "integrate", args: baseArgs(env) };
|
|
503
841
|
}
|
|
842
|
+
if (command.kind === "mcp") {
|
|
843
|
+
await runMcp(command.args, io);
|
|
844
|
+
return 0;
|
|
845
|
+
}
|
|
504
846
|
if (command.kind === "create") await runCreate(command.args, env, io);
|
|
505
847
|
else await runIntegrate(command.args, env, io);
|
|
506
848
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "designsource",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The DS source CLI: add a design system to an existing app (integrate)
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "The DS source CLI: add a design system to an existing app (integrate), start a new Next.js project from it (create), or serve it to a coding agent over MCP (mcp) — tokens, every component and your mockup page.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"shadcn",
|
|
20
20
|
"vue",
|
|
21
21
|
"svelte",
|
|
22
|
-
"angular"
|
|
22
|
+
"angular",
|
|
23
|
+
"mcp",
|
|
24
|
+
"model-context-protocol"
|
|
23
25
|
],
|
|
24
26
|
"type": "module",
|
|
25
27
|
"bin": {
|