@genart-dev/mcp-server 0.1.2 → 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/dist/index.js CHANGED
@@ -25,7 +25,8 @@ import {
25
25
  parseGenart,
26
26
  parseWorkspace,
27
27
  serializeGenart,
28
- serializeWorkspace
28
+ serializeWorkspace,
29
+ createLayerStack
29
30
  } from "@genart-dev/core";
30
31
  import { writeFile } from "fs/promises";
31
32
  var EditorState = class extends EventEmitter {
@@ -49,6 +50,10 @@ var EditorState = class extends EventEmitter {
49
50
  * instead of writing to disk. Set by mcp-host for HTTP-based sessions.
50
51
  */
51
52
  remoteMode = false;
53
+ /** Plugin registry for design mode. Set during server initialization. */
54
+ pluginRegistry = null;
55
+ /** Layer stacks keyed by sketch ID. Created lazily when design tools are used. */
56
+ layerStacks = /* @__PURE__ */ new Map();
52
57
  constructor(options) {
53
58
  super();
54
59
  if (options?.basePath) {
@@ -113,6 +118,7 @@ var EditorState = class extends EventEmitter {
113
118
  this.workspace = ws;
114
119
  this.sketches.clear();
115
120
  this.selection.clear();
121
+ this.layerStacks.clear();
116
122
  for (const ref of ws.sketches) {
117
123
  const sketchPath = this.resolveSketchPath(ref.file);
118
124
  await this.loadSketch(sketchPath);
@@ -154,6 +160,7 @@ var EditorState = class extends EventEmitter {
154
160
  removeSketch(id) {
155
161
  this.sketches.delete(id);
156
162
  this.selection.delete(id);
163
+ this.layerStacks.delete(id);
157
164
  this.emitMutation("sketch:removed", { id });
158
165
  }
159
166
  /** Save the active workspace to disk. */
@@ -197,6 +204,81 @@ var EditorState = class extends EventEmitter {
197
204
  selection: Array.from(this.selection)
198
205
  };
199
206
  }
207
+ /**
208
+ * Get or create a LayerStackAccessor for a sketch.
209
+ * Initializes from the sketch's persisted design layers.
210
+ */
211
+ getLayerStack(sketchId) {
212
+ let stack = this.layerStacks.get(sketchId);
213
+ if (stack) return stack;
214
+ const loaded = this.requireSketch(sketchId);
215
+ const initialLayers = loaded.definition.layers ?? [];
216
+ stack = createLayerStack(initialLayers, (changeType) => {
217
+ this.syncLayersToDefinition(sketchId);
218
+ const mutationType = `design:${changeType}`;
219
+ this.emitMutation(mutationType, { sketchId, changeType });
220
+ });
221
+ this.layerStacks.set(sketchId, stack);
222
+ return stack;
223
+ }
224
+ /**
225
+ * Sync the layer stack's current state back to the sketch definition.
226
+ * Called automatically on every layer mutation.
227
+ */
228
+ syncLayersToDefinition(sketchId) {
229
+ const loaded = this.sketches.get(sketchId);
230
+ const stack = this.layerStacks.get(sketchId);
231
+ if (!loaded || !stack) return;
232
+ const layers = stack.getAll();
233
+ loaded.definition = {
234
+ ...loaded.definition,
235
+ layers: layers.length > 0 ? layers : void 0
236
+ };
237
+ }
238
+ /**
239
+ * Create an McpToolContext for a plugin's MCP tool handler.
240
+ * Provides access to the layer stack, sketch state, and change notifications.
241
+ */
242
+ createMcpToolContext(sketchId) {
243
+ const loaded = this.requireSketch(sketchId);
244
+ const layerStack = this.getLayerStack(sketchId);
245
+ const def = loaded.definition;
246
+ const sketchState = {
247
+ seed: def.state.seed,
248
+ params: def.state.params,
249
+ colorPalette: def.state.colorPalette,
250
+ canvasWidth: def.canvas.width,
251
+ canvasHeight: def.canvas.height,
252
+ rendererId: def.renderer.type
253
+ };
254
+ return {
255
+ layers: layerStack,
256
+ sketchState,
257
+ canvasWidth: def.canvas.width,
258
+ canvasHeight: def.canvas.height,
259
+ async resolveAsset(_assetId) {
260
+ return null;
261
+ },
262
+ async captureComposite(_format) {
263
+ throw new Error("captureComposite is not available in headless MCP mode");
264
+ },
265
+ emitChange(_changeType) {
266
+ }
267
+ };
268
+ }
269
+ /**
270
+ * Get the currently selected sketch ID for design operations.
271
+ * Returns the single selected sketch, or throws if none/multiple selected.
272
+ */
273
+ requireSelectedSketchId() {
274
+ if (this.selection.size === 0) {
275
+ throw new Error("No sketch is selected. Use select_sketch or open_sketch first.");
276
+ }
277
+ if (this.selection.size > 1) {
278
+ throw new Error("Multiple sketches are selected. Design operations require a single sketch.");
279
+ }
280
+ return this.selection.values().next().value;
281
+ }
200
282
  /** Emit a mutation event for external listeners (WebSocket broadcast, sidecar IPC). */
201
283
  emitMutation(type, payload) {
202
284
  this.emit("mutation", { type, payload });
@@ -207,6 +289,11 @@ var EditorState = class extends EventEmitter {
207
289
  // src/server.ts
208
290
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
209
291
  import { z as z2 } from "zod";
292
+ import { createPluginRegistry } from "@genart-dev/core";
293
+ import typographyPlugin from "@genart-dev/plugin-typography";
294
+ import filtersPlugin from "@genart-dev/plugin-filters";
295
+ import shapesPlugin from "@genart-dev/plugin-shapes";
296
+ import layoutGuidesPlugin from "@genart-dev/plugin-layout-guides";
210
297
 
211
298
  // src/tools/workspace.ts
212
299
  import { readFile as readFile2, writeFile as writeFile2, stat } from "fs/promises";
@@ -551,6 +638,7 @@ import { writeFile as writeFile3, stat as stat2, unlink } from "fs/promises";
551
638
  import { basename as basename2, dirname as dirname3, resolve as resolve2 } from "path";
552
639
  import {
553
640
  createDefaultRegistry,
641
+ resolveComponents,
554
642
  resolvePreset,
555
643
  serializeGenart as serializeGenart2,
556
644
  serializeWorkspace as serializeWorkspace3
@@ -647,10 +735,40 @@ async function createSketch(state, input) {
647
735
  const adapter = registry4.resolve(rendererType);
648
736
  algorithm = adapter.getAlgorithmTemplate();
649
737
  }
738
+ let resolvedComponents;
739
+ if (input.components && Object.keys(input.components).length > 0) {
740
+ const shorthand = {};
741
+ for (const [name, value] of Object.entries(input.components)) {
742
+ if (typeof value === "string") {
743
+ shorthand[name] = value;
744
+ } else if (value.version) {
745
+ shorthand[name] = value.version;
746
+ } else if (value.code) {
747
+ if (!resolvedComponents) resolvedComponents = {};
748
+ resolvedComponents[name] = {
749
+ ...value.version ? { version: value.version } : {},
750
+ code: value.code,
751
+ ...value.exports ? { exports: value.exports } : {}
752
+ };
753
+ }
754
+ }
755
+ if (Object.keys(shorthand).length > 0) {
756
+ const resolved = resolveComponents(shorthand, rendererType);
757
+ if (!resolvedComponents) resolvedComponents = {};
758
+ for (const rc of resolved) {
759
+ resolvedComponents[rc.name] = {
760
+ version: rc.version,
761
+ code: rc.code,
762
+ exports: [...rc.exports]
763
+ };
764
+ }
765
+ }
766
+ }
650
767
  const seed = input.seed ?? Math.floor(Math.random() * 1e5);
651
768
  const ts = now2();
769
+ const hasComponents = resolvedComponents && Object.keys(resolvedComponents).length > 0;
652
770
  const sketch = {
653
- genart: "1.1",
771
+ genart: hasComponents ? "1.2" : "1.1",
654
772
  id: input.id,
655
773
  title: input.title,
656
774
  created: ts,
@@ -664,6 +782,7 @@ async function createSketch(state, input) {
664
782
  ...input.philosophy ? { philosophy: input.philosophy } : {},
665
783
  ...input.themes && input.themes.length > 0 ? { themes: input.themes } : {},
666
784
  ...input.skills && input.skills.length > 0 ? { skills: input.skills } : {},
785
+ ...hasComponents ? { components: resolvedComponents } : {},
667
786
  ...input.agent ? { agent: input.agent } : {},
668
787
  ...input.model ? { model: input.model } : {}
669
788
  };
@@ -847,10 +966,44 @@ async function updateAlgorithm(state, input) {
847
966
  );
848
967
  }
849
968
  }
969
+ let resolvedComponents;
970
+ if (input.components && Object.keys(input.components).length > 0) {
971
+ const renderer = def.renderer.type;
972
+ const shorthand = {};
973
+ for (const [name, value] of Object.entries(input.components)) {
974
+ if (typeof value === "string") {
975
+ shorthand[name] = value;
976
+ } else if (value.version) {
977
+ shorthand[name] = value.version;
978
+ } else if (value.code) {
979
+ if (!resolvedComponents) resolvedComponents = {};
980
+ resolvedComponents[name] = {
981
+ ...value.version ? { version: value.version } : {},
982
+ code: value.code,
983
+ ...value.exports ? { exports: value.exports } : {}
984
+ };
985
+ }
986
+ }
987
+ if (Object.keys(shorthand).length > 0) {
988
+ const resolved = resolveComponents(shorthand, renderer);
989
+ if (!resolvedComponents) resolvedComponents = {};
990
+ for (const rc of resolved) {
991
+ resolvedComponents[rc.name] = {
992
+ version: rc.version,
993
+ code: rc.code,
994
+ exports: [...rc.exports]
995
+ };
996
+ }
997
+ }
998
+ }
999
+ const updated = ["algorithm"];
1000
+ const hasNewComponents = resolvedComponents && Object.keys(resolvedComponents).length > 0;
1001
+ if (hasNewComponents) updated.push("components");
850
1002
  const newDef = {
851
1003
  ...def,
852
1004
  modified: now2(),
853
1005
  algorithm: input.algorithm,
1006
+ ...hasNewComponents ? { genart: "1.2", components: resolvedComponents } : {},
854
1007
  ...input.agent ? { agent: input.agent } : {},
855
1008
  ...input.model ? { model: input.model } : {}
856
1009
  };
@@ -864,7 +1017,7 @@ async function updateAlgorithm(state, input) {
864
1017
  }
865
1018
  state.emitMutation("sketch:updated", {
866
1019
  id: input.sketchId,
867
- updated: ["algorithm"]
1020
+ updated
868
1021
  });
869
1022
  return {
870
1023
  success: true,
@@ -872,6 +1025,7 @@ async function updateAlgorithm(state, input) {
872
1025
  renderer: def.renderer.type,
873
1026
  algorithmLength: input.algorithm.length,
874
1027
  validationPassed,
1028
+ ...hasNewComponents ? { componentsUpdated: true } : {},
875
1029
  fileContent: json
876
1030
  };
877
1031
  }
@@ -2173,8 +2327,219 @@ ${guidelines}`,
2173
2327
  };
2174
2328
  }
2175
2329
 
2176
- // src/tools/capture.ts
2330
+ // src/tools/components.ts
2177
2331
  import { writeFile as writeFile5 } from "fs/promises";
2332
+ import {
2333
+ COMPONENT_REGISTRY,
2334
+ resolveComponents as resolveComponents2,
2335
+ serializeGenart as serializeGenart4
2336
+ } from "@genart-dev/core";
2337
+ var VALID_RENDERERS2 = [
2338
+ "p5",
2339
+ "three",
2340
+ "glsl",
2341
+ "canvas2d",
2342
+ "svg"
2343
+ ];
2344
+ var RENDERER_TARGET = {
2345
+ p5: "js",
2346
+ three: "js",
2347
+ canvas2d: "js",
2348
+ svg: "js",
2349
+ glsl: "glsl"
2350
+ };
2351
+ async function listComponents(_state, input) {
2352
+ let entries = Object.values(COMPONENT_REGISTRY);
2353
+ if (input.renderer) {
2354
+ const renderer = input.renderer;
2355
+ const target = RENDERER_TARGET[renderer];
2356
+ if (!target) {
2357
+ throw new Error(
2358
+ `Unknown renderer type: '${input.renderer}'. Valid types: ${VALID_RENDERERS2.join(", ")}`
2359
+ );
2360
+ }
2361
+ entries = entries.filter(
2362
+ (e) => e.target === target && (e.renderers.length === 0 || e.renderers.includes(renderer))
2363
+ );
2364
+ }
2365
+ if (input.category) {
2366
+ const cat = input.category;
2367
+ entries = entries.filter((e) => e.category === cat);
2368
+ }
2369
+ entries.sort((a, b) => {
2370
+ const catCmp = a.category.localeCompare(b.category);
2371
+ if (catCmp !== 0) return catCmp;
2372
+ return a.name.localeCompare(b.name);
2373
+ });
2374
+ const components = entries.map((e) => ({
2375
+ name: e.name,
2376
+ version: e.version,
2377
+ category: e.category,
2378
+ target: e.target,
2379
+ exports: [...e.exports],
2380
+ dependencies: [...e.dependencies],
2381
+ description: e.description
2382
+ }));
2383
+ return {
2384
+ count: components.length,
2385
+ components
2386
+ };
2387
+ }
2388
+ async function addComponent(state, input) {
2389
+ const loaded = state.requireSketch(input.sketchId);
2390
+ const def = loaded.definition;
2391
+ const renderer = def.renderer.type;
2392
+ const entry = COMPONENT_REGISTRY[input.component];
2393
+ if (!entry) {
2394
+ throw new Error(`Unknown component: "${input.component}"`);
2395
+ }
2396
+ const target = RENDERER_TARGET[renderer];
2397
+ if (entry.target !== target) {
2398
+ throw new Error(
2399
+ `Component "${input.component}" has target "${entry.target}" but renderer "${renderer}" requires target "${target}"`
2400
+ );
2401
+ }
2402
+ if (entry.renderers.length > 0 && !entry.renderers.includes(renderer)) {
2403
+ throw new Error(
2404
+ `Component "${input.component}" is not compatible with renderer "${renderer}". Compatible: ${entry.renderers.join(", ")}`
2405
+ );
2406
+ }
2407
+ const existingComponents = {};
2408
+ if (def.components) {
2409
+ for (const [name, value] of Object.entries(def.components)) {
2410
+ if (typeof value === "string") {
2411
+ existingComponents[name] = value;
2412
+ } else if (value.version) {
2413
+ existingComponents[name] = value.version;
2414
+ }
2415
+ }
2416
+ }
2417
+ if (existingComponents[input.component]) {
2418
+ throw new Error(
2419
+ `Component "${input.component}" is already present in sketch "${input.sketchId}"`
2420
+ );
2421
+ }
2422
+ existingComponents[input.component] = input.version ?? "^1.0.0";
2423
+ const resolved = resolveComponents2(existingComponents, renderer);
2424
+ const resolvedRecord = {};
2425
+ for (const rc of resolved) {
2426
+ resolvedRecord[rc.name] = {
2427
+ version: rc.version,
2428
+ code: rc.code,
2429
+ exports: [...rc.exports]
2430
+ };
2431
+ }
2432
+ const previousNames = new Set(
2433
+ def.components ? Object.keys(def.components) : []
2434
+ );
2435
+ const added = resolved.map((rc) => rc.name).filter((name) => !previousNames.has(name));
2436
+ const newDef = {
2437
+ ...def,
2438
+ genart: "1.2",
2439
+ modified: (/* @__PURE__ */ new Date()).toISOString(),
2440
+ components: resolvedRecord
2441
+ };
2442
+ state.sketches.set(input.sketchId, {
2443
+ definition: newDef,
2444
+ path: loaded.path
2445
+ });
2446
+ const json = serializeGenart4(newDef);
2447
+ if (!state.remoteMode) {
2448
+ await writeFile5(loaded.path, json, "utf-8");
2449
+ }
2450
+ state.emitMutation("sketch:updated", {
2451
+ id: input.sketchId,
2452
+ updated: ["components"]
2453
+ });
2454
+ return {
2455
+ success: true,
2456
+ sketchId: input.sketchId,
2457
+ components: resolvedRecord,
2458
+ added,
2459
+ fileContent: json
2460
+ };
2461
+ }
2462
+ async function removeComponent(state, input) {
2463
+ const loaded = state.requireSketch(input.sketchId);
2464
+ const def = loaded.definition;
2465
+ if (!def.components || !def.components[input.component]) {
2466
+ throw new Error(
2467
+ `Component "${input.component}" is not present in sketch "${input.sketchId}"`
2468
+ );
2469
+ }
2470
+ const remaining = { ...def.components };
2471
+ delete remaining[input.component];
2472
+ for (const [name, value] of Object.entries(remaining)) {
2473
+ const entry = COMPONENT_REGISTRY[name];
2474
+ if (entry && entry.dependencies.includes(input.component)) {
2475
+ throw new Error(
2476
+ `Cannot remove "${input.component}": component "${name}" depends on it`
2477
+ );
2478
+ }
2479
+ }
2480
+ let warning;
2481
+ const removedValue = def.components[input.component];
2482
+ const exports = typeof removedValue === "string" ? COMPONENT_REGISTRY[input.component]?.exports ?? [] : removedValue.exports ?? [];
2483
+ const usedExports = exports.filter((exp) => def.algorithm.includes(exp));
2484
+ if (usedExports.length > 0) {
2485
+ warning = `Algorithm may reference these exports from "${input.component}": ${usedExports.join(", ")}. Review your algorithm after removal.`;
2486
+ }
2487
+ const removed = [input.component];
2488
+ const neededDeps = /* @__PURE__ */ new Set();
2489
+ for (const name of Object.keys(remaining)) {
2490
+ const entry = COMPONENT_REGISTRY[name];
2491
+ if (entry) {
2492
+ collectTransitiveDeps(name, neededDeps);
2493
+ }
2494
+ }
2495
+ for (const name of Object.keys(remaining)) {
2496
+ if (!neededDeps.has(name) && !isDirectComponent(name, remaining)) {
2497
+ delete remaining[name];
2498
+ removed.push(name);
2499
+ }
2500
+ }
2501
+ const hasRemaining = Object.keys(remaining).length > 0;
2502
+ const newDef = {
2503
+ ...def,
2504
+ modified: (/* @__PURE__ */ new Date()).toISOString(),
2505
+ ...hasRemaining ? { components: remaining } : { components: void 0 }
2506
+ };
2507
+ state.sketches.set(input.sketchId, {
2508
+ definition: newDef,
2509
+ path: loaded.path
2510
+ });
2511
+ const json = serializeGenart4(newDef);
2512
+ if (!state.remoteMode) {
2513
+ await writeFile5(loaded.path, json, "utf-8");
2514
+ }
2515
+ state.emitMutation("sketch:updated", {
2516
+ id: input.sketchId,
2517
+ updated: ["components"]
2518
+ });
2519
+ return {
2520
+ success: true,
2521
+ sketchId: input.sketchId,
2522
+ removed,
2523
+ ...warning ? { warning } : {},
2524
+ fileContent: json
2525
+ };
2526
+ }
2527
+ function collectTransitiveDeps(name, deps) {
2528
+ const entry = COMPONENT_REGISTRY[name];
2529
+ if (!entry) return;
2530
+ deps.add(name);
2531
+ for (const dep of entry.dependencies) {
2532
+ if (!deps.has(dep)) {
2533
+ collectTransitiveDeps(dep, deps);
2534
+ }
2535
+ }
2536
+ }
2537
+ function isDirectComponent(name, components) {
2538
+ return name in components;
2539
+ }
2540
+
2541
+ // src/tools/capture.ts
2542
+ import { writeFile as writeFile6 } from "fs/promises";
2178
2543
  import {
2179
2544
  createDefaultRegistry as createDefaultRegistry2
2180
2545
  } from "@genart-dev/core";
@@ -2352,7 +2717,7 @@ async function buildScreenshotMetadata(state, multi, info) {
2352
2717
  previewPath: info.previewPath
2353
2718
  };
2354
2719
  if (!state.remoteMode) {
2355
- await writeFile5(info.previewPath, multi.previewPng);
2720
+ await writeFile6(info.previewPath, multi.previewPng);
2356
2721
  metadata.savedPreviewTo = info.previewPath;
2357
2722
  }
2358
2723
  return metadata;
@@ -2413,12 +2778,12 @@ async function captureBatch(state, input) {
2413
2778
 
2414
2779
  // src/tools/export.ts
2415
2780
  import { createWriteStream } from "fs";
2416
- import { stat as stat4, writeFile as writeFile6 } from "fs/promises";
2781
+ import { stat as stat4, writeFile as writeFile7 } from "fs/promises";
2417
2782
  import { dirname as dirname6 } from "path";
2418
2783
  import archiver from "archiver";
2419
2784
  import {
2420
2785
  createDefaultRegistry as createDefaultRegistry3,
2421
- serializeGenart as serializeGenart4
2786
+ serializeGenart as serializeGenart5
2422
2787
  } from "@genart-dev/core";
2423
2788
  var registry3 = createDefaultRegistry3();
2424
2789
  async function validateOutputPath(outputPath) {
@@ -2488,7 +2853,7 @@ async function exportHtml(sketch, outputPath) {
2488
2853
  const adapter = registry3.resolve(sketch.renderer.type);
2489
2854
  const html = adapter.generateStandaloneHTML(sketch);
2490
2855
  const content = Buffer.from(html, "utf-8");
2491
- await writeFile6(outputPath, content);
2856
+ await writeFile7(outputPath, content);
2492
2857
  return {
2493
2858
  success: true,
2494
2859
  sketchId: sketch.id,
@@ -2504,7 +2869,7 @@ async function exportPng(sketch, input) {
2504
2869
  const width = input.width ?? sketch.canvas.width;
2505
2870
  const height = input.height ?? sketch.canvas.height;
2506
2871
  const result = await captureHtml({ html, width, height });
2507
- await writeFile6(input.outputPath, result.bytes);
2872
+ await writeFile7(input.outputPath, result.bytes);
2508
2873
  return {
2509
2874
  success: true,
2510
2875
  sketchId: sketch.id,
@@ -2519,7 +2884,7 @@ async function exportSvg(sketch, input) {
2519
2884
  const height = input.height ?? sketch.canvas.height;
2520
2885
  if (sketch.renderer.type === "svg") {
2521
2886
  const content2 = Buffer.from(sketch.algorithm, "utf-8");
2522
- await writeFile6(input.outputPath, content2);
2887
+ await writeFile7(input.outputPath, content2);
2523
2888
  return {
2524
2889
  success: true,
2525
2890
  sketchId: sketch.id,
@@ -2541,7 +2906,7 @@ async function exportSvg(sketch, input) {
2541
2906
  href="data:image/png;base64,${b64}"/>
2542
2907
  </svg>`;
2543
2908
  const content = Buffer.from(svg, "utf-8");
2544
- await writeFile6(input.outputPath, content);
2909
+ await writeFile7(input.outputPath, content);
2545
2910
  return {
2546
2911
  success: true,
2547
2912
  sketchId: sketch.id,
@@ -2554,7 +2919,7 @@ async function exportSvg(sketch, input) {
2554
2919
  }
2555
2920
  async function exportAlgorithm(sketch, outputPath) {
2556
2921
  const content = Buffer.from(sketch.algorithm, "utf-8");
2557
- await writeFile6(outputPath, content);
2922
+ await writeFile7(outputPath, content);
2558
2923
  return {
2559
2924
  success: true,
2560
2925
  sketchId: sketch.id,
@@ -2569,7 +2934,7 @@ async function exportZip(sketch, input) {
2569
2934
  const width = input.width ?? sketch.canvas.width;
2570
2935
  const height = input.height ?? sketch.canvas.height;
2571
2936
  const html = adapter.generateStandaloneHTML(sketch);
2572
- const genartJson = serializeGenart4(sketch);
2937
+ const genartJson = serializeGenart5(sketch);
2573
2938
  const algorithm = sketch.algorithm;
2574
2939
  const algExt = algorithmExtension(sketch.renderer.type);
2575
2940
  const captureResult = await captureHtml({ html, width, height });
@@ -2603,6 +2968,330 @@ async function exportZip(sketch, input) {
2603
2968
  };
2604
2969
  }
2605
2970
 
2971
+ // src/tools/design.ts
2972
+ function requireSketchId(state, args) {
2973
+ return args.sketchId ?? state.requireSelectedSketchId();
2974
+ }
2975
+ var BLEND_MODES = [
2976
+ "normal",
2977
+ "multiply",
2978
+ "screen",
2979
+ "overlay",
2980
+ "darken",
2981
+ "lighten",
2982
+ "color-dodge",
2983
+ "color-burn",
2984
+ "hard-light",
2985
+ "soft-light",
2986
+ "difference",
2987
+ "exclusion",
2988
+ "hue",
2989
+ "saturation",
2990
+ "color",
2991
+ "luminosity"
2992
+ ];
2993
+ function generateLayerId() {
2994
+ return `layer-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
2995
+ }
2996
+ async function designAddLayer(state, args) {
2997
+ const sketchId = requireSketchId(state, args);
2998
+ const loaded = state.requireSketch(sketchId);
2999
+ const stack = state.getLayerStack(sketchId);
3000
+ const registry4 = state.pluginRegistry;
3001
+ const layerTypeDef = registry4?.resolveLayerType(args.type);
3002
+ if (!layerTypeDef) {
3003
+ throw new Error(
3004
+ `Unknown layer type: '${args.type}'. Use design_list_layers types from registered plugins.`
3005
+ );
3006
+ }
3007
+ const defaults = layerTypeDef.createDefault();
3008
+ const id = generateLayerId();
3009
+ const { width, height } = loaded.definition.canvas;
3010
+ const layer = {
3011
+ id,
3012
+ type: args.type,
3013
+ name: args.name ?? layerTypeDef.displayName,
3014
+ visible: true,
3015
+ locked: false,
3016
+ opacity: args.opacity ?? 1,
3017
+ blendMode: args.blendMode ?? "normal",
3018
+ transform: {
3019
+ x: 0,
3020
+ y: 0,
3021
+ width,
3022
+ height,
3023
+ rotation: 0,
3024
+ scaleX: 1,
3025
+ scaleY: 1,
3026
+ anchorX: 0.5,
3027
+ anchorY: 0.5,
3028
+ ...args.transform
3029
+ },
3030
+ properties: { ...defaults, ...args.properties }
3031
+ };
3032
+ stack.add(layer, args.index);
3033
+ await state.saveSketch(sketchId);
3034
+ return {
3035
+ layerId: id,
3036
+ type: args.type,
3037
+ name: layer.name,
3038
+ index: args.index ?? stack.count - 1,
3039
+ sketchId
3040
+ };
3041
+ }
3042
+ async function designRemoveLayer(state, args) {
3043
+ const sketchId = requireSketchId(state, args);
3044
+ const stack = state.getLayerStack(sketchId);
3045
+ const removed = stack.remove(args.layerId);
3046
+ if (!removed) {
3047
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
3048
+ }
3049
+ await state.saveSketch(sketchId);
3050
+ return { removed: true, layerId: args.layerId, sketchId };
3051
+ }
3052
+ async function designListLayers(state, args) {
3053
+ const sketchId = requireSketchId(state, args);
3054
+ const stack = state.getLayerStack(sketchId);
3055
+ const layers = stack.getAll();
3056
+ return {
3057
+ sketchId,
3058
+ count: layers.length,
3059
+ layers: layers.map((l, i) => ({
3060
+ index: i,
3061
+ id: l.id,
3062
+ type: l.type,
3063
+ name: l.name,
3064
+ visible: l.visible,
3065
+ locked: l.locked,
3066
+ opacity: l.opacity,
3067
+ blendMode: l.blendMode
3068
+ }))
3069
+ };
3070
+ }
3071
+ async function designGetLayer(state, args) {
3072
+ const sketchId = requireSketchId(state, args);
3073
+ const stack = state.getLayerStack(sketchId);
3074
+ const layer = stack.get(args.layerId);
3075
+ if (!layer) {
3076
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
3077
+ }
3078
+ return {
3079
+ sketchId,
3080
+ layer: {
3081
+ id: layer.id,
3082
+ type: layer.type,
3083
+ name: layer.name,
3084
+ visible: layer.visible,
3085
+ locked: layer.locked,
3086
+ opacity: layer.opacity,
3087
+ blendMode: layer.blendMode,
3088
+ transform: layer.transform,
3089
+ properties: layer.properties
3090
+ }
3091
+ };
3092
+ }
3093
+ async function designUpdateLayer(state, args) {
3094
+ const sketchId = requireSketchId(state, args);
3095
+ const stack = state.getLayerStack(sketchId);
3096
+ const layer = stack.get(args.layerId);
3097
+ if (!layer) {
3098
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
3099
+ }
3100
+ const updates = {};
3101
+ if (args.properties) {
3102
+ Object.assign(updates, args.properties);
3103
+ }
3104
+ if (Object.keys(updates).length > 0) {
3105
+ stack.updateProperties(args.layerId, updates);
3106
+ }
3107
+ if (args.name !== void 0) {
3108
+ const current = stack.get(args.layerId);
3109
+ stack.updateProperties(args.layerId, { ...current.properties });
3110
+ const mutableLayer = stack.get(args.layerId);
3111
+ mutableLayer.name = args.name;
3112
+ }
3113
+ await state.saveSketch(sketchId);
3114
+ return { updated: true, layerId: args.layerId, sketchId };
3115
+ }
3116
+ async function designSetTransform(state, args) {
3117
+ const sketchId = requireSketchId(state, args);
3118
+ const stack = state.getLayerStack(sketchId);
3119
+ const layer = stack.get(args.layerId);
3120
+ if (!layer) {
3121
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
3122
+ }
3123
+ const partial = {};
3124
+ if (args.x !== void 0) partial.x = args.x;
3125
+ if (args.y !== void 0) partial.y = args.y;
3126
+ if (args.width !== void 0) partial.width = args.width;
3127
+ if (args.height !== void 0) partial.height = args.height;
3128
+ if (args.rotation !== void 0) partial.rotation = args.rotation;
3129
+ if (args.scaleX !== void 0) partial.scaleX = args.scaleX;
3130
+ if (args.scaleY !== void 0) partial.scaleY = args.scaleY;
3131
+ if (args.anchorX !== void 0) partial.anchorX = args.anchorX;
3132
+ if (args.anchorY !== void 0) partial.anchorY = args.anchorY;
3133
+ stack.updateTransform(args.layerId, partial);
3134
+ await state.saveSketch(sketchId);
3135
+ return {
3136
+ updated: true,
3137
+ layerId: args.layerId,
3138
+ transform: stack.get(args.layerId).transform,
3139
+ sketchId
3140
+ };
3141
+ }
3142
+ async function designSetBlend(state, args) {
3143
+ const sketchId = requireSketchId(state, args);
3144
+ const stack = state.getLayerStack(sketchId);
3145
+ const layer = stack.get(args.layerId);
3146
+ if (!layer) {
3147
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
3148
+ }
3149
+ if (args.blendMode && !BLEND_MODES.includes(args.blendMode)) {
3150
+ throw new Error(
3151
+ `Invalid blend mode '${args.blendMode}'. Must be one of: ${BLEND_MODES.join(", ")}`
3152
+ );
3153
+ }
3154
+ stack.updateBlend(
3155
+ args.layerId,
3156
+ args.blendMode,
3157
+ args.opacity
3158
+ );
3159
+ await state.saveSketch(sketchId);
3160
+ const updated = stack.get(args.layerId);
3161
+ return {
3162
+ updated: true,
3163
+ layerId: args.layerId,
3164
+ blendMode: updated.blendMode,
3165
+ opacity: updated.opacity,
3166
+ sketchId
3167
+ };
3168
+ }
3169
+ async function designReorderLayers(state, args) {
3170
+ const sketchId = requireSketchId(state, args);
3171
+ const stack = state.getLayerStack(sketchId);
3172
+ const layer = stack.get(args.layerId);
3173
+ if (!layer) {
3174
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
3175
+ }
3176
+ stack.reorder(args.layerId, args.newIndex);
3177
+ await state.saveSketch(sketchId);
3178
+ return {
3179
+ reordered: true,
3180
+ layerId: args.layerId,
3181
+ newIndex: args.newIndex,
3182
+ sketchId
3183
+ };
3184
+ }
3185
+ async function designDuplicateLayer(state, args) {
3186
+ const sketchId = requireSketchId(state, args);
3187
+ const stack = state.getLayerStack(sketchId);
3188
+ const layer = stack.get(args.layerId);
3189
+ if (!layer) {
3190
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
3191
+ }
3192
+ const newId = stack.duplicate(args.layerId);
3193
+ await state.saveSketch(sketchId);
3194
+ return {
3195
+ duplicated: true,
3196
+ sourceLayerId: args.layerId,
3197
+ newLayerId: newId,
3198
+ sketchId
3199
+ };
3200
+ }
3201
+ async function designToggleVisibility(state, args) {
3202
+ const sketchId = requireSketchId(state, args);
3203
+ const stack = state.getLayerStack(sketchId);
3204
+ const layer = stack.get(args.layerId);
3205
+ if (!layer) {
3206
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
3207
+ }
3208
+ const newVisible = args.visible ?? !layer.visible;
3209
+ const mutableLayer = layer;
3210
+ mutableLayer.visible = newVisible;
3211
+ stack.updateProperties(args.layerId, { ...layer.properties });
3212
+ await state.saveSketch(sketchId);
3213
+ return {
3214
+ layerId: args.layerId,
3215
+ visible: newVisible,
3216
+ sketchId
3217
+ };
3218
+ }
3219
+ async function designLockLayer(state, args) {
3220
+ const sketchId = requireSketchId(state, args);
3221
+ const stack = state.getLayerStack(sketchId);
3222
+ const layer = stack.get(args.layerId);
3223
+ if (!layer) {
3224
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
3225
+ }
3226
+ const newLocked = args.locked ?? !layer.locked;
3227
+ const mutableLayer = layer;
3228
+ mutableLayer.locked = newLocked;
3229
+ stack.updateProperties(args.layerId, { ...layer.properties });
3230
+ await state.saveSketch(sketchId);
3231
+ return {
3232
+ layerId: args.layerId,
3233
+ locked: newLocked,
3234
+ sketchId
3235
+ };
3236
+ }
3237
+ async function designCaptureComposite(state, args) {
3238
+ const sketchId = requireSketchId(state, args);
3239
+ const stack = state.getLayerStack(sketchId);
3240
+ const layers = stack.getAll();
3241
+ return {
3242
+ sketchId,
3243
+ layerCount: layers.length,
3244
+ visibleCount: layers.filter((l) => l.visible).length,
3245
+ message: "Composite capture requires a rendering surface. Use capture_screenshot to get a rasterized preview of the sketch, then use design_list_layers to see the design layer stack."
3246
+ };
3247
+ }
3248
+
3249
+ // src/tools/design-plugins.ts
3250
+ function registerPluginMcpTools(server, registry4, state) {
3251
+ for (const tool of registry4.getMcpTools()) {
3252
+ const inputSchema = tool.definition.inputSchema;
3253
+ server.tool(
3254
+ tool.name,
3255
+ tool.definition.description,
3256
+ // Pass raw JSON schema — MCP SDK accepts this alongside Zod
3257
+ inputSchema,
3258
+ async (args) => {
3259
+ try {
3260
+ const sketchId = args.sketchId ?? state.requireSelectedSketchId();
3261
+ const context = state.createMcpToolContext(sketchId);
3262
+ const result = await tool.definition.handler(args, context);
3263
+ await state.saveSketch(sketchId);
3264
+ return {
3265
+ content: result.content.map((c) => {
3266
+ if (c.type === "text") {
3267
+ return { type: "text", text: c.text };
3268
+ }
3269
+ return {
3270
+ type: "image",
3271
+ data: c.data,
3272
+ mimeType: c.mimeType
3273
+ };
3274
+ }),
3275
+ isError: result.isError
3276
+ };
3277
+ } catch (e) {
3278
+ return {
3279
+ content: [
3280
+ {
3281
+ type: "text",
3282
+ text: JSON.stringify({
3283
+ error: e instanceof Error ? e.message : String(e)
3284
+ })
3285
+ }
3286
+ ],
3287
+ isError: true
3288
+ };
3289
+ }
3290
+ }
3291
+ );
3292
+ }
3293
+ }
3294
+
2606
3295
  // src/resources/index.ts
2607
3296
  import {
2608
3297
  CANVAS_PRESETS,
@@ -3090,11 +3779,23 @@ function toolError(message) {
3090
3779
  isError: true
3091
3780
  };
3092
3781
  }
3782
+ async function initializePluginRegistry() {
3783
+ const registry4 = createPluginRegistry({
3784
+ surface: "mcp",
3785
+ supportsInteractiveTools: false,
3786
+ supportsRendering: false
3787
+ });
3788
+ await registry4.register(typographyPlugin);
3789
+ await registry4.register(filtersPlugin);
3790
+ await registry4.register(shapesPlugin);
3791
+ await registry4.register(layoutGuidesPlugin);
3792
+ return registry4;
3793
+ }
3093
3794
  function createServer(state) {
3094
3795
  const server = new McpServer(
3095
3796
  {
3096
3797
  name: "@genart/mcp-server",
3097
- version: "0.0.1"
3798
+ version: "0.3.0"
3098
3799
  },
3099
3800
  {
3100
3801
  capabilities: {
@@ -3104,8 +3805,14 @@ function createServer(state) {
3104
3805
  }
3105
3806
  }
3106
3807
  );
3808
+ const registryReady = initializePluginRegistry().then((registry4) => {
3809
+ state.pluginRegistry = registry4;
3810
+ registerPluginMcpTools(server, registry4, state);
3811
+ });
3812
+ server._pluginsReady = registryReady;
3107
3813
  registerWorkspaceTools(server, state);
3108
3814
  registerSketchTools(server, state);
3815
+ registerComponentTools(server, state);
3109
3816
  registerSelectionTools(server, state);
3110
3817
  registerParameterTools(server, state);
3111
3818
  registerArrangementTools(server, state);
@@ -3113,6 +3820,7 @@ function createServer(state) {
3113
3820
  registerMergeTools(server, state);
3114
3821
  registerSnapshotTools(server, state);
3115
3822
  registerKnowledgeTools(server, state);
3823
+ registerDesignTools(server, state);
3116
3824
  registerCaptureTools(server, state);
3117
3825
  registerExportTools(server, state);
3118
3826
  registerResources(server, state);
@@ -3209,7 +3917,7 @@ function registerWorkspaceTools(server, state) {
3209
3917
  function registerSketchTools(server, state) {
3210
3918
  server.tool(
3211
3919
  "create_sketch",
3212
- "Create a new .genart sketch file from metadata, parameters, and algorithm",
3920
+ 'Create a new .genart sketch file from metadata, parameters, and algorithm. IMPORTANT: Do not embed common utilities (PRNG, noise, easing, color math, vector ops) inline in the algorithm. Instead, declare them as components: { "prng": "^1.0.0", "noise-2d": "^1.0.0" }. Then use the exported functions directly in your algorithm (e.g., mulberry32, fbm2D). Use list_components to see all available components for the current renderer.',
3213
3921
  {
3214
3922
  id: z2.string().describe("URL-safe kebab-case identifier"),
3215
3923
  title: z2.string().describe("Human-readable title"),
@@ -3247,6 +3955,16 @@ function registerSketchTools(server, state) {
3247
3955
  algorithm: z2.string().optional().describe("Algorithm source code (default: renderer template). For p5: must be `function sketch(p, state) { ... }` in instance mode. State provides: state.WIDTH, state.HEIGHT, state.SEED (number), state.PARAMS (keyed by param key), state.COLORS (keyed by color key, hex strings). Use p5 instance methods (p.createCanvas, p.background, etc)."),
3248
3956
  seed: z2.number().optional().describe("Initial random seed (default: random)"),
3249
3957
  skills: z2.array(z2.string()).optional().describe("Design skill references"),
3958
+ components: z2.record(
3959
+ z2.union([
3960
+ z2.string(),
3961
+ z2.object({
3962
+ version: z2.string().optional(),
3963
+ code: z2.string().optional(),
3964
+ exports: z2.array(z2.string()).optional()
3965
+ })
3966
+ ])
3967
+ ).optional().describe('Component dependencies. Use list_components to see available. Keys are component names, values are semver ranges (e.g. "^1.0.0") or objects with version/code/exports.'),
3250
3968
  addToWorkspace: z2.string().optional().describe("Path to workspace to add sketch to after creation"),
3251
3969
  agent: z2.string().optional().describe("Your CLI agent name (e.g. 'claude-code', 'codex-cli', 'gemini-cli', 'opencode', 'kiro')"),
3252
3970
  model: z2.string().optional().describe("Your AI model identifier (e.g. 'claude-opus-4-6', 'gpt-4o', 'gemini-2.5-pro')")
@@ -3326,11 +4044,21 @@ function registerSketchTools(server, state) {
3326
4044
  );
3327
4045
  server.tool(
3328
4046
  "update_algorithm",
3329
- "Replace the algorithm source code of a sketch",
4047
+ "Replace the algorithm source code of a sketch. If adding/changing components, pass them in the components field alongside the algorithm.",
3330
4048
  {
3331
4049
  sketchId: z2.string().describe("ID of the sketch to update"),
3332
4050
  algorithm: z2.string().describe("New algorithm source code. For p5: must be `function sketch(p, state) { ... }` in instance mode. State provides: state.WIDTH, state.HEIGHT, state.SEED, state.PARAMS (keyed by param key), state.COLORS (keyed by color key)."),
3333
4051
  validate: z2.boolean().optional().describe("Run renderer-specific validation before saving (default: true)"),
4052
+ components: z2.record(
4053
+ z2.union([
4054
+ z2.string(),
4055
+ z2.object({
4056
+ version: z2.string().optional(),
4057
+ code: z2.string().optional(),
4058
+ exports: z2.array(z2.string()).optional()
4059
+ })
4060
+ ])
4061
+ ).optional().describe("Component dependencies to resolve alongside the algorithm update. Use list_components to see available."),
3334
4062
  agent: z2.string().optional().describe("Your CLI agent name (e.g. 'claude-code', 'codex-cli', 'gemini-cli', 'opencode', 'kiro')"),
3335
4063
  model: z2.string().optional().describe("Your AI model identifier (e.g. 'claude-opus-4-6', 'gpt-4o', 'gemini-2.5-pro')")
3336
4064
  },
@@ -3426,6 +4154,76 @@ function registerSketchTools(server, state) {
3426
4154
  }
3427
4155
  );
3428
4156
  }
4157
+ function registerComponentTools(server, state) {
4158
+ server.tool(
4159
+ "list_components",
4160
+ "List available reusable components from the registry, filtered by renderer and/or category. Components provide common utilities (PRNG, noise, easing, color math, etc.) that can be declared as dependencies instead of inlining code in the algorithm.",
4161
+ {
4162
+ renderer: z2.enum(["p5", "three", "glsl", "canvas2d", "svg"]).optional().describe("Filter by renderer compatibility"),
4163
+ category: z2.enum([
4164
+ "randomness",
4165
+ "noise",
4166
+ "math",
4167
+ "easing",
4168
+ "color",
4169
+ "vector",
4170
+ "geometry",
4171
+ "grid",
4172
+ "particle",
4173
+ "physics",
4174
+ "distribution",
4175
+ "pattern",
4176
+ "sdf",
4177
+ "transform",
4178
+ "animation",
4179
+ "string",
4180
+ "data-structure",
4181
+ "imaging"
4182
+ ]).optional().describe("Filter by component category")
4183
+ },
4184
+ async (args) => {
4185
+ try {
4186
+ const result = await listComponents(state, args);
4187
+ return jsonResult(result);
4188
+ } catch (e) {
4189
+ return toolError(e instanceof Error ? e.message : String(e));
4190
+ }
4191
+ }
4192
+ );
4193
+ server.tool(
4194
+ "add_component",
4195
+ "Add a component dependency to an existing sketch. Resolves the component and any transitive dependencies from the registry, validates renderer compatibility, and writes the resolved form to the sketch file.",
4196
+ {
4197
+ sketchId: z2.string().describe("ID of the sketch to add the component to"),
4198
+ component: z2.string().describe("Component name (e.g. 'prng', 'noise-2d', 'glsl-noise')"),
4199
+ version: z2.string().optional().describe("Version range (default: '^1.0.0')")
4200
+ },
4201
+ async (args) => {
4202
+ try {
4203
+ const result = await addComponent(state, args);
4204
+ return jsonResult(result);
4205
+ } catch (e) {
4206
+ return toolError(e instanceof Error ? e.message : String(e));
4207
+ }
4208
+ }
4209
+ );
4210
+ server.tool(
4211
+ "remove_component",
4212
+ "Remove a component dependency from a sketch. Checks for dependent components and warns if the algorithm references the component's exports.",
4213
+ {
4214
+ sketchId: z2.string().describe("ID of the sketch to remove the component from"),
4215
+ component: z2.string().describe("Component name to remove")
4216
+ },
4217
+ async (args) => {
4218
+ try {
4219
+ const result = await removeComponent(state, args);
4220
+ return jsonResult(result);
4221
+ } catch (e) {
4222
+ return toolError(e instanceof Error ? e.message : String(e));
4223
+ }
4224
+ }
4225
+ );
4226
+ }
3429
4227
  function registerSelectionTools(server, state) {
3430
4228
  server.tool(
3431
4229
  "get_selection",
@@ -3811,6 +4609,247 @@ function registerExportTools(server, state) {
3811
4609
  }
3812
4610
  );
3813
4611
  }
4612
+ function registerDesignTools(server, state) {
4613
+ server.tool(
4614
+ "design_add_layer",
4615
+ "Add a new design layer of a given type to the active sketch. Layer types come from registered plugins (e.g. 'typography:text', 'filter:grain', 'shapes:rect', 'guides:thirds').",
4616
+ {
4617
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4618
+ type: z2.string().describe("Layer type ID (e.g. 'typography:text', 'filter:grain', 'shapes:rect')"),
4619
+ name: z2.string().optional().describe("Layer display name (default: type's display name)"),
4620
+ properties: z2.record(z2.unknown()).optional().describe("Initial layer properties (merged with type defaults)"),
4621
+ transform: z2.object({
4622
+ x: z2.number().optional(),
4623
+ y: z2.number().optional(),
4624
+ width: z2.number().optional(),
4625
+ height: z2.number().optional(),
4626
+ rotation: z2.number().optional(),
4627
+ scaleX: z2.number().optional(),
4628
+ scaleY: z2.number().optional(),
4629
+ anchorX: z2.number().optional(),
4630
+ anchorY: z2.number().optional()
4631
+ }).optional().describe("Layer transform (default: full canvas)"),
4632
+ opacity: z2.number().optional().describe("Layer opacity 0\u20131 (default: 1)"),
4633
+ blendMode: z2.string().optional().describe("Blend mode (default: 'normal')"),
4634
+ index: z2.number().optional().describe("Insert position in layer stack (default: top)")
4635
+ },
4636
+ async (args) => {
4637
+ try {
4638
+ const result = await designAddLayer(state, args);
4639
+ return jsonResult(result);
4640
+ } catch (e) {
4641
+ return toolError(e instanceof Error ? e.message : String(e));
4642
+ }
4643
+ }
4644
+ );
4645
+ server.tool(
4646
+ "design_remove_layer",
4647
+ "Remove a design layer from the active sketch",
4648
+ {
4649
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4650
+ layerId: z2.string().describe("ID of the layer to remove")
4651
+ },
4652
+ async (args) => {
4653
+ try {
4654
+ const result = await designRemoveLayer(state, args);
4655
+ return jsonResult(result);
4656
+ } catch (e) {
4657
+ return toolError(e instanceof Error ? e.message : String(e));
4658
+ }
4659
+ }
4660
+ );
4661
+ server.tool(
4662
+ "design_list_layers",
4663
+ "List all design layers in the active sketch with their types, visibility, and key properties",
4664
+ {
4665
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)")
4666
+ },
4667
+ async (args) => {
4668
+ try {
4669
+ const result = await designListLayers(state, args);
4670
+ return jsonResult(result);
4671
+ } catch (e) {
4672
+ return toolError(e instanceof Error ? e.message : String(e));
4673
+ }
4674
+ }
4675
+ );
4676
+ server.tool(
4677
+ "design_get_layer",
4678
+ "Get full details of a single design layer including all properties and transform",
4679
+ {
4680
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4681
+ layerId: z2.string().describe("ID of the layer to inspect")
4682
+ },
4683
+ async (args) => {
4684
+ try {
4685
+ const result = await designGetLayer(state, args);
4686
+ return jsonResult(result);
4687
+ } catch (e) {
4688
+ return toolError(e instanceof Error ? e.message : String(e));
4689
+ }
4690
+ }
4691
+ );
4692
+ server.tool(
4693
+ "design_update_layer",
4694
+ "Update properties on a design layer (e.g. text content, filter intensity, shape fill color)",
4695
+ {
4696
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4697
+ layerId: z2.string().describe("ID of the layer to update"),
4698
+ name: z2.string().optional().describe("New display name"),
4699
+ properties: z2.record(z2.unknown()).optional().describe("Property key-value pairs to set")
4700
+ },
4701
+ async (args) => {
4702
+ try {
4703
+ const result = await designUpdateLayer(state, args);
4704
+ return jsonResult(result);
4705
+ } catch (e) {
4706
+ return toolError(e instanceof Error ? e.message : String(e));
4707
+ }
4708
+ }
4709
+ );
4710
+ server.tool(
4711
+ "design_set_transform",
4712
+ "Set the position, size, rotation, and scale of a design layer",
4713
+ {
4714
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4715
+ layerId: z2.string().describe("ID of the layer to transform"),
4716
+ x: z2.number().optional().describe("X position"),
4717
+ y: z2.number().optional().describe("Y position"),
4718
+ width: z2.number().optional().describe("Width"),
4719
+ height: z2.number().optional().describe("Height"),
4720
+ rotation: z2.number().optional().describe("Rotation in degrees"),
4721
+ scaleX: z2.number().optional().describe("Horizontal scale"),
4722
+ scaleY: z2.number().optional().describe("Vertical scale"),
4723
+ anchorX: z2.number().optional().describe("Anchor X (0\u20131)"),
4724
+ anchorY: z2.number().optional().describe("Anchor Y (0\u20131)")
4725
+ },
4726
+ async (args) => {
4727
+ try {
4728
+ const result = await designSetTransform(state, args);
4729
+ return jsonResult(result);
4730
+ } catch (e) {
4731
+ return toolError(e instanceof Error ? e.message : String(e));
4732
+ }
4733
+ }
4734
+ );
4735
+ server.tool(
4736
+ "design_set_blend",
4737
+ "Set blend mode and/or opacity on a design layer",
4738
+ {
4739
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4740
+ layerId: z2.string().describe("ID of the layer"),
4741
+ blendMode: z2.enum([
4742
+ "normal",
4743
+ "multiply",
4744
+ "screen",
4745
+ "overlay",
4746
+ "darken",
4747
+ "lighten",
4748
+ "color-dodge",
4749
+ "color-burn",
4750
+ "hard-light",
4751
+ "soft-light",
4752
+ "difference",
4753
+ "exclusion",
4754
+ "hue",
4755
+ "saturation",
4756
+ "color",
4757
+ "luminosity"
4758
+ ]).optional().describe("CSS blend mode"),
4759
+ opacity: z2.number().optional().describe("Layer opacity 0\u20131")
4760
+ },
4761
+ async (args) => {
4762
+ try {
4763
+ const result = await designSetBlend(state, args);
4764
+ return jsonResult(result);
4765
+ } catch (e) {
4766
+ return toolError(e instanceof Error ? e.message : String(e));
4767
+ }
4768
+ }
4769
+ );
4770
+ server.tool(
4771
+ "design_reorder_layers",
4772
+ "Move a design layer to a new position in the z-order stack",
4773
+ {
4774
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4775
+ layerId: z2.string().describe("ID of the layer to move"),
4776
+ newIndex: z2.number().describe("New position (0 = bottom, n-1 = top)")
4777
+ },
4778
+ async (args) => {
4779
+ try {
4780
+ const result = await designReorderLayers(state, args);
4781
+ return jsonResult(result);
4782
+ } catch (e) {
4783
+ return toolError(e instanceof Error ? e.message : String(e));
4784
+ }
4785
+ }
4786
+ );
4787
+ server.tool(
4788
+ "design_duplicate_layer",
4789
+ "Clone a design layer with a new ID, inserted directly above the source",
4790
+ {
4791
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4792
+ layerId: z2.string().describe("ID of the layer to duplicate")
4793
+ },
4794
+ async (args) => {
4795
+ try {
4796
+ const result = await designDuplicateLayer(state, args);
4797
+ return jsonResult(result);
4798
+ } catch (e) {
4799
+ return toolError(e instanceof Error ? e.message : String(e));
4800
+ }
4801
+ }
4802
+ );
4803
+ server.tool(
4804
+ "design_toggle_visibility",
4805
+ "Show or hide a design layer",
4806
+ {
4807
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4808
+ layerId: z2.string().describe("ID of the layer"),
4809
+ visible: z2.boolean().optional().describe("Set visibility (default: toggle)")
4810
+ },
4811
+ async (args) => {
4812
+ try {
4813
+ const result = await designToggleVisibility(state, args);
4814
+ return jsonResult(result);
4815
+ } catch (e) {
4816
+ return toolError(e instanceof Error ? e.message : String(e));
4817
+ }
4818
+ }
4819
+ );
4820
+ server.tool(
4821
+ "design_lock_layer",
4822
+ "Lock or unlock a design layer to prevent accidental edits",
4823
+ {
4824
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4825
+ layerId: z2.string().describe("ID of the layer"),
4826
+ locked: z2.boolean().optional().describe("Set lock state (default: toggle)")
4827
+ },
4828
+ async (args) => {
4829
+ try {
4830
+ const result = await designLockLayer(state, args);
4831
+ return jsonResult(result);
4832
+ } catch (e) {
4833
+ return toolError(e instanceof Error ? e.message : String(e));
4834
+ }
4835
+ }
4836
+ );
4837
+ server.tool(
4838
+ "design_capture_composite",
4839
+ "Get info about the design layer composite for a sketch. For full visual capture use capture_screenshot.",
4840
+ {
4841
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)")
4842
+ },
4843
+ async (args) => {
4844
+ try {
4845
+ const result = await designCaptureComposite(state, args);
4846
+ return jsonResult(result);
4847
+ } catch (e) {
4848
+ return toolError(e instanceof Error ? e.message : String(e));
4849
+ }
4850
+ }
4851
+ );
4852
+ }
3814
4853
  function registerKnowledgeTools(server, _state) {
3815
4854
  server.tool(
3816
4855
  "list_skills",