@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/lib.js CHANGED
@@ -1,6 +1,11 @@
1
1
  // src/server.ts
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { z as z2 } from "zod";
4
+ import { createPluginRegistry } from "@genart-dev/core";
5
+ import typographyPlugin from "@genart-dev/plugin-typography";
6
+ import filtersPlugin from "@genart-dev/plugin-filters";
7
+ import shapesPlugin from "@genart-dev/plugin-shapes";
8
+ import layoutGuidesPlugin from "@genart-dev/plugin-layout-guides";
4
9
 
5
10
  // src/tools/workspace.ts
6
11
  import { readFile, writeFile, stat } from "fs/promises";
@@ -345,6 +350,7 @@ import { writeFile as writeFile2, stat as stat2, unlink } from "fs/promises";
345
350
  import { basename as basename2, dirname as dirname2, resolve } from "path";
346
351
  import {
347
352
  createDefaultRegistry,
353
+ resolveComponents,
348
354
  resolvePreset,
349
355
  serializeGenart,
350
356
  serializeWorkspace as serializeWorkspace2
@@ -441,10 +447,40 @@ async function createSketch(state, input) {
441
447
  const adapter = registry4.resolve(rendererType);
442
448
  algorithm = adapter.getAlgorithmTemplate();
443
449
  }
450
+ let resolvedComponents;
451
+ if (input.components && Object.keys(input.components).length > 0) {
452
+ const shorthand = {};
453
+ for (const [name, value] of Object.entries(input.components)) {
454
+ if (typeof value === "string") {
455
+ shorthand[name] = value;
456
+ } else if (value.version) {
457
+ shorthand[name] = value.version;
458
+ } else if (value.code) {
459
+ if (!resolvedComponents) resolvedComponents = {};
460
+ resolvedComponents[name] = {
461
+ ...value.version ? { version: value.version } : {},
462
+ code: value.code,
463
+ ...value.exports ? { exports: value.exports } : {}
464
+ };
465
+ }
466
+ }
467
+ if (Object.keys(shorthand).length > 0) {
468
+ const resolved = resolveComponents(shorthand, rendererType);
469
+ if (!resolvedComponents) resolvedComponents = {};
470
+ for (const rc of resolved) {
471
+ resolvedComponents[rc.name] = {
472
+ version: rc.version,
473
+ code: rc.code,
474
+ exports: [...rc.exports]
475
+ };
476
+ }
477
+ }
478
+ }
444
479
  const seed = input.seed ?? Math.floor(Math.random() * 1e5);
445
480
  const ts = now2();
481
+ const hasComponents = resolvedComponents && Object.keys(resolvedComponents).length > 0;
446
482
  const sketch = {
447
- genart: "1.1",
483
+ genart: hasComponents ? "1.2" : "1.1",
448
484
  id: input.id,
449
485
  title: input.title,
450
486
  created: ts,
@@ -458,6 +494,7 @@ async function createSketch(state, input) {
458
494
  ...input.philosophy ? { philosophy: input.philosophy } : {},
459
495
  ...input.themes && input.themes.length > 0 ? { themes: input.themes } : {},
460
496
  ...input.skills && input.skills.length > 0 ? { skills: input.skills } : {},
497
+ ...hasComponents ? { components: resolvedComponents } : {},
461
498
  ...input.agent ? { agent: input.agent } : {},
462
499
  ...input.model ? { model: input.model } : {}
463
500
  };
@@ -641,10 +678,44 @@ async function updateAlgorithm(state, input) {
641
678
  );
642
679
  }
643
680
  }
681
+ let resolvedComponents;
682
+ if (input.components && Object.keys(input.components).length > 0) {
683
+ const renderer = def.renderer.type;
684
+ const shorthand = {};
685
+ for (const [name, value] of Object.entries(input.components)) {
686
+ if (typeof value === "string") {
687
+ shorthand[name] = value;
688
+ } else if (value.version) {
689
+ shorthand[name] = value.version;
690
+ } else if (value.code) {
691
+ if (!resolvedComponents) resolvedComponents = {};
692
+ resolvedComponents[name] = {
693
+ ...value.version ? { version: value.version } : {},
694
+ code: value.code,
695
+ ...value.exports ? { exports: value.exports } : {}
696
+ };
697
+ }
698
+ }
699
+ if (Object.keys(shorthand).length > 0) {
700
+ const resolved = resolveComponents(shorthand, renderer);
701
+ if (!resolvedComponents) resolvedComponents = {};
702
+ for (const rc of resolved) {
703
+ resolvedComponents[rc.name] = {
704
+ version: rc.version,
705
+ code: rc.code,
706
+ exports: [...rc.exports]
707
+ };
708
+ }
709
+ }
710
+ }
711
+ const updated = ["algorithm"];
712
+ const hasNewComponents = resolvedComponents && Object.keys(resolvedComponents).length > 0;
713
+ if (hasNewComponents) updated.push("components");
644
714
  const newDef = {
645
715
  ...def,
646
716
  modified: now2(),
647
717
  algorithm: input.algorithm,
718
+ ...hasNewComponents ? { genart: "1.2", components: resolvedComponents } : {},
648
719
  ...input.agent ? { agent: input.agent } : {},
649
720
  ...input.model ? { model: input.model } : {}
650
721
  };
@@ -658,7 +729,7 @@ async function updateAlgorithm(state, input) {
658
729
  }
659
730
  state.emitMutation("sketch:updated", {
660
731
  id: input.sketchId,
661
- updated: ["algorithm"]
732
+ updated
662
733
  });
663
734
  return {
664
735
  success: true,
@@ -666,6 +737,7 @@ async function updateAlgorithm(state, input) {
666
737
  renderer: def.renderer.type,
667
738
  algorithmLength: input.algorithm.length,
668
739
  validationPassed,
740
+ ...hasNewComponents ? { componentsUpdated: true } : {},
669
741
  fileContent: json
670
742
  };
671
743
  }
