@adobe/design-data-agent-mcp 1.10.1 → 1.12.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@adobe/design-data-agent-mcp`
2
2
 
3
- MCP server and Claude Code skill for the [Spectrum Design Data](../../packages/design-data/) agent surface. Read tools (`primer`, `resolve_token`, `query_tokens`, `describe_component`, `describe_guideline`) run fully in-process via `@adobe/design-data-wasm` — no CLI binary required for those. Only `authoring_session_step_intent` still invokes the native binary (for NLP suggest ranking, not yet on the wasm surface).
3
+ MCP server and Claude Code skill for the [Spectrum Design Data](../../packages/design-data/) agent surface. Read tools (`primer`, `resolve_token`, `query_tokens`, `describe_component`, `describe_guideline`, `list_guidelines`) run fully in-process via `@adobe/design-data-wasm` — no CLI binary required for those. Only `authoring_session_step_intent` still invokes the native binary (for NLP suggest ranking, not yet on the wasm surface).
4
4
 
5
5
  ## Install
6
6
 
@@ -77,10 +77,11 @@ node tools/design-data-agent-mcp/src/index.js
77
77
  > json`, run from the config's directory) to resolve its `.design-data.toml`
78
78
  > source (path/npm/github/git) and any top-level `manifest` cascade, then
79
79
  > materializes the result to a temp dir and points `primer` / `resolve_token` /
80
- > `query_tokens` / `validate_usage` at it instead of the embedded Spectrum
81
- > snapshot. `describe_component` / `describe_guideline` are unaffected — they always
82
- > read components/guidelines from `@adobe/spectrum-design-data` regardless of
83
- > cascade state. If resolution fails
80
+ > `query_tokens` / `validate_usage` / `describe_component` /
81
+ > `describe_guideline` / `list_guidelines` at it instead of the embedded
82
+ > Spectrum snapshot. Component, relationship, and guideline catalogs are copied
83
+ > from the fallback dataset and overlaid with platform manifest extensions. If
84
+ > resolution fails
84
85
  > (e.g. no network for a github source), the server logs a warning and falls
85
86
  > back to the embedded/local dataset rather than crashing.
86
87
 
@@ -153,6 +154,7 @@ node tools/design-data-agent-mcp/src/index.js
153
154
  | `query_tokens` | Filter tokens by expression |
154
155
  | `describe_component` | Fetch component schema and token bindings |
155
156
  | `describe_guideline` | Fetch guideline documentBlocks content and metadata |
157
+ | `list_guidelines` | List guideline slugs and metadata, optionally by category |
156
158
  | `validate_usage` | Validate token usage and return a diagnostic report |
157
159
  | `diff_datasets` | Compare two datasets and return a semantic diff |
158
160
  | `write` | Write agent-generated product context to the dataset |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/design-data-agent-mcp",
3
- "version": "1.10.1",
3
+ "version": "1.12.0",
4
4
  "description": "MCP server and Claude Code skill for the design-data agent surface — read tools run in-process via wasm",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@modelcontextprotocol/sdk": "^1.27.1",
33
- "@adobe/design-data": "3.1.0",
34
- "@adobe/design-data-wasm": "0.9.0",
35
- "@adobe/spectrum-design-data": "3.2.4"
33
+ "@adobe/design-data": "3.2.1",
34
+ "@adobe/design-data-wasm": "0.10.0",
35
+ "@adobe/spectrum-design-data": "3.2.5"
36
36
  },
