@adobe/design-data-mcp 1.1.0 → 1.2.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/LICENSE CHANGED
@@ -186,7 +186,7 @@ file or class name and description of purpose be included on the
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
189
+ Copyright 2026 Adobe
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/design-data-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for Spectrum design tokens and component schemas via the design-data CLI",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -27,7 +27,9 @@
27
27
  "provenance": true
28
28
  },
29
29
  "dependencies": {
30
- "@modelcontextprotocol/sdk": "^1.27.1"
30
+ "@modelcontextprotocol/sdk": "^1.27.1",
31
+ "@adobe/design-data-wasm": "0.1.0",
32
+ "@adobe/spectrum-design-data": "0.3.0"
31
33
  },
32
34
  "devDependencies": {
33
35
  "ava": "^6.0.1"
@@ -9,57 +9,83 @@
9
9
  // governing permissions and limitations under the License.
10
10
 
11
11
  /**
12
- * MCP tool definitions that wrap the @adobe/design-data CLI.
12
+ * MCP tool definitions for @adobe/design-data-mcp.
13
13
  *
14
- * Each tool shells out to `npx @adobe/design-data <subcommand>` and returns
15
- * the parsed JSON output. The CLI handles data resolution automatically —
16
- * it uses the embedded Spectrum snapshot when no `.design-data.toml` is present,
17
- * or the configured source/version if one exists in the project.
14
+ * All tools run in-process via @adobe/design-data-wasm no CLI binary or npx
15
+ * required. Dataset.embedded() provides the canonical Spectrum snapshot with
16
+ * zero configuration.
18
17
  */
19
18
 
20
- import { spawnSync } from "child_process";
19
+ import { createRequire } from "module";
20
+ import { readFileSync, existsSync } from "fs";
21
+ import { join, dirname } from "path";
21
22
 
22
- /** npx executable name — .cmd suffix required on Windows without shell: true. */
23
- const NPX = process.platform === "win32" ? "npx.cmd" : "npx";
23
+ let _wasm;
24
+ /** Lazy-load and cache the wasm module (nodejs target, no init() required). */
25
+ async function getWasm() {
26
+ if (!_wasm) _wasm = await import("@adobe/design-data-wasm");
27
+ return _wasm;
28
+ }
24
29
 
30
+ let _dataset;
25
31
  /**
26
- * Run `npx -y @adobe/design-data` with the given args and return parsed JSON stdout.
27
- *
28
- * Uses spawnSync (blocking) intentionally: this MCP server serves a single
29
- * stdio client and processes requests sequentially, so blocking the event
30
- * loop is safe. The -y flag suppresses the interactive "install?" prompt on
31
- * first run so the server doesn't hang waiting for user input.
32
+ * Return the embedded Spectrum dataset, caching it after first access.
32
33
  *
33
- * Throws if the process can't start (result.error) or exits non-zero.
34
+ * Dataset.embedded() clones the in-memory graph on every call; caching here
35
+ * avoids that per-request cost.
34
36
  */
35
- function runDesignData(args) {
36
- const result = spawnSync(NPX, ["-y", "@adobe/design-data", ...args], {
37
- encoding: "utf8",
38
- // Allow up to 30s for first-run install + binary execution.
39
- timeout: 30_000,
40
- });
41
-
42
- // result.error is set when the process can't start at all (binary not found,
43
- // timeout exceeded, etc.) — check before result.status.
44
- if (result.error) throw result.error;
45
-
46
- if (result.status !== 0) {
47
- const msg = (result.stderr || result.stdout || "").trim();
48
- throw new Error(`design-data exited ${result.status}: ${msg}`);
37
+ async function getDataset() {
38
+ if (!_dataset) {
39
+ const wasm = await getWasm();
40
+ _dataset = wasm.Dataset.embedded();
49
41
  }
42
+ return _dataset;
43
+ }
44
+
45
+ /**
46
+ * Score tokens by keyword-overlap against an intent string.
47
+ *
48
+ * Pure function — accepts the token array so it can be tested independently.
49
+ *
50
+ * @param {object[]} tokens - Array of token result objects with a `name` string.
51
+ * @param {string} intent - Natural-language intent to match against.
52
+ * @param {number} limit - Maximum results to return.
53
+ * @returns {{ name: string, confidence: number, uuid: string, raw: unknown }[]}
54
+ */
55
+ export function scoreTokensByKeyword(tokens, intent, limit = 5) {
56
+ const words = intent.toLowerCase().split(/\s+/).filter(Boolean);
57
+ if (words.length === 0) return [];
58
+ return tokens
59
+ .map((token) => {
60
+ const nameStr = token.name?.toLowerCase() ?? "";
61
+ const matches = words.filter((w) => nameStr.includes(w)).length;
62
+ const confidence = matches / words.length;
63
+ return { token, confidence };
64
+ })
65
+ .filter(({ confidence }) => confidence > 0)
66
+ .sort((a, b) => b.confidence - a.confidence)
67
+ .slice(0, limit)
68
+ .map(({ token, confidence }) => ({
69
+ name: token.name,
70
+ confidence: Math.round(confidence * 100) / 100,
71
+ uuid: token.uuid,
72
+ raw: token.raw,
73
+ }));
74
+ }
50
75
 
76
+ /** Return the @adobe/spectrum-design-data package root directory, or null. */
77
+ function resolveSpectrumDataPackage() {
51
78
  try {
52
- return JSON.parse(result.stdout);
79
+ const req = createRequire(import.meta.url);
80
+ return dirname(req.resolve("@adobe/spectrum-design-data/package.json"));
53
81
  } catch {
54
- // Some commands (e.g. component) output valid JSON but without --format json.
55
- // If JSON.parse fails, return the raw string so the caller can decide.
56
- return result.stdout.trim();
82
+ return null;
57
83
  }
58
84
  }
59
85
 
60
86
  export function createDesignDataTools() {
61
87
  return [
62
- // ── primer ────────────────────────────────────────────────────────────────
88
+ // ── primer ─────────────────────────────────────────────────────────────
63
89
  {
64
90
  name: "design-data-primer",
65
91
  description:
@@ -72,12 +98,28 @@ export function createDesignDataTools() {
72
98
  properties: {},
73
99
  additionalProperties: false,
74
100
  },
75
- handler() {
76
- return runDesignData(["primer", "--format", "json"]);
101
+ async handler() {
102
+ const [wasm, ds] = await Promise.all([getWasm(), getDataset()]);
103
+
104
+ return {
105
+ source: "embedded",
106
+ tokenCount: ds.tokenCount(),
107
+ modeSets: {
108
+ colorScheme: wasm.getFieldValues("colorScheme") ?? [],
109
+ scale: wasm.getFieldValues("scale") ?? [],
110
+ contrast: wasm.getFieldValues("contrast") ?? [],
111
+ },
112
+ taxonomyFields: {
113
+ indexed: wasm.getIndexedFields(),
114
+ advisory: wasm.getAdvisoryFields() ?? [],
115
+ },
116
+ components: wasm.getFieldValues("component") ?? [],
117
+ properties: wasm.getFieldValues("property") ?? [],
118
+ };
77
119
  },
78
120
  },
79
121
 
80
- // ── query ─────────────────────────────────────────────────────────────────
122
+ // ── query ───────────────────────────────────────────────────────────────
81
123
  {
82
124
  name: "design-data-query",
83
125
  description:
@@ -97,18 +139,20 @@ export function createDesignDataTools() {
97
139
  required: ["filter"],
98
140
  additionalProperties: false,
99
141
  },
100
- handler({ filter }) {
101
- return runDesignData(["query", "--filter", filter, "--format", "json"]);
142
+ async handler({ filter }) {
143
+ const ds = await getDataset();
144
+ return ds.query(filter);
102
145
  },
103
146
  },
104
147
 
105
- // ── suggest ───────────────────────────────────────────────────────────────
148
+ // ── suggest ─────────────────────────────────────────────────────────────
106
149
  {
107
150
  name: "design-data-suggest",
108
151
  description:
109
- "Suggest Spectrum tokens matching a natural-language intent. " +
110
- "Returns ranked matches with confidence scores, token names, and values. " +
111
- "Use when the user describes what they need rather than knowing the token name.",
152
+ "Suggest Spectrum tokens matching a natural-language intent using keyword-overlap " +
153
+ "scoring. Returns matches ranked by confidence, token names, and values. " +
154
+ "Use when the user describes what they need rather than knowing the token name. " +
155
+ "(TODO: swap to wasm NLP suggest when available for higher-quality ranking.)",
112
156
  inputSchema: {
113
157
  type: "object",
114
158
  properties: {
@@ -126,20 +170,14 @@ export function createDesignDataTools() {
126
170
  required: ["intent"],
127
171
  additionalProperties: false,
128
172
  },
129
- handler({ intent, limit = 5 }) {
130
- return runDesignData([
131
- "suggest",
132
- "--",
133
- intent,
134
- "--format",
135
- "json",
136
- "--limit",
137
- String(limit),
138
- ]);
173
+ async handler({ intent, limit = 5 }) {
174
+ const ds = await getDataset();
175
+ const allTokens = ds.query("");
176
+ return scoreTokensByKeyword(allTokens, intent, limit);
139
177
  },
140
178
  },
141
179
 
142
- // ── component ─────────────────────────────────────────────────────────────
180
+ // ── component ───────────────────────────────────────────────────────────
143
181
  {
144
182
  name: "design-data-component",
145
183
  description:
@@ -159,13 +197,26 @@ export function createDesignDataTools() {
159
197
  required: ["id"],
160
198
  additionalProperties: false,
161
199
  },
162
- handler({ id }) {
163
- // component always outputs JSON — no --format flag.
164
- return runDesignData(["component", "--", id]);
200
+ async handler({ id }) {
201
+ const pkgRoot = resolveSpectrumDataPackage();
202
+ if (!pkgRoot) {
203
+ throw new Error(
204
+ `@adobe/spectrum-design-data is not installed — cannot load component "${id}". ` +
205
+ `Install it with: pnpm add @adobe/spectrum-design-data`,
206
+ );
207
+ }
208
+ const componentFile = join(pkgRoot, "components", `${id}.json`);
209
+ if (!existsSync(componentFile)) {
210
+ throw new Error(
211
+ `Component not found: "${id}". ` +
212
+ `Call design-data-primer to see available component IDs.`,
213
+ );
214
+ }
215
+ return JSON.parse(readFileSync(componentFile, "utf-8"));
165
216
  },
166
217
  },
167
218
 
168
- // ── resolve ───────────────────────────────────────────────────────────────
219
+ // ── resolve ─────────────────────────────────────────────────────────────
169
220
  {
170
221
  name: "design-data-resolve",
171
222
  description:
@@ -186,22 +237,29 @@ export function createDesignDataTools() {
186
237
  },
187
238
  scale: {
188
239
  type: "string",
189
- description: 'Scale mode, e.g. "medium" or "large"',
240
+ description: 'Scale mode, e.g. "desktop" or "mobile"',
190
241
  },
191
242
  contrast: {
192
243
  type: "string",
193
- description: 'Contrast mode, e.g. "standard" or "high"',
244
+ description: 'Contrast mode, e.g. "regular" or "high"',
194
245
  },
195
246
  },
196
247
  required: ["property"],
197
248
  additionalProperties: false,
198
249
  },
199
- handler({ property, colorScheme, scale, contrast }) {
200
- const args = ["resolve", "--", property, "--format", "json"];
201
- if (colorScheme) args.push("--color-scheme", colorScheme);
202
- if (scale) args.push("--scale", scale);
203
- if (contrast) args.push("--contrast", contrast);
204
- return runDesignData(args);
250
+ async handler({ property, colorScheme, scale, contrast }) {
251
+ const ds = await getDataset();
252
+ const context = {};
253
+ if (colorScheme) context.colorScheme = colorScheme;
254
+ if (scale) context.scale = scale;
255
+ if (contrast) context.contrast = contrast;
256
+ const result = ds.resolve(property, context);
257
+ if (!result) {
258
+ throw new Error(
259
+ `No token found for property "${property}" in context ${JSON.stringify(context)}`,
260
+ );
261
+ }
262
+ return result;
205
263
  },
206
264
  },
207
265
  ];