@@ -1967,8 +2039,219 @@ ${guidelines}`,
1967
2039
  };
1968
2040
  }
1969
2041
 
1970
- // src/tools/capture.ts
2042
+ // src/tools/components.ts
1971
2043
  import { writeFile as writeFile4 } from "fs/promises";
2044
+ import {
2045
+ COMPONENT_REGISTRY,
2046
+ resolveComponents as resolveComponents2,
2047
+ serializeGenart as serializeGenart3
2048
+ } from "@genart-dev/core";
2049
+ var VALID_RENDERERS2 = [
2050
+ "p5",
2051
+ "three",
2052
+ "glsl",
2053
+ "canvas2d",
2054
+ "svg"
2055
+ ];
2056
+ var RENDERER_TARGET = {
2057
+ p5: "js",
2058
+ three: "js",
2059
+ canvas2d: "js",
2060
+ svg: "js",
2061
+ glsl: "glsl"
2062
+ };
2063
+ async function listComponents(_state, input) {
2064
+ let entries = Object.values(COMPONENT_REGISTRY);
2065
+ if (input.renderer) {
2066
+ const renderer = input.renderer;
2067
+ const target = RENDERER_TARGET[renderer];
2068
+ if (!target) {
2069
+ throw new Error(
2070
+ `Unknown renderer type: '${input.renderer}'. Valid types: ${VALID_RENDERERS2.join(", ")}`
2071
+ );
2072
+ }
2073
+ entries = entries.filter(
2074
+ (e) => e.target === target && (e.renderers.length === 0 || e.renderers.includes(renderer))
2075
+ );
2076
+ }
2077
+ if (input.category) {
2078
+ const cat = input.category;
2079
+ entries = entries.filter((e) => e.category === cat);
2080
+ }
2081
+ entries.sort((a, b) => {
2082
+ const catCmp = a.category.localeCompare(b.category);
2083
+ if (catCmp !== 0) return catCmp;
2084
+ return a.name.localeCompare(b.name);
2085
+ });
2086
+ const components = entries.map((e) => ({
2087
+ name: e.name,
2088
+ version: e.version,
2089
+ category: e.category,
2090
+ target: e.target,
2091
+ exports: [...e.exports],
2092
+ dependencies: [...e.dependencies],
2093
+ description: e.description
2094
+ }));
2095
+ return {
2096
+ count: components.length,
2097
+ components
2098
+ };
2099
+ }
2100
+ async function addComponent(state, input) {
2101
+ const loaded = state.requireSketch(input.sketchId);
2102
+ const def = loaded.definition;
2103
+ const renderer = def.renderer.type;
2104
+ const entry = COMPONENT_REGISTRY[input.component];
2105
+ if (!entry) {
2106
+ throw new Error(`Unknown component: "${input.component}"`);
2107
+ }
2108
+ const target = RENDERER_TARGET[renderer];
2109
+ if (entry.target !== target) {
2110
+ throw new Error(
2111
+ `Component "${input.component}" has target "${entry.target}" but renderer "${renderer}" requires target "${target}"`
2112
+ );
2113
+ }
2114
+ if (entry.renderers.length > 0 && !entry.renderers.includes(renderer)) {
2115
+ throw new Error(
2116
+ `Component "${input.component}" is not compatible with renderer "${renderer}". Compatible: ${entry.renderers.join(", ")}`
2117
+ );
2118
+ }
2119
+ const existingComponents = {};
2120
+ if (def.components) {
2121
+ for (const [name, value] of Object.entries(def.components)) {
2122
+ if (typeof value === "string") {
2123
+ existingComponents[name] = value;
2124
+ } else if (value.version) {
2125
+ existingComponents[name] = value.version;
2126
+ }
2127
+ }
2128
+ }
2129
+ if (existingComponents[input.component]) {
2130
+ throw new Error(
2131
+ `Component "${input.component}" is already present in sketch "${input.sketchId}"`
2132
+ );
2133
+ }
2134
+ existingComponents[input.component] = input.version ?? "^1.0.0";
2135
+ const resolved = resolveComponents2(existingComponents, renderer);
2136
+ const resolvedRecord = {};
2137
+ for (const rc of resolved) {
2138
+ resolvedRecord[rc.name] = {
2139
+ version: rc.version,
2140
+ code: rc.code,
2141
+ exports: [...rc.exports]
2142
+ };
2143
+ }
2144
+ const previousNames = new Set(
2145
+ def.components ? Object.keys(def.components) : []
2146
+ );
2147
+ const added = resolved.map((rc) => rc.name).filter((name) => !previousNames.has(name));
2148
+ const newDef = {
2149
+ ...def,
2150
+ genart: "1.2",
2151
+ modified: (/* @__PURE__ */ new Date()).toISOString(),
2152
+ components: resolvedRecord
2153
+ };
2154
+ state.sketches.set(input.sketchId, {
2155
+ definition: newDef,
2156
+ path: loaded.path
2157
+ });
2158
+ const json = serializeGenart3(newDef);
2159
+ if (!state.remoteMode) {
2160
+ await writeFile4(loaded.path, json, "utf-8");
2161
+ }
2162
+ state.emitMutation("sketch:updated", {
2163
+ id: input.sketchId,
2164
+ updated: ["components"]
2165
+ });
2166
+ return {
2167
+ success: true,
2168
+ sketchId: input.sketchId,
2169
+ components: resolvedRecord,
2170
+ added,
2171
+ fileContent: json
2172
+ };
2173
+ }
2174
+ async function removeComponent(state, input) {
2175
+ const loaded = state.requireSketch(input.sketchId);
2176
+ const def = loaded.definition;
2177
+ if (!def.components || !def.components[input.component]) {
2178
+ throw new Error(
2179
+ `Component "${input.component}" is not present in sketch "${input.sketchId}"`
2180
+ );
2181
+ }
2182
+ const remaining = { ...def.components };
2183
+ delete remaining[input.component];
2184
+ for (const [name, value] of Object.entries(remaining)) {
2185
+ const entry = COMPONENT_REGISTRY[name];
2186
+ if (entry && entry.dependencies.includes(input.component)) {
2187
+ throw new Error(
2188
+ `Cannot remove "${input.component}": component "${name}" depends on it`
2189
+ );
2190
+ }
2191
+ }
2192
+ let warning;
2193
+ const removedValue = def.components[input.component];
2194
+ const exports = typeof removedValue === "string" ? COMPONENT_REGISTRY[input.component]?.exports ?? [] : removedValue.exports ?? [];
2195
+ const usedExports = exports.filter((exp) => def.algorithm.includes(exp));
2196
+ if (usedExports.length > 0) {
2197
+ warning = `Algorithm may reference these exports from "${input.component}": ${usedExports.join(", ")}. Review your algorithm after removal.`;
2198
+ }
2199
+ const removed = [input.component];
2200
+ const neededDeps = /* @__PURE__ */ new Set();
2201
+ for (const name of Object.keys(remaining)) {
2202
+ const entry = COMPONENT_REGISTRY[name];
2203
+ if (entry) {
2204
+ collectTransitiveDeps(name, neededDeps);
2205
+ }
2206
+ }
2207
+ for (const name of Object.keys(remaining)) {
2208
+ if (!neededDeps.has(name) && !isDirectComponent(name, remaining)) {
2209
+ delete remaining[name];
2210
+ removed.push(name);
2211
+ }
2212
+ }
2213
+ const hasRemaining = Object.keys(remaining).length > 0;
2214
+ const newDef = {
2215
+ ...def,
2216
+ modified: (/* @__PURE__ */ new Date()).toISOString(),
2217
+ ...hasRemaining ? { components: remaining } : { components: void 0 }
2218
+ };
2219
+ state.sketches.set(input.sketchId, {
2220
+ definition: newDef,
2221
+ path: loaded.path
2222
+ });
2223
+ const json = serializeGenart3(newDef);
2224
+ if (!state.remoteMode) {
2225
+ await writeFile4(loaded.path, json, "utf-8");
2226
+ }
2227
+ state.emitMutation("sketch:updated", {
2228
+ id: input.sketchId,
2229
+ updated: ["components"]
2230
+ });
2231
+ return {
2232
+ success: true,
2233
+ sketchId: input.sketchId,
2234
+ removed,
2235
+ ...warning ? { warning } : {},
2236
+ fileContent: json
2237
+ };
2238
+ }
2239
+ function collectTransitiveDeps(name, deps) {
2240
+ const entry = COMPONENT_REGISTRY[name];
2241
+ if (!entry) return;
2242
+ deps.add(name);
2243
+ for (const dep of entry.dependencies) {
2244
+ if (!deps.has(dep)) {
2245
+ collectTransitiveDeps(dep, deps);
2246
+ }
2247
+ }
2248
+ }
2249
+ function isDirectComponent(name, components) {
2250
+ return name in components;
2251
+ }
2252
+
2253
+ // src/tools/capture.ts
2254
+ import { writeFile as writeFile5 } from "fs/promises";
1972
2255
  import {
1973
2256
  createDefaultRegistry as createDefaultRegistry2
1974
2257
  } from "@genart-dev/core";
@@ -2146,7 +2429,7 @@ async function buildScreenshotMetadata(state, multi, info) {
2146
2429
  previewPath: info.previewPath
2147
2430
  };
2148
2431
  if (!state.remoteMode) {
2149
- await writeFile4(info.previewPath, multi.previewPng);
2432
+ await writeFile5(info.previewPath, multi.previewPng);
2150
2433
  metadata.savedPreviewTo = info.previewPath;
2151
2434
  }
2152
2435
  return metadata;
@@ -2207,12 +2490,12 @@ async function captureBatch(state, input) {
2207
2490
 
2208
2491
  // src/tools/export.ts
2209
2492
  import { createWriteStream } from "fs";
2210
- import { stat as stat4, writeFile as writeFile5 } from "fs/promises";
2493
+ import { stat as stat4, writeFile as writeFile6 } from "fs/promises";
2211
2494
  import { dirname as dirname5 } from "path";
2212
2495
  import archiver from "archiver";
2213
2496
  import {
2214
2497
  createDefaultRegistry as createDefaultRegistry3,
2215
- serializeGenart as serializeGenart3
2498
+ serializeGenart as serializeGenart4
2216
2499
  } from "@genart-dev/core";
2217
2500
  var registry3 = createDefaultRegistry3();
2218
2501
  async function validateOutputPath(outputPath) {
@@ -2282,7 +2565,7 @@ async function exportHtml(sketch, outputPath) {
2282
2565
  const adapter = registry3.resolve(sketch.renderer.type);
2283
2566
  const html = adapter.generateStandaloneHTML(sketch);
2284
2567
  const content = Buffer.from(html, "utf-8");
2285
- await writeFile5(outputPath, content);
2568
+ await writeFile6(outputPath, content);
2286
2569
  return {
2287
2570
  success: true,
2288
2571
  sketchId: sketch.id,
@@ -2298,7 +2581,7 @@ async function exportPng(sketch, input) {
2298
2581
  const width = input.width ?? sketch.canvas.width;
2299
2582
  const height = input.height ?? sketch.canvas.height;
2300
2583
  const result = await captureHtml({ html, width, height });
2301
- await writeFile5(input.outputPath, result.bytes);
2584
+ await writeFile6(input.outputPath, result.bytes);
2302
2585
  return {
2303
2586
  success: true,
2304
2587
  sketchId: sketch.id,
@@ -2313,7 +2596,7 @@ async function exportSvg(sketch, input) {
2313
2596
  const height = input.height ?? sketch.canvas.height;
2314
2597
  if (sketch.renderer.type === "svg") {
2315
2598
  const content2 = Buffer.from(sketch.algorithm, "utf-8");
2316
- await writeFile5(input.outputPath, content2);
2599
+ await writeFile6(input.outputPath, content2);
2317
2600
  return {
2318
2601
  success: true,
2319
2602
  sketchId: sketch.id,
@@ -2335,7 +2618,7 @@ async function exportSvg(sketch, input) {
2335
2618
  href="data:image/png;base64,${b64}"/>
2336
2619
  </svg>`;