37
37
  "devDependencies": {
38
38
  "ava": "^6.0.1"
@@ -6,7 +6,7 @@ description: >
6
6
  system, token lookup, spec-conformance, drift detection, or token authoring on custom data.
7
7
  metadata:
8
8
  author: adobe
9
- version: "1.10.1"
9
+ version: "1.12.0"
10
10
  when_to_use: >
11
11
  Trigger on: design system, design tokens, spec-conformant, drift, validate tokens, token
12
12
  authoring, custom dataset, DESIGN_DATA_PATH, design-data validate, design-data diff,
@@ -67,9 +67,18 @@ all subsequent lookups. No inputs required.
67
67
 
68
68
  ### Resolve a token to its literal value — `resolve_token`
69
69
 
70
- Required: `property` (string) — e.g. `"accent-background-color-default"`
71
- Optional: `colorScheme` (`"light"` or `"dark"`), `scale` (`"desktop"` or `"mobile"`),
72
- `contrast` (`"regular"` or `"high"`)
70
+ Required: `property` (string) — the bare `name.property` segment, e.g. `"background-color"`
71
+ or `"corner-radius"` (see `primer().properties` for the full list). **Not** a flattened
72
+ legacyKey like `"accent-background-color-default"` — that form never resolves.
73
+ Optional context: `colorScheme` (`"light"` or `"dark"`), `scale` (`"desktop"` or `"mobile"`),
74
+ `contrast` (`"regular"` or `"high"`). Optional narrowing: `component`, `variant`, `state`, and
75
+ `colorRole`. Set `excludeDeprecated` to omit tokens with `lifecycle.deprecatedIn`.
76
+
77
+ > **Gotcha:** when several tokens share a property, use `variant`, `state`, or `colorRole` to
78
+ > narrow the match, then check `ambiguous` and `deprecated` on the result. `component` is
79
+ > sparsely populated in the current dataset, so prefer the other fields when available.
80
+ > `resolve_token` still uses cascade ranking when multiple candidates remain; use `query_tokens`
81
+ > to inspect the full candidate set.
73
82
 
74
83
  ### Query tokens by filter expression — `query_tokens`
75
84
 
@@ -83,12 +92,14 @@ Filter syntax examples:
83
92
  ```
84
93
  property=background-color
85
94
  property=*background*
86
- component=button
87
- component=button,state=hover
88
95
  property=background-color|property=border-color
89
96
  $schema=https://spectrum.adobe.com/page/design-token/
90
97
  ```
91
98
 
99
+ > **Gotcha:** `component=<id>` currently always returns `[]` — tokens aren't
100
+ > component-indexed in this dataset. Use `describe_component` to see a component's known
101
+ > token bindings instead.
102
+
92
103
  > **Exit codes:** `0` = matches found; empty array = no matches (not an error).
93
104
 
94
105
  ***
@@ -17,11 +17,22 @@
17
17
  * The CLI already does config discovery, github fetch/cache, and manifest-cascade
18
18
  * application on every data-touching subcommand — this just shells out to it once
19
19
  * (`design-data query --filter "" --format json`, cwd = the config's directory)
20
- * and materializes the result as a single `*.tokens.json` cascade-array file in a
21
- * temp dir, which loadDataset()/validateDataset() already know how to read.
20
+ * and materializes the result as a cascade dataset in a temp dir, which
21
+ * loadDataset()/validateDataset() already know how to read. Catalog directories
22
+ * are copied from the configured fallback dataset and then overlaid with any
23
+ * manifest extensions so component/guideline reads see the same cascade.
22
24
  */
23
25
 
24
- import { existsSync, mkdtempSync, statSync, writeFileSync } from "node:fs";
26
+ import {
27
+ cpSync,
28
+ existsSync,
29
+ mkdirSync,
30
+ mkdtempSync,
31
+ readdirSync,
32
+ readFileSync,
33
+ statSync,
34
+ writeFileSync,
35
+ } from "node:fs";
25
36
  import { tmpdir } from "node:os";
26
37
  import { dirname, join } from "node:path";
27
38
  import { runCli } from "./cli.js";
@@ -32,6 +43,80 @@ function resolveConfigDir(configPath) {
32
43
  return statSync(configPath).isDirectory() ? configPath : dirname(configPath);
33
44
  }
34
45
 
46
+ function resolveManifestPath(configDir) {
47
+ const configPath = join(configDir, ".design-data.toml");
48
+ if (!existsSync(configPath)) return null;
49
+
50
+ const configText = readFileSync(configPath, "utf-8");
51
+ const match = configText.match(/^\s*manifest\s*=\s*["']([^"']+)["']\s*$/m);
52
+ const manifestPath = join(configDir, match?.[1] ?? "manifest.json");
53
+ return existsSync(manifestPath) ? manifestPath : null;
54
+ }
55
+
56
+ function materializeCatalogs(config, cascadeDir, configDir) {
57
+ const manifestPath = resolveManifestPath(configDir);
58
+ let extensionsDir = null;
59
+ if (manifestPath) {
60
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
61
+ extensionsDir = join(
62
+ dirname(manifestPath),
63
+ manifest.extensionsDir ?? "extensions",
64
+ );
65
+ }
66
+
67
+ for (const [key, subdir] of [
68
+ ["componentsDir", "components"],
69
+ ["relationshipsDir", "relationships"],
70
+ ["guidelinesDir", "guidelines"],
71
+ ]) {
72
+ const targetDir = join(cascadeDir, subdir);
73
+ mkdirSync(targetDir, { recursive: true });
74
+ const fallbackDir = config[key];
75
+ if (fallbackDir && existsSync(fallbackDir)) {
76
+ cpSync(fallbackDir, targetDir, { recursive: true, force: true });
77
+ }
78
+ const extensionDir = extensionsDir && join(extensionsDir, subdir);
79
+ if (extensionDir && existsSync(extensionDir)) {
80
+ cpSync(extensionDir, targetDir, { recursive: true, force: true });
81
+ if (subdir === "guidelines") {
82
+ mergeGuidelineManifest(targetDir, extensionDir);
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ function mergeGuidelineManifest(targetDir, extensionDir) {
89
+ const manifestPath = join(targetDir, "manifest.json");
90
+ const manifest = existsSync(manifestPath)
91
+ ? JSON.parse(readFileSync(manifestPath, "utf-8"))
92
+ : { guidelines: [] };
93
+ const entries = new Map(
94
+ (manifest.guidelines ?? []).map((entry) => [entry.slug, entry]),
95
+ );
96
+
97
+ for (const file of readdirSync(extensionDir)) {
98
+ if (!file.endsWith(".json") || file === "manifest.json") continue;
99
+ const document = JSON.parse(
100
+ readFileSync(join(extensionDir, file), "utf-8"),
101
+ );
102
+ const slug = document.name ?? file.replace(/\.json$/, "");
103
+ entries.set(slug, {
104
+ slug,
105
+ ...(document.title && { title: document.title }),
106
+ ...(document.category && { category: document.category }),
107
+ ...(document.status && { status: document.status }),
108
+ ...(document.sourceUrl && { sourceUrl: document.sourceUrl }),
109
+ file: `guidelines/${file}`,
110
+ });
111
+ }
112
+
113
+ writeFileSync(
114
+ manifestPath,
115
+ JSON.stringify({ ...manifest, guidelines: [...entries.values()] }),
116
+ "utf-8",
117
+ );
118
+ }
119
+
35
120
  /**
36
121
  * Resolve `config.designDataConfig`'s cascade and populate
37
122
  * `config.cascadeDataPath` / `config.cascadeActive` with the result.
@@ -74,6 +159,7 @@ export async function bootstrapCascade(config, { run = runCli } = {}) {
74
159
  JSON.stringify(tokens),
75
160
  "utf-8",
76
161
  );
162
+ materializeCatalogs(config, dir, configDir);
77
163
 
78
164
  config.cascadeDataPath = dir;
79
165
  config.cascadeActive = true;
package/src/tools/read.js CHANGED
@@ -15,8 +15,8 @@
15
15
  * required. primer and describe_component were migrated in issue m1r.
16
16
  * describe_guideline (spectrum-design-data-9fe.7) is the read-back counterpart to
17
17
  * data_create/data_edit's "guidelines" category, closing the same read/write
18
- * asymmetry describe_component already closes for components. Its guideline-loading
19
- * logic (including the path-traversal guard) is shared with the sibling
18
+ * asymmetry describe_component already closes for components. Its guideline
19
+ * loading and listing logic is shared with the sibling
20
20
  * @adobe/design-data-mcp package's `design-data-guideline` tool via the
21
21
  * `@adobe/design-data/guideline` module, rather than duplicated here.
22
22
  *
@@ -26,19 +26,17 @@
26
26
  * Cascade scope (see cascade-bootstrap.js / spectrum-design-data-h890.14): once a
27
27
  * `.design-data.toml` cascade is resolved, primer/resolve_token/query_tokens/
28
28
  * validate_usage all reflect it (they read config.cascadeDataPath instead of
29
- * config.dataPath when config.cascadeActive). describe_component/describe_guideline
30
- * do not — components/relationships/guidelines still come from config.componentsDir /
31
- * config.relationshipsDir / config.guidelinesDir, which resolve from the embedded
32
- * @adobe/spectrum-design-data package regardless of cascade state. A platform source
33
- * repo generally carries a token cascade only, not its own component/guideline
34
- * schemas, so this is left out of scope rather than guessed at; revisit if a platform
35
- * manifest starts declaring components or guidelines.
29
+ * config.dataPath when config.cascadeActive). Cascade bootstrap copies the
30
+ * fallback component/relationship/guideline catalogs into that snapshot and
31
+ * overlays manifest extensions, so describe_component/describe_guideline/list_guidelines
32
+ * read the same active cascade while preserving the embedded fallback for inactive
33
+ * cascades.
36
34
  */
37
35
 
38
36
  import { readFileSync, existsSync, readdirSync } from "fs";
39
37
  import { join } from "path";
40
38
  import { loadDataset } from "@adobe/design-data/load";
41
- import { loadGuideline } from "@adobe/design-data/guideline";
39
+ import { listGuidelines, loadGuideline } from "@adobe/design-data/guideline";
42
40
  import { config } from "../config.js";
43
41
  import { checkDatasetFreshness } from "../dataset-freshness.js";
44
42
 
@@ -100,6 +98,15 @@ function validateGuidelineId(id) {
100
98
  validateKebabId(id, "guideline");
101
99
  }
102
100
 
101
+ function getCascadeAwareDir(baseDir, subdir) {
102
+ if (!baseDir || !config.cascadeActive || !config.cascadeDataPath) {
103
+ return baseDir;
104
+ }
105
+
106
+ const cascadeDir = join(config.cascadeDataPath, subdir);
107
+ return existsSync(cascadeDir) ? cascadeDir : baseDir;
108
+ }
109
+
103
110
  export function createReadTools() {
104
111
  return [
105
112
  {
@@ -114,16 +121,16 @@ export function createReadTools() {
114
121
  async handler() {
115
122
  // Shape note: this response intentionally diverges from the CLI PrimerData
116
123
  // struct (sdk/core/src/primer.rs). The CLI emits modeSets as an array of
117
- // {name, values} objects and taxonomyFields as a flat array. This in-process
118
- // shape uses keyed objects (matching the sibling design-data-mcp), which agents
119
- // and the SKILL.md skill prompt consume by key name. Skill contract:
120
- // tokenCount, modeSets.{colorScheme,scale,contrast}, components[],
124
+ // {name, modes, defaultMode} objects and taxonomyFields as a flat array. This
125
+ // in-process shape uses keyed objects (matching the sibling design-data-mcp),
126
+ // which agents and the SKILL.md skill prompt consume by key name. Skill
127
+ // contract: tokenCount, modeSets.{colorScheme,scale,contrast}, components[],
121
128
  // taxonomyFields.{indexed,advisory}. provenance is included for metrics:
122
129
  // for the embedded dataset it carries designDataVersion (@adobe/spectrum-design-data
123
130
  // version baked in at wasm build time); for custom datasets the source differs.
124
131
  const wasm = await getWasm();
125
132
  const ds = await getDataset();
126
- const { provenance } = ds.primer();
133
+ const { provenance, modeSets: rawModeSets } = ds.primer();
127
134
  // Best-effort staleness check (spectrum-design-data-9fe.5) — silent on
128
135
  // failure, never blocks the primer response.
129
136
  const datasetStatus = await checkDatasetFreshness(
@@ -135,11 +142,13 @@ export function createReadTools() {
135
142
  // and consumers should prefer it going forward.
136
143
  source: config.cascadeActive ? "cascade" : "embedded",
137
144
  tokenCount: ds.tokenCount(),
138
- modeSets: {
139
- colorScheme: wasm.getFieldValues("colorScheme") ?? [],
140
- scale: wasm.getFieldValues("scale") ?? [],
141
- contrast: wasm.getFieldValues("contrast") ?? [],
142
- },
145
+ // Mode-set dimensions (colorScheme, scale, contrast) live in
146
+ // graph.mode_sets, not the field-catalog registry, so they're read from
147
+ // ds.primer()'s modeSets array rather than wasm.getFieldValues()
148
+ // (spectrum-design-data-v9bb).
149
+ modeSets: Object.fromEntries(
150
+ rawModeSets.map((ms) => [ms.name, ms.modes]),
151
+ ),
143
152
  taxonomyFields: {
144
153
  indexed: wasm.getIndexedFields(),
145
154
  advisory: wasm.getAdvisoryFields() ?? [],
@@ -156,7 +165,9 @@ export function createReadTools() {
156
165
  {
157
166
  name: "resolve_token",
158
167
  description:
159
- "Resolve a design token property to its final value for a given color scheme, scale, and contrast level.",
168
+ "Resolve a design token property to its final value for a given context. " +
169
+ "Use component, variant, state, or colorRole to disambiguate shared properties; " +
170
+ "inspect ambiguous and deprecated in the result.",
160
171
  inputSchema: {
161
172
  type: "object",
162
173
  required: ["property"],
@@ -164,7 +175,9 @@ export function createReadTools() {
164
175
  property: {
165
176
  type: "string",
166
177
  description:
167
- "Token property name, e.g. accent-background-color-default",
178
+ "Bare token property name (the name.property segment, e.g. " +
179
+ '"background-color", "corner-radius" — see primer().properties for ' +
180
+ "the full list). Not a flattened legacyKey.",
168
181
  },
169
182
  colorScheme: {
170
183
  type: "string",
@@ -180,10 +193,46 @@ export function createReadTools() {
180
193
  enum: ["regular", "high"],
181
194
  description: "Contrast: regular or high",
182
195
  },
196
+ component: {
197
+ type: "string",
198
+ description:
199
+ "Optional component name-object field. It is sparsely populated; " +
200
+ "prefer variant, state, or colorRole when available.",
201
+ },
202
+ variant: {
203
+ type: "string",
204
+ description:
205
+ "Optional token variant used to narrow shared properties.",
206
+ },
207
+ state: {
208
+ type: "string",
209
+ description:
210
+ "Optional token state used to narrow shared properties (including array-valued state).",
211
+ },
212
+ colorRole: {
213
+ type: "string",
214
+ description:
215
+ "Optional color role such as accent, negative, or positive.",
216
+ },
217
+ excludeDeprecated: {
218
+ type: "boolean",
219
+ description:
220
+ "Exclude tokens with lifecycle.deprecatedIn from candidate resolution.",
221
+ },
183
222
  },
184
223
  additionalProperties: false,
185
224
  },
186
- async handler({ property, colorScheme, scale, contrast }) {
225
+ async handler({
226
+ property,
227
+ colorScheme,
228
+ scale,
229
+ contrast,
230
+ component,
231
+ variant,
232
+ state,
233
+ colorRole,
234
+ excludeDeprecated,
235
+ }) {
187
236
  const ds = await loadDataset(
188
237
  config.cascadeActive ? config.cascadeDataPath : config.dataPath,
189
238
  );
@@ -191,10 +240,19 @@ export function createReadTools() {
191
240
  if (colorScheme) context.colorScheme = colorScheme;
192
241
  if (scale) context.scale = scale;
193
242
  if (contrast) context.contrast = contrast;
194
- const result = ds.resolve(property, context);
243
+ const result = ds.resolve(property, context, {
244
+ component,
245
+ variant,
246
+ state,
247
+ colorRole,
248
+ excludeDeprecated: excludeDeprecated ?? false,
249
+ });
195
250
  if (!result) {
251
+ const deprecatedHint = excludeDeprecated
252
+ ? " Only deprecated matches may exist; retry without excludeDeprecated to inspect them."
253
+ : "";
196
254
  throw new Error(
197
- `No token found for property "${property}" in context ${JSON.stringify(context)}`,
255
+ `No token found for property "${property}" in context ${JSON.stringify(context)}.${deprecatedHint}`,
198
256
  );
199
257
  }
200
258
  return result;
@@ -211,7 +269,11 @@ export function createReadTools() {
211
269
  properties: {
212
270
  filter: {
213
271
  type: "string",
214
- description: 'Filter expression, e.g. "category=color"',
272
+ description:
273
+ 'Filter expression, e.g. "property=background-color". Valid keys: ' +
274
+ "property, component, variant, state, colorScheme, scale, contrast, " +
275
+ 'uuid, $schema. Note: "component" is not currently populated on ' +
276
+ "tokens — component=<id> always returns []; use describe_component instead.",
215
277
  },
216
278
  },
217
279
  additionalProperties: false,
@@ -238,7 +300,10 @@ export function createReadTools() {
238
300
  },
239
301
  async handler({ id }) {
240
302
  validateComponentId(id);
241
- const componentsDir = config.componentsDir;
303
+ const componentsDir = getCascadeAwareDir(
304
+ config.componentsDir,
305
+ "components",
306
+ );
242
307
  if (!componentsDir) {
243
308
  throw new Error(
244
309
  `@adobe/spectrum-design-data is not installed — cannot load component "${id}". ` +
@@ -268,7 +333,10 @@ export function createReadTools() {
268
333
  // of the component file into relationships/<id>.json — merge them back
269
334
  // in so callers relying on this tool for a component's token bindings
270
335
  // still see them (see spectrum-design-data-x29.4).
271
- const relationshipsDir = config.relationshipsDir;
336
+ const relationshipsDir = getCascadeAwareDir(
337
+ config.relationshipsDir,
338
+ "relationships",
339
+ );
272
340
  const relationshipFile = relationshipsDir
273
341
  ? join(relationshipsDir, `${id}.json`)
274
342
  : null;
@@ -302,7 +370,10 @@ export function createReadTools() {
302
370
  },
303
371
  async handler({ id }) {
304
372
  validateGuidelineId(id);
305
- const guidelinesDir = config.guidelinesDir;
373
+ const guidelinesDir = getCascadeAwareDir(
374
+ config.guidelinesDir,
375
+ "guidelines",
376
+ );
306
377
  if (!guidelinesDir) {
307
378
  throw new Error(
308
379
  `@adobe/spectrum-design-data is not installed — cannot load guideline "${id}". ` +
@@ -315,9 +386,8 @@ export function createReadTools() {
315
386
  if (err.message.startsWith("Not found:")) {
316
387
  let available;
317
388
  try {
318
- available = readdirSync(guidelinesDir)
319
- .filter((f) => f.endsWith(".json") && f !== "manifest.json")
320
- .map((f) => f.replace(/\.json$/, ""))
389
+ available = listGuidelines(guidelinesDir)
390
+ .map((guideline) => guideline.slug)
321
391
  .sort()
322
392
  .join(", ");
323
393
  } catch {
@@ -332,5 +402,39 @@ export function createReadTools() {
332
402
  }
333
403
  },
334
404
  },
405
+
406
+ {
407
+ name: "list_guidelines",
408
+ description:
409
+ "List available Spectrum design guidelines and their metadata. Optionally " +
410
+ "filter the catalog by category before calling describe_guideline.",
411
+ inputSchema: {
412
+ type: "object",
413
+ properties: {
414
+ category: {
415
+ type: "string",
416
+ description:
417
+ "Optional guideline category, e.g. designing or implementing",
418
+ },
419
+ },
420
+ additionalProperties: false,
421
+ },
422
+ async handler({ category } = {}) {
423
+ const guidelinesDir = getCascadeAwareDir(
424
+ config.guidelinesDir,
425
+ "guidelines",
426
+ );
427
+ if (!guidelinesDir) {
428
+ throw new Error(
429
+ `@adobe/spectrum-design-data is not installed — cannot list guidelines. ` +
430
+ `Install it with: pnpm add @adobe/spectrum-design-data`,
431
+ );
432
+ }
433
+ const guidelines = listGuidelines(guidelinesDir);
434
+ return category
435
+ ? guidelines.filter((guideline) => guideline.category === category)
436
+ : guidelines;
437
+ },
438
+ },
335
439
  ];
336
440
  }