@adobe/design-data-agent-mcp 1.7.2 → 1.8.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/design-data-agent-mcp",
3
- "version": "1.7.2",
3
+ "version": "1.8.1",
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": "2.0.3",
33
+ "@adobe/design-data": "3.0.0",
34
34
  "@adobe/design-data-wasm": "0.4.0",
35
- "@adobe/spectrum-design-data": "0.7.1"
35
+ "@adobe/spectrum-design-data": "0.9.0"
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.7.2"
9
+ version: "1.8.1"
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,
package/src/cli.js CHANGED
@@ -11,7 +11,7 @@
11
11
  import { spawn } from "child_process";
12
12
  import { config } from "./config.js";
13
13
 
14
- export function runCli(args, { timeout = 10_000 } = {}) {
14
+ export function runCli(args, { timeout = 10_000, stdin } = {}) {
15
15
  return new Promise((resolve, reject) => {
16
16
  const proc = spawn(config.bin, args, {
17
17
  // Anchor the CLI's working directory to the resolved data root so its own
@@ -19,8 +19,12 @@ export function runCli(args, { timeout = 10_000 } = {}) {
19
19
  // even when Claude Code launched the server from a monorepo subdirectory.
20
20
  cwd: config.dataRoot,
21
21
  // isolates CLI stdout from the MCP JSON-RPC stream on the parent's stdout
22
- stdio: ["ignore", "pipe", "pipe"],
22
+ stdio: [stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
23
23
  });
24
+ if (stdin != null) {
25
+ proc.stdin.write(stdin);
26
+ proc.stdin.end();
27
+ }
24
28
  let stdout = "";
25
29
  let stderr = "";
26
30
  proc.stdout.on("data", (d) => {
package/src/index.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  ListToolsRequestSchema,
20
20
  } from "@modelcontextprotocol/sdk/types.js";
21
21
  import { createAuthoringTools } from "./tools/authoring.js";
22
+ import { createDataTools } from "./tools/data.js";
22
23
  import { createReadTools } from "./tools/read.js";
23
24
  import { createValidateTools } from "./tools/validate.js";
24
25
  import { createDiffTools } from "./tools/diff.js";
@@ -31,6 +32,7 @@ export function createAllTools() {
31
32
  ...createDiffTools(),
32
33
  ...createWriteTools(),
33
34
  ...createAuthoringTools(),
35
+ ...createDataTools(),
34
36
  ];
35
37
  }
36
38
 
@@ -9,28 +9,45 @@
9
9
  // governing permissions and limitations under the License.
10
10
 
11
11
  /**
12
- * Authoring-session tools for design-data-agent-mcp.
12
+ * Authoring-session, lifecycle, and mode-set tools for design-data-agent-mcp.
13
13
  *
14
- * Most operations use @adobe/design-data (on-disk session store) and run
15
- * fully in-process. The exception is authoring_session_step_intent, which still
16
- * delegates to the CLI because the NLP `suggest` ranking is not yet on the wasm
17
- * surface. When that API is added, step_intent can be migrated here too.
14
+ * All operations delegate to the `design-data` CLI, which is already a hard
15
+ * prerequisite of this server (every read tool shells out the same way). This
16
+ * guarantees output byte-identical to the CLI by construction and avoids a
17
+ * parallel JS reimplementation of the Rust cascade write path.
18
+ *
19
+ * The one exception — authoring_session_step_intent — was the original CLI
20
+ * shell-out; it is left unchanged.
21
+ *
22
+ * CLI surface used:
23
+ * design-data authoring-session {start | step intent|classification|values |
24
+ * commit | cancel | get | list}
25
+ * design-data lifecycle {edit | deprecate | rename | rewire-alias | remove}
26
+ * design-data mode-set {add-mode | rename-mode | remove-mode |
27
+ * create-mode-set | remove-mode-set}
18
28
  */
19
29
 
20
- import {
21
- startSession,
22
- getSession,
23
- listSessions,
24
- stepClassification,
25
- stepValues,
26
- commitSession,
27
- cancelSession,
28
- } from "@adobe/design-data/session";
29
30
  import { runCli } from "../cli.js";
30
31
  import { config } from "../config.js";
31
32
 
33
+ /**
34
+ * Run CLI args, throw on non-zero exit, return parsed JSON.
35
+ *
36
+ * @param {string[]} args
37
+ * @param {{ timeout?: number }} [opts]
38
+ * @returns {Promise<unknown>}
39
+ */
40
+ async function runJson(args, { timeout = 30_000 } = {}) {
41
+ const { exitCode, stdout, stderr } = await runCli(args, { timeout });
42
+ if (exitCode !== 0)
43
+ throw new Error(stderr || `design-data ${args[0]} exited ${exitCode}`);
44
+ return JSON.parse(stdout);
45
+ }
46
+
32
47
  export function createAuthoringTools() {
33
48
  return [
49
+ // ── Authoring session ──────────────────────────────────────────────────
50
+
34
51
  {
35
52
  name: "start_authoring_session",
36
53
  description:
@@ -54,7 +71,7 @@ export function createAuthoringTools() {
54
71
  "dataset_path is required (or set DESIGN_DATA_PATH in the environment)",
55
72
  );
56
73
  }
57
- return startSession(path);
74
+ return runJson(["authoring-session", "start", path]);
58
75
  },
59
76
  },
60
77
 
@@ -77,9 +94,8 @@ export function createAuthoringTools() {
77
94
  additionalProperties: false,
78
95
  },
79
96
  async handler({ session_id, intent }) {
80
- // step_intent requires NLP suggest ranking — still uses the CLI.
81
- // Will be migrated when suggest is added to the wasm surface.
82
- const { exitCode, stdout, stderr } = await runCli(
97
+ // step_intent uses CLI for NLP suggest ranking (not yet on wasm surface).
98
+ return runJson(
83
99
  [
84
100
  "authoring-session",
85
101
  "step",
@@ -91,11 +107,6 @@ export function createAuthoringTools() {
91
107
  ],
92
108
  { timeout: 30_000 },
93
109
  );
94
- if (exitCode !== 0)
95
- throw new Error(
96
- stderr || `authoring-session step intent exited ${exitCode}`,
97
- );
98
- return JSON.parse(stdout);
99
110
  },
100
111
  },
101
112
 
@@ -103,7 +114,8 @@ export function createAuthoringTools() {
103
114
  name: "authoring_session_step_classification",
104
115
  description:
105
116
  "Set the layer, property, and name-object fields for the token. " +
106
- "name_fields is an array of {key, value} objects.",
117
+ "name_fields is an array of {key, value} objects. " +
118
+ "Classification is validated against the active fields catalog.",
107
119
  inputSchema: {
108
120
  type: "object",
109
121
  required: ["session_id", "layer", "property"],
@@ -134,11 +146,22 @@ export function createAuthoringTools() {
134
146
  additionalProperties: false,
135
147
  },
136
148
  async handler({ session_id, layer, property, name_fields = [] }) {
137
- return stepClassification(session_id, {
149
+ const args = [
150
+ "authoring-session",
151
+ "step",
152
+ "classification",
153
+ "--session-id",
154
+ session_id,
155
+ "--layer",
138
156
  layer,
157
+ "--property",
139
158
  property,
140
- nameFields: name_fields,
141
- });
159
+ ];
160
+ for (const { key, value } of name_fields) {
161
+ // ponytail: CLI parse_name_fields uses split_once('=') — values with '=' are safe.
162
+ args.push("--name-field", `${key}=${value}`);
163
+ }
164
+ return runJson(args);
142
165
  },
143
166
  },
144
167
 
@@ -180,15 +203,24 @@ export function createAuthoringTools() {
180
203
  additionalProperties: false,
181
204
  },
182
205
  async handler({ session_id, rows }) {
183
- return stepValues(session_id, rows);
206
+ return runJson([
207
+ "authoring-session",
208
+ "step",
209
+ "values",
210
+ "--session-id",
211
+ session_id,
212
+ "--rows",
213
+ JSON.stringify(rows),
214
+ ]);
184
215
  },
185
216
  },
186
217
 
187
218
  {
188
219
  name: "authoring_session_commit",
189
220
  description:
190
- "Build and write the token to disk, then remove the session. " +
191
- "Requires schema_url (the JSON Schema URL for the token type) and target (output file path).",
221
+ "Build and write the token to disk as a cascade array element, then remove the session. " +
222
+ "Requires schema_url (the JSON Schema URL for the token type) and target " +
223
+ "(the *.tokens.json cascade file to write into).",
192
224
  inputSchema: {
193
225
  type: "object",
194
226
  required: ["session_id", "schema_url", "target"],
@@ -202,7 +234,7 @@ export function createAuthoringTools() {
202
234
  target: {
203
235
  type: "string",
204
236
  description:
205
- "Target legacy JSON file to write to (created if absent, merged if present).",
237
+ "Target *.tokens.json cascade file to write into (created if absent, upserted if present).",
206
238
  },
207
239
  rationale: {
208
240
  type: "string",
@@ -216,7 +248,6 @@ export function createAuthoringTools() {
216
248
  type: "string",
217
249
  description:
218
250
  "Path to schemas directory containing token-types/ and token-file.json. " +
219
- "Used for Layer-1 JSON-Schema validation before writing. " +
220
251
  "Defaults to @adobe/spectrum-tokens schemas when omitted.",
221
252
  },
222
253
  is_override: {
@@ -236,15 +267,22 @@ export function createAuthoringTools() {
236
267
  schema_path,
237
268
  is_override = false,
238
269
  }) {
239
- return commitSession({
240
- sessionId: session_id,
241
- schemaUrl: schema_url,
270
+ const args = [
271
+ "authoring-session",
272
+ "commit",
273
+ "--session-id",
274
+ session_id,
275
+ "--schema-url",
276
+ schema_url,
277
+ "--target",
242
278
  target,
279
+ "--rationale",
243
280
  rationale,
244
- productContext: product_context,
245
- schemaPath: schema_path ?? null,
246
- isOverride: is_override,
247
- });
281
+ ];
282
+ if (schema_path) args.push("--schema-path", schema_path);
283
+ if (product_context) args.push("--product-context", product_context);
284
+ if (is_override) args.push("--is-override");
285
+ return runJson(args, { timeout: 60_000 });
248
286
  },
249
287
  },
250
288
 
@@ -258,7 +296,12 @@ export function createAuthoringTools() {
258
296
  additionalProperties: false,
259
297
  },
260
298
  async handler({ session_id }) {
261
- return cancelSession(session_id);
299
+ return runJson([
300
+ "authoring-session",
301
+ "cancel",
302
+ "--session-id",
303
+ session_id,
304
+ ]);
262
305
  },
263
306
  },
264
307
 
@@ -272,7 +315,12 @@ export function createAuthoringTools() {
272
315
  additionalProperties: false,
273
316
  },
274
317
  async handler({ session_id }) {
275
- return getSession(session_id);
318
+ return runJson([
319
+ "authoring-session",
320
+ "get",
321
+ "--session-id",
322
+ session_id,
323
+ ]);
276
324
  },
277
325
  },
278
326
 
@@ -285,7 +333,534 @@ export function createAuthoringTools() {
285
333
  additionalProperties: false,
286
334
  },
287
335
  async handler() {
288
- return listSessions();
336
+ return runJson(["authoring-session", "list"]);
337
+ },
338
+ },
339
+
340
+ // ── Token lifecycle ────────────────────────────────────────────────────
341
+
342
+ {
343
+ name: "edit_token",
344
+ description:
345
+ "Merge field updates into an already-committed cascade token, then re-validate. " +
346
+ "The token's uuid is always preserved and silently ignored if included in updates.",
347
+ inputSchema: {
348
+ type: "object",
349
+ required: ["uuid", "target", "updates"],
350
+ properties: {
351
+ uuid: { type: "string", description: "UUID of the token to edit." },
352
+ target: {
353
+ type: "string",
354
+ description:
355
+ "Path to the *.tokens.json cascade file containing the token.",
356
+ },
357
+ updates: {
358
+ type: "object",
359
+ description:
360
+ 'Fields to merge into the token object, e.g. { "value": "#ff0000" }.',
361
+ },
362
+ rationale: { type: "string", description: "Why the token changed." },
363
+ tokens_root: {
364
+ type: "string",
365
+ description:
366
+ "Root tokens directory — required when updates contains a $ref.",
367
+ },
368
+ schema_path: {
369
+ type: "string",
370
+ description:
371
+ "Schemas directory for validation (default: auto-resolved from target).",
372
+ },
373
+ },
374
+ additionalProperties: false,
375
+ },
376
+ async handler({
377
+ uuid,
378
+ target,
379
+ updates,
380
+ rationale = "",
381
+ tokens_root,
382
+ schema_path,
383
+ }) {
384
+ const args = [
385
+ "lifecycle",
386
+ "edit",
387
+ "--uuid",
388
+ uuid,
389
+ "--target",
390
+ target,
391
+ "--updates",
392
+ JSON.stringify(updates),
393
+ "--rationale",
394
+ rationale,
395
+ ];
396
+ if (tokens_root) args.push("--tokens-root", tokens_root);
397
+ if (schema_path) args.push("--schema-path", schema_path);
398
+ return runJson(args);
399
+ },
400
+ },
401
+
402
+ {
403
+ name: "deprecate_token",
404
+ description:
405
+ "Stamp a deprecation marker onto a cascade token. Optionally record a replacement UUID " +
406
+ "and a planned-removal spec version.",
407
+ inputSchema: {
408
+ type: "object",
409
+ required: ["uuid", "target", "spec_version"],
410
+ properties: {
411
+ uuid: {
412
+ type: "string",
413
+ description: "UUID of the token to deprecate.",
414
+ },
415
+ target: {
416
+ type: "string",
417
+ description:
418
+ "Path to the *.tokens.json cascade file containing the token.",
419
+ },
420
+ spec_version: {
421
+ type: "string",
422
+ description:
423
+ "Dataset specVersion string to stamp into the deprecated value, e.g. '1.0.0'.",
424
+ },
425
+ deprecated_comment: {
426
+ type: "string",
427
+ description:
428
+ "Human-readable deprecation explanation / migration guidance.",
429
+ },
430
+ replaced_by: {
431
+ type: "string",
432
+ description:
433
+ "Replacement token UUID (string) or JSON array of UUIDs.",
434
+ },
435
+ planned_removal: {
436
+ type: "string",
437
+ description: "Spec version at which the token will be removed.",
438
+ },
439
+ rationale: {
440
+ type: "string",
441
+ description: "Why the token was deprecated.",
442
+ },
443
+ schema_path: {
444
+ type: "string",
445
+ description: "Schemas directory for validation.",
446
+ },
447
+ },
448
+ additionalProperties: false,
449
+ },
450
+ async handler({
451
+ uuid,
452
+ target,
453
+ spec_version,
454
+ deprecated_comment,
455
+ replaced_by,
456
+ planned_removal,
457
+ rationale = "",
458
+ schema_path,
459
+ }) {
460
+ const args = [
461
+ "lifecycle",
462
+ "deprecate",
463
+ "--uuid",
464
+ uuid,
465
+ "--target",
466
+ target,
467
+ "--spec-version",
468
+ spec_version,
469
+ "--rationale",
470
+ rationale,
471
+ ];
472
+ if (deprecated_comment)
473
+ args.push("--deprecated-comment", deprecated_comment);
474
+ if (replaced_by) args.push("--replaced-by", replaced_by);
475
+ if (planned_removal) args.push("--planned-removal", planned_removal);
476
+ if (schema_path) args.push("--schema-path", schema_path);
477
+ return runJson(args);
478
+ },
479
+ },
480
+
481
+ {
482
+ name: "rename_token",
483
+ description:
484
+ "Assign a new name object to a cascade token, preserving its UUID. " +
485
+ "Optionally set a replaced_by pointer on the token being renamed.",
486
+ inputSchema: {
487
+ type: "object",
488
+ required: ["uuid", "target", "new_name"],
489
+ properties: {
490
+ uuid: { type: "string", description: "UUID of the token to rename." },
491
+ target: {
492
+ type: "string",
493
+ description:
494
+ "Path to the *.tokens.json cascade file containing the token.",
495
+ },
496
+ new_name: {
497
+ description:
498
+ 'New name — either a JSON object ({"property":"…","component":"…"}) or a plain string.',
499
+ },
500
+ replaced_by_target: {
501
+ type: "string",
502
+ description:
503
+ "UUID to set as the replaced_by pointer (for retiring the old name).",
504
+ },
505
+ rationale: {
506
+ type: "string",
507
+ description: "Why the token was renamed.",
508
+ },
509
+ schema_path: {
510
+ type: "string",
511
+ description: "Schemas directory for validation.",
512
+ },
513
+ },
514
+ additionalProperties: false,
515
+ },
516
+ async handler({
517
+ uuid,
518
+ target,
519
+ new_name,
520
+ replaced_by_target,
521
+ rationale = "",
522
+ schema_path,
523
+ }) {
524
+ if (
525
+ new_name === null ||
526
+ new_name === undefined ||
527
+ (typeof new_name !== "string" && typeof new_name !== "object")
528
+ ) {
529
+ throw new Error(
530
+ "new_name must be a string or object, got: " +
531
+ JSON.stringify(new_name),
532
+ );
533
+ }
534
+ const newNameArg =
535
+ typeof new_name === "object" ? JSON.stringify(new_name) : new_name;
536
+ const args = [
537
+ "lifecycle",
538
+ "rename",
539
+ "--uuid",
540
+ uuid,
541
+ "--target",
542
+ target,
543
+ "--new-name",
544
+ newNameArg,
545
+ "--rationale",
546
+ rationale,
547
+ ];
548
+ if (replaced_by_target)
549
+ args.push("--replaced-by-target", replaced_by_target);
550
+ if (schema_path) args.push("--schema-path", schema_path);
551
+ return runJson(args);
552
+ },
553
+ },
554
+
555
+ {
556
+ name: "rewire_alias",
557
+ description:
558
+ "Change the $ref target on an alias token. The new ref target must resolve in the cascade. " +
559
+ "Rejects if the token is not an alias.",
560
+ inputSchema: {
561
+ type: "object",
562
+ required: ["uuid", "target", "new_ref", "tokens_root"],
563
+ properties: {
564
+ uuid: {
565
+ type: "string",
566
+ description: "UUID of the alias token to rewire.",
567
+ },
568
+ target: {
569
+ type: "string",
570
+ description:
571
+ "Path to the *.tokens.json cascade file containing the token.",
572
+ },
573
+ new_ref: {
574
+ type: "string",
575
+ description:
576
+ "New $ref value — must be a UUID that resolves in the cascade.",
577
+ },
578
+ tokens_root: {
579
+ type: "string",
580
+ description:
581
+ "Root tokens directory for ref-resolution verification.",
582
+ },
583
+ rationale: {
584
+ type: "string",
585
+ description: "Why the alias target changed.",
586
+ },
587
+ schema_path: {
588
+ type: "string",
589
+ description: "Schemas directory for validation.",
590
+ },
591
+ },
592
+ additionalProperties: false,
593
+ },
594
+ async handler({
595
+ uuid,
596
+ target,
597
+ new_ref,
598
+ tokens_root,
599
+ rationale = "",
600
+ schema_path,
601
+ }) {
602
+ const args = [
603
+ "lifecycle",
604
+ "rewire-alias",
605
+ "--uuid",
606
+ uuid,
607
+ "--target",
608
+ target,
609
+ "--new-ref",
610
+ new_ref,
611
+ "--tokens-root",
612
+ tokens_root,
613
+ "--rationale",
614
+ rationale,
615
+ ];
616
+ if (schema_path) args.push("--schema-path", schema_path);
617
+ return runJson(args);
618
+ },
619
+ },
620
+
621
+ {
622
+ name: "remove_token",
623
+ description:
624
+ "Delete a token from a cascade file. Aborts if any other token in the dataset " +
625
+ "holds a $ref pointing at this token's UUID.",
626
+ inputSchema: {
627
+ type: "object",
628
+ required: ["uuid", "target", "tokens_root"],
629
+ properties: {
630
+ uuid: { type: "string", description: "UUID of the token to remove." },
631
+ target: {
632
+ type: "string",
633
+ description:
634
+ "Path to the *.tokens.json cascade file containing the token.",
635
+ },
636
+ tokens_root: {
637
+ type: "string",
638
+ description: "Root tokens directory for inbound-reference guard.",
639
+ },
640
+ },
641
+ additionalProperties: false,
642
+ },
643
+ async handler({ uuid, target, tokens_root }) {
644
+ return runJson([
645
+ "lifecycle",
646
+ "remove",
647
+ "--uuid",
648
+ uuid,
649
+ "--target",
650
+ target,
651
+ "--tokens-root",
652
+ tokens_root,
653
+ ]);
654
+ },
655
+ },
656
+
657
+ // ── Mode-set ───────────────────────────────────────────────────────────
658
+
659
+ {
660
+ name: "add_mode",
661
+ description: "Append a new mode to an existing mode-set file.",
662
+ inputSchema: {
663
+ type: "object",
664
+ required: ["mode_set_file", "mode"],
665
+ properties: {
666
+ mode_set_file: {
667
+ type: "string",
668
+ description: "Path to the mode-set JSON file.",
669
+ },
670
+ mode: {
671
+ type: "string",
672
+ description: "Name of the new mode to add.",
673
+ },
674
+ make_default: {
675
+ type: "boolean",
676
+ description: "Make this the default mode.",
677
+ },
678
+ },
679
+ additionalProperties: false,
680
+ },
681
+ async handler({ mode_set_file, mode, make_default = false }) {
682
+ const args = [
683
+ "mode-set",
684
+ "add-mode",
685
+ "--mode-set-file",
686
+ mode_set_file,
687
+ "--mode",
688
+ mode,
689
+ ];
690
+ if (make_default) args.push("--make-default");
691
+ return runJson(args);
692
+ },
693
+ },
694
+
695
+ {
696
+ name: "rename_mode",
697
+ description:
698
+ "Rename a mode in a mode-set file and propagate the change to all token files " +
699
+ "under tokens_root that use that mode value.",
700
+ inputSchema: {
701
+ type: "object",
702
+ required: ["mode_set_file", "tokens_root", "old", "new"],
703
+ properties: {
704
+ mode_set_file: {
705
+ type: "string",
706
+ description: "Path to the mode-set JSON file.",
707
+ },
708
+ tokens_root: {
709
+ type: "string",
710
+ description: "Root tokens directory for propagation.",
711
+ },
712
+ old: {
713
+ type: "string",
714
+ description: "Existing mode name to rename.",
715
+ },
716
+ new: {
717
+ type: "string",
718
+ description: "Replacement mode name.",
719
+ },
720
+ },
721
+ additionalProperties: false,
722
+ },
723
+ async handler({ mode_set_file, tokens_root, old, new: new_mode }) {
724
+ return runJson([
725
+ "mode-set",
726
+ "rename-mode",
727
+ "--mode-set-file",
728
+ mode_set_file,
729
+ "--tokens-root",
730
+ tokens_root,
731
+ "--old",
732
+ old,
733
+ "--new",
734
+ new_mode,
735
+ ]);
736
+ },
737
+ },
738
+
739
+ {
740
+ name: "remove_mode",
741
+ description:
742
+ "Remove a mode from a mode-set file. Aborts if any token references that mode value " +
743
+ "or if the mode is the current default.",
744
+ inputSchema: {
745
+ type: "object",
746
+ required: ["mode_set_file", "tokens_root", "mode"],
747
+ properties: {
748
+ mode_set_file: {
749
+ type: "string",
750
+ description: "Path to the mode-set JSON file.",
751
+ },
752
+ tokens_root: {
753
+ type: "string",
754
+ description: "Root tokens directory for reference guard.",
755
+ },
756
+ mode: {
757
+ type: "string",
758
+ description: "Mode name to remove.",
759
+ },
760
+ },
761
+ additionalProperties: false,
762
+ },
763
+ async handler({ mode_set_file, tokens_root, mode }) {
764
+ return runJson([
765
+ "mode-set",
766
+ "remove-mode",
767
+ "--mode-set-file",
768
+ mode_set_file,
769
+ "--tokens-root",
770
+ tokens_root,
771
+ "--mode",
772
+ mode,
773
+ ]);
774
+ },
775
+ },
776
+
777
+ {
778
+ name: "create_mode_set",
779
+ description:
780
+ "Author a new mode-set file for a new cascade dimension. " +
781
+ "The file must not already exist. default must be a member of modes.",
782
+ inputSchema: {
783
+ type: "object",
784
+ required: ["mode_set_file", "name", "modes", "default"],
785
+ properties: {
786
+ mode_set_file: {
787
+ type: "string",
788
+ description:
789
+ "Destination path for the new mode-set JSON file (must not exist).",
790
+ },
791
+ name: {
792
+ type: "string",
793
+ description:
794
+ "Logical name used as the key in token name objects, e.g. 'colorScheme'.",
795
+ },
796
+ modes: {
797
+ type: "array",
798
+ items: { type: "string" },
799
+ description: "Ordered list of mode names, e.g. ['light', 'dark'].",
800
+ },
801
+ default: {
802
+ type: "string",
803
+ description: "Default mode — must be a member of modes.",
804
+ },
805
+ description: {
806
+ type: "string",
807
+ description: "Human-readable description embedded in the file.",
808
+ },
809
+ },
810
+ additionalProperties: false,
811
+ },
812
+ async handler({
813
+ mode_set_file,
814
+ name,
815
+ modes,
816
+ default: defaultMode,
817
+ description = "",
818
+ }) {
819
+ return runJson([
820
+ "mode-set",
821
+ "create-mode-set",
822
+ "--mode-set-file",
823
+ mode_set_file,
824
+ "--name",
825
+ name,
826
+ "--modes",
827
+ JSON.stringify(modes),
828
+ "--default",
829
+ defaultMode,
830
+ "--description",
831
+ description,
832
+ ]);
833
+ },
834
+ },
835
+
836
+ {
837
+ name: "remove_mode_set",
838
+ description:
839
+ "Delete a mode-set file. Aborts if any token in the dataset references this dimension.",
840
+ inputSchema: {
841
+ type: "object",
842
+ required: ["mode_set_file", "tokens_root"],
843
+ properties: {
844
+ mode_set_file: {
845
+ type: "string",
846
+ description: "Path to the mode-set JSON file to remove.",
847
+ },
848
+ tokens_root: {
849
+ type: "string",
850
+ description: "Root tokens directory for reference guard.",
851
+ },
852
+ },
853
+ additionalProperties: false,
854
+ },
855
+ async handler({ mode_set_file, tokens_root }) {
856
+ return runJson([
857
+ "mode-set",
858
+ "remove-mode-set",
859
+ "--mode-set-file",
860
+ mode_set_file,
861
+ "--tokens-root",
862
+ tokens_root,
863
+ ]);
289
864
  },
290
865
  },
291
866
  ];
@@ -0,0 +1,162 @@
1
+ // Copyright 2026 Adobe. All rights reserved.
2
+ // This file is licensed to you under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License. You may obtain a copy
4
+ // of the License at http://www.apache.org/licenses/LICENSE-2.0
5
+ //
6
+ // Unless required by applicable law or agreed to in writing, software distributed under
7
+ // the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
8
+ // OF ANY KIND, either express or implied. See the License for the specific language
9
+ // governing permissions and limitations under the License.
10
+
11
+ /**
12
+ * create/edit tools for non-token data categories (components, fields, registry,
13
+ * mode-sets, guidelines) — Phase C authoring surface.
14
+ *
15
+ * All operations delegate to `design-data data create|edit`, which validates
16
+ * against the JSON schema before writing. Output is byte-identical to the CLI.
17
+ *
18
+ * CLI surface used:
19
+ * design-data data create --category <c> --file - [--dataset <dir>] [--spec-schemas <dir>]
20
+ * design-data data edit --category <c> --file - [--dataset <dir>] [--spec-schemas <dir>]
21
+ */
22
+
23
+ import { resolve } from "path";
24
+ import { runCli } from "../cli.js";
25
+ import { config } from "../config.js";
26
+
27
+ const CATEGORIES = [
28
+ "components",
29
+ "fields",
30
+ "registry",
31
+ "mode-sets",
32
+ "guidelines",
33
+ ];
34
+
35
+ /** Run CLI, throw on non-zero exit, return parsed JSON. */
36
+ async function runJson(args, stdin, { timeout = 30_000 } = {}) {
37
+ const { exitCode, stdout, stderr } = await runCli(args, { timeout, stdin });
38
+ if (exitCode !== 0)
39
+ throw new Error(stderr || `design-data ${args[0]} exited ${exitCode}`);
40
+ return JSON.parse(stdout);
41
+ }
42
+
43
+ /**
44
+ * Build the base args for a data create/edit call, resolving dataset root and
45
+ * spec schemas dir from config / env.
46
+ */
47
+ function baseArgs(subcommand, category, datasetOverride, specSchemasOverride) {
48
+ // Only use config.dataPath when it ends in /tokens — the bare-cwd fallback
49
+ // (anchorPath(".")) does not end in tokens and must not be stripped.
50
+ const dataPathTokens = config.dataPath?.match(/^(.*?)\/tokens\/?$/)
51
+ ? config.dataPath.replace(/\/tokens\/?$/, "")
52
+ : null;
53
+ const dataset =
54
+ datasetOverride ??
55
+ dataPathTokens ??
56
+ resolve(config.dataRoot, "packages/design-data");
57
+
58
+ const args = [
59
+ "data",
60
+ subcommand,
61
+ "--category",
62
+ category,
63
+ "--file",
64
+ "-",
65
+ "--dataset",
66
+ dataset,
67
+ ];
68
+
69
+ const specSchemas =
70
+ specSchemasOverride ??
71
+ (process.env.DESIGN_DATA_SPEC_SCHEMAS
72
+ ? resolve(process.env.DESIGN_DATA_SPEC_SCHEMAS)
73
+ : null);
74
+ if (specSchemas) args.push("--spec-schemas", specSchemas);
75
+
76
+ return { args, dataset };
77
+ }
78
+
79
+ export function createDataTools() {
80
+ return [
81
+ {
82
+ name: "data_create",
83
+ description:
84
+ "Create a new non-token data object (component, field, registry entry, mode-set, or " +
85
+ "guideline). Validates the JSON document against the category schema before writing. " +
86
+ "Fails if the file already exists — use data_edit to overwrite.",
87
+ inputSchema: {
88
+ type: "object",
89
+ required: ["category", "document"],
90
+ properties: {
91
+ category: {
92
+ type: "string",
93
+ enum: CATEGORIES,
94
+ description: "Data category to create in.",
95
+ },
96
+ document: {
97
+ type: "object",
98
+ description:
99
+ "Full JSON document to write. Must include the category's required fields " +
100
+ "(e.g. `name`, `$schema`, `specVersion`). The filename is derived from the `name` field.",
101
+ additionalProperties: true,
102
+ },
103
+ dataset: {
104
+ type: "string",
105
+ description:
106
+ "Path to the dataset root (packages/design-data). Defaults to DESIGN_DATA_PATH minus /tokens.",
107
+ },
108
+ spec_schemas: {
109
+ type: "string",
110
+ description:
111
+ "Path to the design-data-spec schemas dir. Defaults to DESIGN_DATA_SPEC_SCHEMAS or auto-probed.",
112
+ },
113
+ },
114
+ additionalProperties: false,
115
+ },
116
+ async handler({ category, document, dataset, spec_schemas }) {
117
+ const { args } = baseArgs("create", category, dataset, spec_schemas);
118
+ return runJson(args, JSON.stringify(document));
119
+ },
120
+ },
121
+
122
+ {
123
+ name: "data_edit",
124
+ description:
125
+ "Replace an existing non-token data object with a new JSON document. Validates the " +
126
+ "document against the category schema before writing. Fails if the file does not exist " +
127
+ "— use data_create to create it. Edit is a full document replace, not a patch.",
128
+ inputSchema: {
129
+ type: "object",
130
+ required: ["category", "document"],
131
+ properties: {
132
+ category: {
133
+ type: "string",
134
+ enum: CATEGORIES,
135
+ description: "Data category to edit in.",
136
+ },
137
+ document: {
138
+ type: "object",
139
+ description:
140
+ "Full replacement JSON document. The `name` field must match the existing file.",
141
+ additionalProperties: true,
142
+ },
143
+ dataset: {
144
+ type: "string",
145
+ description:
146
+ "Path to the dataset root (packages/design-data). Defaults to DESIGN_DATA_PATH minus /tokens.",
147
+ },
148
+ spec_schemas: {
149
+ type: "string",
150
+ description:
151
+ "Path to the design-data-spec schemas dir. Defaults to DESIGN_DATA_SPEC_SCHEMAS or auto-probed.",
152
+ },
153
+ },
154
+ additionalProperties: false,
155
+ },
156
+ async handler({ category, document, dataset, spec_schemas }) {
157
+ const { args } = baseArgs("edit", category, dataset, spec_schemas);
158
+ return runJson(args, JSON.stringify(document));
159
+ },
160
+ },
161
+ ];
162
+ }
@@ -9,7 +9,7 @@
9
9
  // governing permissions and limitations under the License.
10
10
 
11
11
  import { join } from "path";
12
- import { writeProductContext } from "@adobe/design-data/write";
12
+ import { runCli } from "../cli.js";
13
13
  import { config } from "../config.js";
14
14
 
15
15
  export function createWriteTools() {
@@ -36,7 +36,12 @@ export function createWriteTools() {
36
36
  async handler({ output, rationale } = {}) {
37
37
  const resolvedOutput =
38
38
  output ?? join(config.dataPath, "product-context.json");
39
- return writeProductContext({ output: resolvedOutput, rationale });
39
+ const args = ["write", "--output", resolvedOutput];
40
+ if (rationale) args.push("--rationale", rationale);
41
+ const { exitCode, stdout, stderr } = await runCli(args);
42
+ if (exitCode !== 0)
43
+ throw new Error(stderr || `design-data write exited ${exitCode}`);
44
+ return stdout;
40
45
  },
41
46
  },
42
47
  ];