2337
2620
  const content = Buffer.from(svg, "utf-8");
2338
- await writeFile5(input.outputPath, content);
2621
+ await writeFile6(input.outputPath, content);
2339
2622
  return {
2340
2623
  success: true,
2341
2624
  sketchId: sketch.id,
@@ -2348,7 +2631,7 @@ async function exportSvg(sketch, input) {
2348
2631
  }
2349
2632
  async function exportAlgorithm(sketch, outputPath) {
2350
2633
  const content = Buffer.from(sketch.algorithm, "utf-8");
2351
- await writeFile5(outputPath, content);
2634
+ await writeFile6(outputPath, content);
2352
2635
  return {
2353
2636
  success: true,
2354
2637
  sketchId: sketch.id,
@@ -2363,7 +2646,7 @@ async function exportZip(sketch, input) {
2363
2646
  const width = input.width ?? sketch.canvas.width;
2364
2647
  const height = input.height ?? sketch.canvas.height;
2365
2648
  const html = adapter.generateStandaloneHTML(sketch);
2366
- const genartJson = serializeGenart3(sketch);
2649
+ const genartJson = serializeGenart4(sketch);
2367
2650
  const algorithm = sketch.algorithm;
2368
2651
  const algExt = algorithmExtension(sketch.renderer.type);
2369
2652
  const captureResult = await captureHtml({ html, width, height });
@@ -2397,6 +2680,330 @@ async function exportZip(sketch, input) {
2397
2680
  };
2398
2681
  }
2399
2682
 
2683
+ // src/tools/design.ts
2684
+ function requireSketchId(state, args) {
2685
+ return args.sketchId ?? state.requireSelectedSketchId();
2686
+ }
2687
+ var BLEND_MODES = [
2688
+ "normal",
2689
+ "multiply",
2690
+ "screen",
2691
+ "overlay",
2692
+ "darken",
2693
+ "lighten",
2694
+ "color-dodge",
2695
+ "color-burn",
2696
+ "hard-light",
2697
+ "soft-light",
2698
+ "difference",
2699
+ "exclusion",
2700
+ "hue",
2701
+ "saturation",
2702
+ "color",
2703
+ "luminosity"
2704
+ ];
2705
+ function generateLayerId() {
2706
+ return `layer-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
2707
+ }
2708
+ async function designAddLayer(state, args) {
2709
+ const sketchId = requireSketchId(state, args);
2710
+ const loaded = state.requireSketch(sketchId);
2711
+ const stack = state.getLayerStack(sketchId);
2712
+ const registry4 = state.pluginRegistry;
2713
+ const layerTypeDef = registry4?.resolveLayerType(args.type);
2714
+ if (!layerTypeDef) {
2715
+ throw new Error(
2716
+ `Unknown layer type: '${args.type}'. Use design_list_layers types from registered plugins.`
2717
+ );
2718
+ }
2719
+ const defaults = layerTypeDef.createDefault();
2720
+ const id = generateLayerId();
2721
+ const { width, height } = loaded.definition.canvas;
2722
+ const layer = {
2723
+ id,
2724
+ type: args.type,
2725
+ name: args.name ?? layerTypeDef.displayName,
2726
+ visible: true,
2727
+ locked: false,
2728
+ opacity: args.opacity ?? 1,
2729
+ blendMode: args.blendMode ?? "normal",
2730
+ transform: {
2731
+ x: 0,
2732
+ y: 0,
2733
+ width,
2734
+ height,
2735
+ rotation: 0,
2736
+ scaleX: 1,
2737
+ scaleY: 1,
2738
+ anchorX: 0.5,
2739
+ anchorY: 0.5,
2740
+ ...args.transform
2741
+ },
2742
+ properties: { ...defaults, ...args.properties }
2743
+ };
2744
+ stack.add(layer, args.index);
2745
+ await state.saveSketch(sketchId);
2746
+ return {
2747
+ layerId: id,
2748
+ type: args.type,
2749
+ name: layer.name,
2750
+ index: args.index ?? stack.count - 1,
2751
+ sketchId
2752
+ };
2753
+ }
2754
+ async function designRemoveLayer(state, args) {
2755
+ const sketchId = requireSketchId(state, args);
2756
+ const stack = state.getLayerStack(sketchId);
2757
+ const removed = stack.remove(args.layerId);
2758
+ if (!removed) {
2759
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
2760
+ }
2761
+ await state.saveSketch(sketchId);
2762
+ return { removed: true, layerId: args.layerId, sketchId };
2763
+ }
2764
+ async function designListLayers(state, args) {
2765
+ const sketchId = requireSketchId(state, args);
2766
+ const stack = state.getLayerStack(sketchId);
2767
+ const layers = stack.getAll();
2768
+ return {
2769
+ sketchId,
2770
+ count: layers.length,
2771
+ layers: layers.map((l, i) => ({
2772
+ index: i,
2773
+ id: l.id,
2774
+ type: l.type,
2775
+ name: l.name,
2776
+ visible: l.visible,
2777
+ locked: l.locked,
2778
+ opacity: l.opacity,
2779
+ blendMode: l.blendMode
2780
+ }))
2781
+ };
2782
+ }
2783
+ async function designGetLayer(state, args) {
2784
+ const sketchId = requireSketchId(state, args);
2785
+ const stack = state.getLayerStack(sketchId);
2786
+ const layer = stack.get(args.layerId);
2787
+ if (!layer) {
2788
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
2789
+ }
2790
+ return {
2791
+ sketchId,
2792
+ layer: {
2793
+ id: layer.id,
2794
+ type: layer.type,
2795
+ name: layer.name,
2796
+ visible: layer.visible,
2797
+ locked: layer.locked,
2798
+ opacity: layer.opacity,
2799
+ blendMode: layer.blendMode,
2800
+ transform: layer.transform,
2801
+ properties: layer.properties
2802
+ }
2803
+ };
2804
+ }
2805
+ async function designUpdateLayer(state, args) {
2806
+ const sketchId = requireSketchId(state, args);
2807
+ const stack = state.getLayerStack(sketchId);
2808
+ const layer = stack.get(args.layerId);
2809
+ if (!layer) {
2810
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
2811
+ }
2812
+ const updates = {};
2813
+ if (args.properties) {
2814
+ Object.assign(updates, args.properties);
2815
+ }
2816
+ if (Object.keys(updates).length > 0) {
2817
+ stack.updateProperties(args.layerId, updates);
2818
+ }
2819
+ if (args.name !== void 0) {
2820
+ const current = stack.get(args.layerId);
2821
+ stack.updateProperties(args.layerId, { ...current.properties });
2822
+ const mutableLayer = stack.get(args.layerId);
2823
+ mutableLayer.name = args.name;
2824
+ }
2825
+ await state.saveSketch(sketchId);
2826
+ return { updated: true, layerId: args.layerId, sketchId };
2827
+ }
2828
+ async function designSetTransform(state, args) {
2829
+ const sketchId = requireSketchId(state, args);
2830
+ const stack = state.getLayerStack(sketchId);
2831
+ const layer = stack.get(args.layerId);
2832
+ if (!layer) {
2833
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
2834
+ }
2835
+ const partial = {};
2836
+ if (args.x !== void 0) partial.x = args.x;
2837
+ if (args.y !== void 0) partial.y = args.y;
2838
+ if (args.width !== void 0) partial.width = args.width;
2839
+ if (args.height !== void 0) partial.height = args.height;
2840
+ if (args.rotation !== void 0) partial.rotation = args.rotation;
2841
+ if (args.scaleX !== void 0) partial.scaleX = args.scaleX;
2842
+ if (args.scaleY !== void 0) partial.scaleY = args.scaleY;
2843
+ if (args.anchorX !== void 0) partial.anchorX = args.anchorX;
2844
+ if (args.anchorY !== void 0) partial.anchorY = args.anchorY;
2845
+ stack.updateTransform(args.layerId, partial);
2846
+ await state.saveSketch(sketchId);
2847
+ return {
2848
+ updated: true,
2849
+ layerId: args.layerId,
2850
+ transform: stack.get(args.layerId).transform,
2851
+ sketchId
2852
+ };
2853
+ }
2854
+ async function designSetBlend(state, args) {
2855
+ const sketchId = requireSketchId(state, args);
2856
+ const stack = state.getLayerStack(sketchId);
2857
+ const layer = stack.get(args.layerId);
2858
+ if (!layer) {
2859
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
2860
+ }
2861
+ if (args.blendMode && !BLEND_MODES.includes(args.blendMode)) {
2862
+ throw new Error(
2863
+ `Invalid blend mode '${args.blendMode}'. Must be one of: ${BLEND_MODES.join(", ")}`
2864
+ );
2865
+ }
2866
+ stack.updateBlend(
2867
+ args.layerId,
2868
+ args.blendMode,
2869
+ args.opacity
2870
+ );
2871
+ await state.saveSketch(sketchId);
2872
+ const updated = stack.get(args.layerId);
2873
+ return {
2874
+ updated: true,
2875
+ layerId: args.layerId,
2876
+ blendMode: updated.blendMode,
2877
+ opacity: updated.opacity,
2878
+ sketchId
2879
+ };
2880
+ }
2881
+ async function designReorderLayers(state, args) {
2882
+ const sketchId = requireSketchId(state, args);
2883
+ const stack = state.getLayerStack(sketchId);
2884
+ const layer = stack.get(args.layerId);
2885
+ if (!layer) {
2886
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
2887
+ }
2888
+ stack.reorder(args.layerId, args.newIndex);
2889
+ await state.saveSketch(sketchId);
2890
+ return {
2891
+ reordered: true,
2892
+ layerId: args.layerId,
2893
+ newIndex: args.newIndex,
2894
+ sketchId
2895
+ };
2896
+ }
2897
+ async function designDuplicateLayer(state, args) {
2898
+ const sketchId = requireSketchId(state, args);
2899
+ const stack = state.getLayerStack(sketchId);
2900
+ const layer = stack.get(args.layerId);
2901
+ if (!layer) {
2902
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
2903
+ }
2904
+ const newId = stack.duplicate(args.layerId);
2905
+ await state.saveSketch(sketchId);
2906
+ return {
2907
+ duplicated: true,
2908
+ sourceLayerId: args.layerId,
2909
+ newLayerId: newId,
2910
+ sketchId
2911
+ };
2912
+ }
2913
+ async function designToggleVisibility(state, args) {
2914
+ const sketchId = requireSketchId(state, args);
2915
+ const stack = state.getLayerStack(sketchId);
2916
+ const layer = stack.get(args.layerId);
2917
+ if (!layer) {
2918
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
2919
+ }
2920
+ const newVisible = args.visible ?? !layer.visible;
2921
+ const mutableLayer = layer;
2922
+ mutableLayer.visible = newVisible;
2923
+ stack.updateProperties(args.layerId, { ...layer.properties });
2924
+ await state.saveSketch(sketchId);
2925
+ return {
2926
+ layerId: args.layerId,
2927
+ visible: newVisible,
2928
+ sketchId
2929
+ };
2930
+ }
2931
+ async function designLockLayer(state, args) {
2932
+ const sketchId = requireSketchId(state, args);
2933
+ const stack = state.getLayerStack(sketchId);
2934
+ const layer = stack.get(args.layerId);
2935
+ if (!layer) {
2936
+ throw new Error(`Layer '${args.layerId}' not found in sketch '${sketchId}'.`);
2937
+ }
2938
+ const newLocked = args.locked ?? !layer.locked;
2939
+ const mutableLayer = layer;
2940
+ mutableLayer.locked = newLocked;
2941
+ stack.updateProperties(args.layerId, { ...layer.properties });
2942
+ await state.saveSketch(sketchId);
2943
+ return {
2944
+ layerId: args.layerId,
2945
+ locked: newLocked,
2946
+ sketchId
2947
+ };
2948
+ }
2949
+ async function designCaptureComposite(state, args) {
2950
+ const sketchId = requireSketchId(state, args);
2951
+ const stack = state.getLayerStack(sketchId);
2952
+ const layers = stack.getAll();
2953
+ return {
2954
+ sketchId,
2955
+ layerCount: layers.length,
2956
+ visibleCount: layers.filter((l) => l.visible).length,
2957
+ 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."
2958
+ };
2959
+ }
2960
+
2961
+ // src/tools/design-plugins.ts
2962
+ function registerPluginMcpTools(server, registry4, state) {
2963
+ for (const tool of registry4.getMcpTools()) {
2964
+ const inputSchema = tool.definition.inputSchema;
2965
+ server.tool(
2966
+ tool.name,
2967
+ tool.definition.description,
2968
+ // Pass raw JSON schema — MCP SDK accepts this alongside Zod
2969
+ inputSchema,
2970
+ async (args) => {
2971
+ try {
2972
+ const sketchId = args.sketchId ?? state.requireSelectedSketchId();
2973
+ const context = state.createMcpToolContext(sketchId);
2974
+ const result = await tool.definition.handler(args, context);
2975
+ await state.saveSketch(sketchId);
2976
+ return {
2977
+ content: result.content.map((c) => {
2978
+ if (c.type === "text") {
2979
+ return { type: "text", text: c.text };
2980
+ }
2981
+ return {
2982
+ type: "image",
2983
+ data: c.data,
2984
+ mimeType: c.mimeType
2985
+ };
2986
+ }),
2987
+ isError: result.isError
2988
+ };
2989
+ } catch (e) {
2990
+ return {
2991
+ content: [
2992
+ {
2993
+ type: "text",
2994
+ text: JSON.stringify({
2995
+ error: e instanceof Error ? e.message : String(e)
2996
+ })
2997
+ }
2998
+ ],
2999
+ isError: true
3000
+ };
3001
+ }
3002
+ }
3003
+ );
3004
+ }
3005
+ }
3006
+
2400
3007
  // src/resources/index.ts
2401
3008
  import {
2402
3009
  CANVAS_PRESETS,
@@ -2884,11 +3491,23 @@ function toolError(message) {
2884
3491
  isError: true
2885
3492
  };
2886
3493
  }
3494
+ async function initializePluginRegistry() {
3495
+ const registry4 = createPluginRegistry({
3496
+ surface: "mcp",
3497
+ supportsInteractiveTools: false,
3498
+ supportsRendering: false
3499
+ });
3500
+ await registry4.register(typographyPlugin);
3501
+ await registry4.register(filtersPlugin);
3502
+ await registry4.register(shapesPlugin);
3503
+ await registry4.register(layoutGuidesPlugin);
3504
+ return registry4;
3505
+ }
2887
3506
  function createServer(state) {
2888
3507
  const server = new McpServer(
2889
3508
  {
2890
3509
  name: "@genart/mcp-server",
2891
- version: "0.0.1"
3510
+ version: "0.3.0"
2892
3511
  },
2893
3512
  {
2894
3513
  capabilities: {
@@ -2898,8 +3517,14 @@ function createServer(state) {
2898
3517
  }
2899
3518
  }
2900
3519
  );
3520
+ const registryReady = initializePluginRegistry().then((registry4) => {
3521
+ state.pluginRegistry = registry4;
3522
+ registerPluginMcpTools(server, registry4, state);
3523
+ });
3524
+ server._pluginsReady = registryReady;
2901
3525
  registerWorkspaceTools(server, state);
2902
3526
  registerSketchTools(server, state);
3527
+ registerComponentTools(server, state);
2903
3528
  registerSelectionTools(server, state);
2904
3529
  registerParameterTools(server, state);
2905
3530
  registerArrangementTools(server, state);
@@ -2907,6 +3532,7 @@ function createServer(state) {
2907
3532
  registerMergeTools(server, state);
2908
3533
  registerSnapshotTools(server, state);
2909
3534
  registerKnowledgeTools(server, state);
3535
+ registerDesignTools(server, state);
2910
3536
  registerCaptureTools(server, state);
2911
3537
  registerExportTools(server, state);
2912
3538
  registerResources(server, state);
@@ -3003,7 +3629,7 @@ function registerWorkspaceTools(server, state) {
3003
3629
  function registerSketchTools(server, state) {
3004
3630
  server.tool(
3005
3631
  "create_sketch",
3006
- "Create a new .genart sketch file from metadata, parameters, and algorithm",
3632
+ '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.',
3007
3633
  {
3008
3634
  id: z2.string().describe("URL-safe kebab-case identifier"),
3009
3635
  title: z2.string().describe("Human-readable title"),
@@ -3041,6 +3667,16 @@ function registerSketchTools(server, state) {
3041
3667
  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)."),
3042
3668
  seed: z2.number().optional().describe("Initial random seed (default: random)"),
3043
3669
  skills: z2.array(z2.string()).optional().describe("Design skill references"),
3670
+ components: z2.record(
3671
+ z2.union([
3672
+ z2.string(),
3673
+ z2.object({
3674
+ version: z2.string().optional(),
3675
+ code: z2.string().optional(),
3676
+ exports: z2.array(z2.string()).optional()
3677
+ })
3678
+ ])
3679
+ ).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.'),
3044
3680
  addToWorkspace: z2.string().optional().describe("Path to workspace to add sketch to after creation"),
3045
3681
  agent: z2.string().optional().describe("Your CLI agent name (e.g. 'claude-code', 'codex-cli', 'gemini-cli', 'opencode', 'kiro')"),
3046
3682
  model: z2.string().optional().describe("Your AI model identifier (e.g. 'claude-opus-4-6', 'gpt-4o', 'gemini-2.5-pro')")
@@ -3120,11 +3756,21 @@ function registerSketchTools(server, state) {
3120
3756
  );
3121
3757
  server.tool(
3122
3758
  "update_algorithm",
3123
- "Replace the algorithm source code of a sketch",
3759
+ "Replace the algorithm source code of a sketch. If adding/changing components, pass them in the components field alongside the algorithm.",
3124
3760
  {
3125
3761
  sketchId: z2.string().describe("ID of the sketch to update"),
3126
3762
  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)."),
3127
3763
  validate: z2.boolean().optional().describe("Run renderer-specific validation before saving (default: true)"),
3764
+ components: z2.record(
3765
+ z2.union([
3766
+ z2.string(),
3767
+ z2.object({
3768
+ version: z2.string().optional(),
3769
+ code: z2.string().optional(),
3770
+ exports: z2.array(z2.string()).optional()
3771
+ })
3772
+ ])
3773
+ ).optional().describe("Component dependencies to resolve alongside the algorithm update. Use list_components to see available."),
3128
3774
  agent: z2.string().optional().describe("Your CLI agent name (e.g. 'claude-code', 'codex-cli', 'gemini-cli', 'opencode', 'kiro')"),
3129
3775
  model: z2.string().optional().describe("Your AI model identifier (e.g. 'claude-opus-4-6', 'gpt-4o', 'gemini-2.5-pro')")
3130
3776
  },
@@ -3220,6 +3866,76 @@ function registerSketchTools(server, state) {
3220
3866
  }
3221
3867
  );
3222
3868
  }
3869
+ function registerComponentTools(server, state) {
3870
+ server.tool(
3871
+ "list_components",
3872
+ "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.",
3873
+ {
3874
+ renderer: z2.enum(["p5", "three", "glsl", "canvas2d", "svg"]).optional().describe("Filter by renderer compatibility"),
3875
+ category: z2.enum([
3876
+ "randomness",
3877
+ "noise",
3878
+ "math",
3879
+ "easing",
3880
+ "color",
3881
+ "vector",
3882
+ "geometry",
3883
+ "grid",
3884
+ "particle",
3885
+ "physics",
3886
+ "distribution",
3887
+ "pattern",
3888
+ "sdf",
3889
+ "transform",
3890
+ "animation",
3891
+ "string",
3892
+ "data-structure",
3893
+ "imaging"
3894
+ ]).optional().describe("Filter by component category")
3895
+ },
3896
+ async (args) => {
3897
+ try {
3898
+ const result = await listComponents(state, args);
3899
+ return jsonResult(result);
3900
+ } catch (e) {
3901
+ return toolError(e instanceof Error ? e.message : String(e));
3902
+ }
3903
+ }
3904
+ );
3905
+ server.tool(
3906
+ "add_component",
3907
+ "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.",
3908
+ {
3909
+ sketchId: z2.string().describe("ID of the sketch to add the component to"),
3910
+ component: z2.string().describe("Component name (e.g. 'prng', 'noise-2d', 'glsl-noise')"),
3911
+ version: z2.string().optional().describe("Version range (default: '^1.0.0')")
3912
+ },
3913
+ async (args) => {
3914
+ try {
3915
+ const result = await addComponent(state, args);
3916
+ return jsonResult(result);
3917
+ } catch (e) {
3918
+ return toolError(e instanceof Error ? e.message : String(e));
3919
+ }
3920
+ }
3921
+ );
3922
+ server.tool(
3923
+ "remove_component",
3924
+ "Remove a component dependency from a sketch. Checks for dependent components and warns if the algorithm references the component's exports.",
3925
+ {
3926
+ sketchId: z2.string().describe("ID of the sketch to remove the component from"),
3927
+ component: z2.string().describe("Component name to remove")
3928
+ },
3929
+ async (args) => {
3930
+ try {
3931
+ const result = await removeComponent(state, args);
3932
+ return jsonResult(result);
3933
+ } catch (e) {
3934
+ return toolError(e instanceof Error ? e.message : String(e));
3935
+ }
3936
+ }
3937
+ );
3938
+ }
3223
3939
  function registerSelectionTools(server, state) {
3224
3940
  server.tool(
3225
3941
  "get_selection",
@@ -3605,6 +4321,247 @@ function registerExportTools(server, state) {
3605
4321
  }
3606
4322
  );
3607
4323
  }
4324
+ function registerDesignTools(server, state) {
4325
+ server.tool(
4326
+ "design_add_layer",
4327
+ "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').",
4328
+ {
4329
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4330
+ type: z2.string().describe("Layer type ID (e.g. 'typography:text', 'filter:grain', 'shapes:rect')"),
4331
+ name: z2.string().optional().describe("Layer display name (default: type's display name)"),
4332
+ properties: z2.record(z2.unknown()).optional().describe("Initial layer properties (merged with type defaults)"),
4333
+ transform: z2.object({
4334
+ x: z2.number().optional(),
4335
+ y: z2.number().optional(),
4336
+ width: z2.number().optional(),
4337
+ height: z2.number().optional(),
4338
+ rotation: z2.number().optional(),
4339
+ scaleX: z2.number().optional(),
4340
+ scaleY: z2.number().optional(),
4341
+ anchorX: z2.number().optional(),
4342
+ anchorY: z2.number().optional()
4343
+ }).optional().describe("Layer transform (default: full canvas)"),
4344
+ opacity: z2.number().optional().describe("Layer opacity 0\u20131 (default: 1)"),
4345
+ blendMode: z2.string().optional().describe("Blend mode (default: 'normal')"),
4346
+ index: z2.number().optional().describe("Insert position in layer stack (default: top)")
4347
+ },
4348
+ async (args) => {
4349
+ try {
4350
+ const result = await designAddLayer(state, args);
4351
+ return jsonResult(result);
4352
+ } catch (e) {
4353
+ return toolError(e instanceof Error ? e.message : String(e));
4354
+ }
4355
+ }
4356
+ );
4357
+ server.tool(
4358
+ "design_remove_layer",
4359
+ "Remove a design layer from the active sketch",
4360
+ {
4361
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4362
+ layerId: z2.string().describe("ID of the layer to remove")
4363
+ },
4364
+ async (args) => {
4365
+ try {
4366
+ const result = await designRemoveLayer(state, args);
4367
+ return jsonResult(result);
4368
+ } catch (e) {
4369
+ return toolError(e instanceof Error ? e.message : String(e));
4370
+ }
4371
+ }
4372
+ );
4373
+ server.tool(
4374
+ "design_list_layers",
4375
+ "List all design layers in the active sketch with their types, visibility, and key properties",
4376
+ {
4377
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)")
4378
+ },
4379
+ async (args) => {
4380
+ try {
4381
+ const result = await designListLayers(state, args);
4382
+ return jsonResult(result);
4383
+ } catch (e) {
4384
+ return toolError(e instanceof Error ? e.message : String(e));
4385
+ }
4386
+ }
4387
+ );
4388
+ server.tool(
4389
+ "design_get_layer",
4390
+ "Get full details of a single design layer including all properties and transform",
4391
+ {
4392
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4393
+ layerId: z2.string().describe("ID of the layer to inspect")
4394
+ },
4395
+ async (args) => {
4396
+ try {
4397
+ const result = await designGetLayer(state, args);
4398
+ return jsonResult(result);
4399
+ } catch (e) {
4400
+ return toolError(e instanceof Error ? e.message : String(e));
4401
+ }
4402
+ }
4403
+ );
4404
+ server.tool(
4405
+ "design_update_layer",
4406
+ "Update properties on a design layer (e.g. text content, filter intensity, shape fill color)",
4407
+ {
4408
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4409
+ layerId: z2.string().describe("ID of the layer to update"),
4410
+ name: z2.string().optional().describe("New display name"),
4411
+ properties: z2.record(z2.unknown()).optional().describe("Property key-value pairs to set")
4412
+ },
4413
+ async (args) => {
4414
+ try {
4415
+ const result = await designUpdateLayer(state, args);
4416
+ return jsonResult(result);
4417
+ } catch (e) {
4418
+ return toolError(e instanceof Error ? e.message : String(e));
4419
+ }
4420
+ }
4421
+ );
4422
+ server.tool(
4423
+ "design_set_transform",
4424
+ "Set the position, size, rotation, and scale of a design layer",
4425
+ {
4426
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4427
+ layerId: z2.string().describe("ID of the layer to transform"),
4428
+ x: z2.number().optional().describe("X position"),
4429
+ y: z2.number().optional().describe("Y position"),
4430
+ width: z2.number().optional().describe("Width"),
4431
+ height: z2.number().optional().describe("Height"),
4432
+ rotation: z2.number().optional().describe("Rotation in degrees"),
4433
+ scaleX: z2.number().optional().describe("Horizontal scale"),
4434
+ scaleY: z2.number().optional().describe("Vertical scale"),
4435
+ anchorX: z2.number().optional().describe("Anchor X (0\u20131)"),
4436
+ anchorY: z2.number().optional().describe("Anchor Y (0\u20131)")
4437
+ },
4438
+ async (args) => {
4439
+ try {
4440
+ const result = await designSetTransform(state, args);
4441
+ return jsonResult(result);
4442
+ } catch (e) {
4443
+ return toolError(e instanceof Error ? e.message : String(e));
4444
+ }
4445
+ }
4446
+ );
4447
+ server.tool(
4448
+ "design_set_blend",
4449
+ "Set blend mode and/or opacity on a design layer",
4450
+ {
4451
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4452
+ layerId: z2.string().describe("ID of the layer"),
4453
+ blendMode: z2.enum([
4454
+ "normal",
4455
+ "multiply",
4456
+ "screen",
4457
+ "overlay",
4458
+ "darken",
4459
+ "lighten",
4460
+ "color-dodge",
4461
+ "color-burn",
4462
+ "hard-light",
4463
+ "soft-light",
4464
+ "difference",
4465
+ "exclusion",
4466
+ "hue",
4467
+ "saturation",
4468
+ "color",
4469
+ "luminosity"
4470
+ ]).optional().describe("CSS blend mode"),
4471
+ opacity: z2.number().optional().describe("Layer opacity 0\u20131")
4472
+ },
4473
+ async (args) => {
4474
+ try {
4475
+ const result = await designSetBlend(state, args);
4476
+ return jsonResult(result);
4477
+ } catch (e) {
4478
+ return toolError(e instanceof Error ? e.message : String(e));
4479
+ }
4480
+ }
4481
+ );
4482
+ server.tool(
4483
+ "design_reorder_layers",
4484
+ "Move a design layer to a new position in the z-order stack",
4485
+ {
4486
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4487
+ layerId: z2.string().describe("ID of the layer to move"),
4488
+ newIndex: z2.number().describe("New position (0 = bottom, n-1 = top)")
4489
+ },
4490
+ async (args) => {
4491
+ try {
4492
+ const result = await designReorderLayers(state, args);
4493
+ return jsonResult(result);
4494
+ } catch (e) {
4495
+ return toolError(e instanceof Error ? e.message : String(e));
4496
+ }
4497
+ }
4498
+ );
4499
+ server.tool(
4500
+ "design_duplicate_layer",
4501
+ "Clone a design layer with a new ID, inserted directly above the source",
4502
+ {
4503
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4504
+ layerId: z2.string().describe("ID of the layer to duplicate")
4505
+ },
4506
+ async (args) => {
4507
+ try {
4508
+ const result = await designDuplicateLayer(state, args);
4509
+ return jsonResult(result);
4510
+ } catch (e) {
4511
+ return toolError(e instanceof Error ? e.message : String(e));
4512
+ }
4513
+ }
4514
+ );
4515
+ server.tool(
4516
+ "design_toggle_visibility",
4517
+ "Show or hide a design layer",
4518
+ {
4519
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4520
+ layerId: z2.string().describe("ID of the layer"),
4521
+ visible: z2.boolean().optional().describe("Set visibility (default: toggle)")
4522
+ },
4523
+ async (args) => {
4524
+ try {
4525
+ const result = await designToggleVisibility(state, args);
4526
+ return jsonResult(result);
4527
+ } catch (e) {
4528
+ return toolError(e instanceof Error ? e.message : String(e));
4529
+ }
4530
+ }
4531
+ );
4532
+ server.tool(
4533
+ "design_lock_layer",
4534
+ "Lock or unlock a design layer to prevent accidental edits",
4535
+ {
4536
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)"),
4537
+ layerId: z2.string().describe("ID of the layer"),
4538
+ locked: z2.boolean().optional().describe("Set lock state (default: toggle)")
4539
+ },
4540
+ async (args) => {
4541
+ try {
4542
+ const result = await designLockLayer(state, args);
4543
+ return jsonResult(result);
4544
+ } catch (e) {
4545
+ return toolError(e instanceof Error ? e.message : String(e));
4546
+ }
4547
+ }
4548
+ );
4549
+ server.tool(
4550
+ "design_capture_composite",
4551
+ "Get info about the design layer composite for a sketch. For full visual capture use capture_screenshot.",
4552
+ {
4553
+ sketchId: z2.string().optional().describe("Target sketch ID (default: selected sketch)")
4554
+ },
4555
+ async (args) => {
4556
+ try {
4557
+ const result = await designCaptureComposite(state, args);
4558
+ return jsonResult(result);
4559
+ } catch (e) {
4560
+ return toolError(e instanceof Error ? e.message : String(e));
4561
+ }
4562
+ }
4563
+ );
4564
+ }
3608
4565
  function registerKnowledgeTools(server, _state) {
3609
4566
  server.tool(
3610
4567
  "list_skills",
@@ -3676,10 +4633,11 @@ function notifyMutation(type, payload) {
3676
4633
  import {
3677
4634
  parseGenart as parseGenart4,
3678
4635
  parseWorkspace as parseWorkspace2,
3679
- serializeGenart as serializeGenart4,
3680
- serializeWorkspace as serializeWorkspace3
4636
+ serializeGenart as serializeGenart5,
4637
+ serializeWorkspace as serializeWorkspace3,
4638
+ createLayerStack
3681
4639
  } from "@genart-dev/core";
3682
- import { writeFile as writeFile6 } from "fs/promises";
4640
+ import { writeFile as writeFile7 } from "fs/promises";
3683
4641
  var EditorState = class extends EventEmitter {
3684
4642
  /** Absolute path to the active .genart-workspace file, or null. */
3685
4643
  workspacePath = null;
@@ -3701,6 +4659,10 @@ var EditorState = class extends EventEmitter {
3701
4659
  * instead of writing to disk. Set by mcp-host for HTTP-based sessions.
3702
4660
  */
3703
4661
  remoteMode = false;
4662
+ /** Plugin registry for design mode. Set during server initialization. */
4663
+ pluginRegistry = null;
4664
+ /** Layer stacks keyed by sketch ID. Created lazily when design tools are used. */
4665
+ layerStacks = /* @__PURE__ */ new Map();
3704
4666
  constructor(options) {
3705
4667
  super();
3706
4668
  if (options?.basePath) {
@@ -3765,6 +4727,7 @@ var EditorState = class extends EventEmitter {
3765
4727
  this.workspace = ws;
3766
4728
  this.sketches.clear();
3767
4729
  this.selection.clear();
4730
+ this.layerStacks.clear();
3768
4731
  for (const ref of ws.sketches) {
3769
4732
  const sketchPath = this.resolveSketchPath(ref.file);
3770
4733
  await this.loadSketch(sketchPath);
@@ -3806,6 +4769,7 @@ var EditorState = class extends EventEmitter {
3806
4769
  removeSketch(id) {
3807
4770
  this.sketches.delete(id);
3808
4771
  this.selection.delete(id);
4772
+ this.layerStacks.delete(id);
3809
4773
  this.emitMutation("sketch:removed", { id });
3810
4774
  }
3811
4775
  /** Save the active workspace to disk. */
@@ -3815,16 +4779,16 @@ var EditorState = class extends EventEmitter {
3815
4779
  }
3816
4780
  const json = serializeWorkspace3(this.workspace);
3817
4781
  if (!this.remoteMode) {
3818
- await writeFile6(this.workspacePath, json, "utf-8");
4782
+ await writeFile7(this.workspacePath, json, "utf-8");
3819
4783
  }
3820
4784
  this.emitMutation("workspace:saved", { path: this.workspacePath });
3821
4785
  }
3822
4786
  /** Save a sketch to disk. */
3823
4787
  async saveSketch(id) {
3824
4788
  const loaded = this.requireSketch(id);
3825
- const json = serializeGenart4(loaded.definition);
4789
+ const json = serializeGenart5(loaded.definition);
3826
4790
  if (!this.remoteMode) {
3827
- await writeFile6(loaded.path, json, "utf-8");
4791
+ await writeFile7(loaded.path, json, "utf-8");
3828
4792
  }
3829
4793
  this.emitMutation("sketch:saved", { id, path: loaded.path });
3830
4794
  }
@@ -3849,6 +4813,81 @@ var EditorState = class extends EventEmitter {
3849
4813
  selection: Array.from(this.selection)
3850
4814
  };
3851
4815
  }
4816
+ /**
4817
+ * Get or create a LayerStackAccessor for a sketch.
4818
+ * Initializes from the sketch's persisted design layers.
4819
+ */
4820
+ getLayerStack(sketchId) {
4821
+ let stack = this.layerStacks.get(sketchId);
4822
+ if (stack) return stack;
4823
+ const loaded = this.requireSketch(sketchId);
4824
+ const initialLayers = loaded.definition.layers ?? [];
4825
+ stack = createLayerStack(initialLayers, (changeType) => {
4826
+ this.syncLayersToDefinition(sketchId);
4827
+ const mutationType = `design:${changeType}`;
4828
+ this.emitMutation(mutationType, { sketchId, changeType });
4829
+ });
4830
+ this.layerStacks.set(sketchId, stack);
4831
+ return stack;
4832
+ }
4833
+ /**
4834
+ * Sync the layer stack's current state back to the sketch definition.
4835
+ * Called automatically on every layer mutation.
4836
+ */
4837
+ syncLayersToDefinition(sketchId) {
4838
+ const loaded = this.sketches.get(sketchId);
4839
+ const stack = this.layerStacks.get(sketchId);
4840
+ if (!loaded || !stack) return;
4841
+ const layers = stack.getAll();
4842
+ loaded.definition = {
4843
+ ...loaded.definition,
4844
+ layers: layers.length > 0 ? layers : void 0
4845
+ };
4846
+ }
4847
+ /**
4848
+ * Create an McpToolContext for a plugin's MCP tool handler.
4849
+ * Provides access to the layer stack, sketch state, and change notifications.
4850
+ */
4851
+ createMcpToolContext(sketchId) {
4852
+ const loaded = this.requireSketch(sketchId);
4853
+ const layerStack = this.getLayerStack(sketchId);
4854
+ const def = loaded.definition;
4855
+ const sketchState = {
4856
+ seed: def.state.seed,
4857
+ params: def.state.params,
4858
+ colorPalette: def.state.colorPalette,
4859
+ canvasWidth: def.canvas.width,
4860
+ canvasHeight: def.canvas.height,
4861
+ rendererId: def.renderer.type
4862
+ };
4863
+ return {
4864
+ layers: layerStack,
4865
+ sketchState,
4866
+ canvasWidth: def.canvas.width,
4867
+ canvasHeight: def.canvas.height,
4868
+ async resolveAsset(_assetId) {
4869
+ return null;
4870
+ },
4871
+ async captureComposite(_format) {
4872
+ throw new Error("captureComposite is not available in headless MCP mode");
4873
+ },
4874
+ emitChange(_changeType) {
4875
+ }
4876
+ };
4877
+ }
4878
+ /**
4879
+ * Get the currently selected sketch ID for design operations.
4880
+ * Returns the single selected sketch, or throws if none/multiple selected.
4881
+ */
4882
+ requireSelectedSketchId() {
4883
+ if (this.selection.size === 0) {
4884
+ throw new Error("No sketch is selected. Use select_sketch or open_sketch first.");
4885
+ }
4886
+ if (this.selection.size > 1) {
4887
+ throw new Error("Multiple sketches are selected. Design operations require a single sketch.");
4888
+ }
4889
+ return this.selection.values().next().value;
4890
+ }
3852
4891
  /** Emit a mutation event for external listeners (WebSocket broadcast, sidecar IPC). */
3853
4892
  emitMutation(type, payload) {
3854
4893
  this.emit("mutation", { type, payload });