@efectoapp/mcp-server 0.1.22 → 0.1.24

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.
@@ -14,13 +14,9 @@
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.stateTools = void 0;
17
- exports.ensurePoster = ensurePoster;
18
17
  exports.resetState = resetState;
19
18
  exports.getCurrentState = getCurrentState;
20
19
  exports.handleStateTool = handleStateTool;
21
- const os_1 = require("os");
22
- const fs_1 = require("fs");
23
- const path_1 = require("path");
24
20
  // Helper to clamp values to valid ranges
25
21
  function clamp(value, min, max, fallback) {
26
22
  const v = value ?? fallback;
@@ -399,75 +395,9 @@ function normalizeFontFamily(input) {
399
395
  }
400
396
  // Current poster state (single poster for now)
401
397
  let currentPoster = null;
402
- // --- File-based state persistence ---
403
- // Some MCP clients spawn a fresh process per tool call, losing in-memory state.
404
- // We persist to a temp file so state survives process restarts.
405
- const STATE_FILE_PATH = (0, path_1.join)((0, os_1.tmpdir)(), 'efecto-mcp-state.json');
406
- const STATE_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
407
- /** Write current poster state to the temp file (best-effort). */
408
- function persistState() {
409
- if (!currentPoster)
410
- return;
411
- try {
412
- const payload = JSON.stringify({ timestamp: Date.now(), state: currentPoster });
413
- (0, fs_1.writeFileSync)(STATE_FILE_PATH, payload, 'utf-8');
414
- }
415
- catch {
416
- // Silently ignore — falls back to in-memory only
417
- }
418
- }
419
- /** Load persisted state from the temp file if valid and not stale. */
420
- function loadPersistedState() {
421
- try {
422
- const raw = (0, fs_1.readFileSync)(STATE_FILE_PATH, 'utf-8');
423
- const parsed = JSON.parse(raw);
424
- if (!parsed.state || !parsed.timestamp)
425
- return null;
426
- if (Date.now() - parsed.timestamp > STATE_MAX_AGE_MS)
427
- return null;
428
- return parsed.state;
429
- }
430
- catch {
431
- return null;
432
- }
433
- }
434
- /**
435
- * Ensure a poster exists. Restores from persisted file if needed,
436
- * or auto-creates a default poster as a last resort.
437
- */
438
- function ensurePoster() {
439
- if (currentPoster)
440
- return currentPoster;
441
- // Try restoring from persisted state
442
- const persisted = loadPersistedState();
443
- if (persisted) {
444
- currentPoster = persisted;
445
- return currentPoster;
446
- }
447
- // Auto-create a default poster (same defaults as create_poster)
448
- currentPoster = {
449
- canvas: {
450
- aspectRatio: '9:16',
451
- backgroundColor: '#1a1a1a',
452
- layoutMode: 'column',
453
- alignItems: 'center',
454
- justifyContent: 'start',
455
- gap: 0.04,
456
- padding: { top: 0.08, right: 0.08, bottom: 0.08, left: 0.08 },
457
- },
458
- layers: [createDefaultBackgroundLayer('#1a1a1a')],
459
- postProcesses: [],
460
- };
461
- persistState();
462
- return currentPoster;
463
- }
464
398
  // Reset state (useful for testing)
465
399
  function resetState() {
466
400
  currentPoster = null;
467
- try {
468
- (0, fs_1.unlinkSync)(STATE_FILE_PATH);
469
- }
470
- catch { /* ignore */ }
471
401
  }
472
402
  // Get current state
473
403
  function getCurrentState() {
@@ -800,113 +730,6 @@ function parseGradientStopsInput(input, fallbackStops) {
800
730
  .filter((stop) => stop !== null);
801
731
  return parsed.length >= MIN_GRADIENT_STOPS ? parsed : null;
802
732
  }
803
- /**
804
- * Build a Fill object from flat MCP tool params (fillType, gradientStops, etc.).
805
- * Returns null when no gradient params are present (caller should fall back to solid).
806
- */
807
- function buildFillFromParams(params) {
808
- const fillType = params.fillType;
809
- const hasGradientParams = fillType === 'linear' || fillType === 'radial' ||
810
- params.gradientStyle !== undefined ||
811
- params.gradientStops !== undefined ||
812
- params.gradientStartColor !== undefined ||
813
- params.gradientEndColor !== undefined;
814
- if (!hasGradientParams)
815
- return null;
816
- const gradientStyle = fillType === 'radial' || params.gradientStyle === 'radial' ? 'radial' : 'linear';
817
- const defaults = gradientStyle === 'radial'
818
- ? DEFAULT_RADIAL_GRADIENT_FILL
819
- : DEFAULT_LINEAR_GRADIENT_FILL;
820
- const explicitStops = parseGradientStopsInput(params.gradientStops, defaults.stops);
821
- const stops = explicitStops ?? [
822
- {
823
- color: params.gradientStartColor || defaults.stops[0].color,
824
- opacity: defaults.stops[0].opacity,
825
- position: defaults.stops[0].position,
826
- },
827
- {
828
- color: params.gradientEndColor || defaults.stops[1].color,
829
- opacity: defaults.stops[1].opacity,
830
- position: defaults.stops[1].position,
831
- },
832
- ];
833
- if (gradientStyle === 'radial') {
834
- return normalizeRadialGradientFill({
835
- type: 'radial',
836
- center: {
837
- x: Number.isFinite(params.gradientCenterX)
838
- ? params.gradientCenterX
839
- : DEFAULT_RADIAL_GRADIENT_FILL.center.x,
840
- y: Number.isFinite(params.gradientCenterY)
841
- ? params.gradientCenterY
842
- : DEFAULT_RADIAL_GRADIENT_FILL.center.y,
843
- },
844
- radius: Number.isFinite(params.gradientRadius)
845
- ? params.gradientRadius
846
- : DEFAULT_RADIAL_GRADIENT_FILL.radius,
847
- stops,
848
- });
849
- }
850
- return normalizeLinearGradientFill({
851
- type: 'linear',
852
- angle: Number.isFinite(params.gradientAngle)
853
- ? params.gradientAngle
854
- : DEFAULT_LINEAR_GRADIENT_FILL.angle,
855
- stops,
856
- });
857
- }
858
- /** Reusable schema properties for gradient fill params — spread into tool schemas. */
859
- const GRADIENT_FILL_SCHEMA_PROPS = {
860
- fillType: {
861
- type: 'string',
862
- enum: ['solid', 'linear', 'radial'],
863
- description: 'Fill type: "solid" (default), "linear" gradient, or "radial" gradient',
864
- },
865
- gradientStops: {
866
- type: 'array',
867
- description: 'Gradient color stops (2-8). Overrides start/end colors when provided.',
868
- items: {
869
- type: 'object',
870
- properties: {
871
- color: { type: 'string', description: 'Stop color in hex' },
872
- opacity: { type: 'number', description: 'Stop opacity (0-1)' },
873
- position: { type: 'number', description: 'Stop position (0-1)' },
874
- },
875
- required: ['color', 'position'],
876
- },
877
- minItems: 2,
878
- maxItems: 8,
879
- },
880
- gradientAngle: {
881
- type: 'number',
882
- description: 'Linear gradient angle in degrees (default 135)',
883
- },
884
- gradientStartColor: {
885
- type: 'string',
886
- description: 'Gradient start color in hex (shortcut for 2-stop gradient)',
887
- },
888
- gradientEndColor: {
889
- type: 'string',
890
- description: 'Gradient end color in hex (shortcut for 2-stop gradient)',
891
- },
892
- gradientStyle: {
893
- type: 'string',
894
- enum: ['linear', 'radial'],
895
- description: 'Gradient style (alternative to fillType for setting gradient type)',
896
- },
897
- gradientCenterX: {
898
- type: 'number',
899
- description: 'Radial gradient center X (0-1)',
900
- },
901
- gradientCenterY: {
902
- type: 'number',
903
- description: 'Radial gradient center Y (0-1)',
904
- },
905
- gradientRadius: {
906
- type: 'number',
907
- description: 'Radial gradient radius (0.05-2)',
908
- },
909
- };
910
733
  // Tool definitions
911
734
  exports.stateTools = [
912
735
  {
@@ -1115,7 +938,6 @@ exports.stateTools = [
1115
938
  // Shape style properties (rectangle/ellipse/polygon/star/line)
1116
939
  fillColor: { type: 'string', description: 'Shape fill color in hex (e.g. #ffffff)' },
1117
940
  fillOpacity: { type: 'number', description: 'Shape fill opacity (0-1)' },
1118
- ...GRADIENT_FILL_SCHEMA_PROPS,
1119
941
  strokeColor: { type: 'string', description: 'Shape stroke color in hex' },
1120
942
  strokeWidth: { type: 'number', description: 'Shape stroke width (normalized; e.g. 0.005-0.02)' },
1121
943
  strokeOpacity: { type: 'number', description: 'Shape stroke opacity (0-1)' },
@@ -1324,7 +1146,6 @@ exports.stateTools = [
1324
1146
  // Shape style
1325
1147
  fillColor: { type: 'string', description: 'Shape fill color (hex)' },
1326
1148
  fillOpacity: { type: 'number', description: 'Shape fill opacity (0-1)' },
1327
- ...GRADIENT_FILL_SCHEMA_PROPS,
1328
1149
  strokeColor: { type: 'string', description: 'Shape stroke color (hex)' },
1329
1150
  strokeWidth: { type: 'number', description: 'Shape stroke width (normalized; e.g. 0.005-0.02)' },
1330
1151
  strokeOpacity: { type: 'number', description: 'Shape stroke opacity (0-1)' },
@@ -1496,7 +1317,6 @@ exports.stateTools = [
1496
1317
  paddingLeft: { type: 'number', description: 'Padding left as % of canvas' },
1497
1318
  fillColor: { type: 'string', description: 'Fill color (group background OR shape fill)' },
1498
1319
  fillOpacity: { type: 'number', description: 'Fill opacity (group background OR shape fill)' },
1499
- ...GRADIENT_FILL_SCHEMA_PROPS,
1500
1320
  cornerRadius: { type: 'number', description: 'Corner radius (group OR shape)' },
1501
1321
  // Shadow properties (all layer types)
1502
1322
  shadowOffsetX: { type: 'number', description: 'Shadow horizontal offset in CSS px' },
@@ -1769,7 +1589,6 @@ async function handleStateTool(name, args) {
1769
1589
  layers: [createDefaultBackgroundLayer(backgroundColor)],
1770
1590
  postProcesses: [],
1771
1591
  };
1772
- persistState();
1773
1592
  return {
1774
1593
  content: [
1775
1594
  {
@@ -1785,7 +1604,11 @@ async function handleStateTool(name, args) {
1785
1604
  };
1786
1605
  }
1787
1606
  case 'clear_poster': {
1788
- ensurePoster();
1607
+ if (!currentPoster) {
1608
+ return {
1609
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
1610
+ };
1611
+ }
1789
1612
  const bgLayer = currentPoster.layers.find((l) => l.type === 'background');
1790
1613
  const defaultBg = createDefaultBackgroundLayer(currentPoster.canvas.backgroundColor || '#1a1a1a');
1791
1614
  currentPoster = {
@@ -1793,7 +1616,6 @@ async function handleStateTool(name, args) {
1793
1616
  layers: bgLayer ? [bgLayer] : [defaultBg],
1794
1617
  postProcesses: [],
1795
1618
  };
1796
- persistState();
1797
1619
  return {
1798
1620
  content: [{
1799
1621
  type: 'text',
@@ -1805,7 +1627,11 @@ async function handleStateTool(name, args) {
1805
1627
  };
1806
1628
  }
1807
1629
  case 'set_page_layout': {
1808
- ensurePoster();
1630
+ if (!currentPoster) {
1631
+ return {
1632
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
1633
+ };
1634
+ }
1809
1635
  const layoutMode = params.layoutMode;
1810
1636
  if (layoutMode !== undefined) {
1811
1637
  if (layoutMode !== 'absolute' && layoutMode !== 'row' && layoutMode !== 'column') {
@@ -1843,7 +1669,6 @@ async function handleStateTool(name, args) {
1843
1669
  };
1844
1670
  currentPoster.canvas.padding = nextPadding;
1845
1671
  }
1846
- persistState();
1847
1672
  return {
1848
1673
  content: [
1849
1674
  {
@@ -1858,7 +1683,11 @@ async function handleStateTool(name, args) {
1858
1683
  };
1859
1684
  }
1860
1685
  case 'modify_canvas': {
1861
- ensurePoster();
1686
+ if (!currentPoster) {
1687
+ return {
1688
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
1689
+ };
1690
+ }
1862
1691
  const canvas = currentPoster.canvas;
1863
1692
  if (params.aspectRatio)
1864
1693
  canvas.aspectRatio = params.aspectRatio;
@@ -1868,7 +1697,6 @@ async function handleStateTool(name, args) {
1868
1697
  canvas.canvasColor = params.canvasColor;
1869
1698
  if (params.overflow !== undefined)
1870
1699
  canvas.overflow = params.overflow;
1871
- persistState();
1872
1700
  return {
1873
1701
  content: [
1874
1702
  {
@@ -1887,7 +1715,11 @@ async function handleStateTool(name, args) {
1887
1715
  };
1888
1716
  }
1889
1717
  case 'set_background': {
1890
- ensurePoster();
1718
+ if (!currentPoster) {
1719
+ return {
1720
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
1721
+ };
1722
+ }
1891
1723
  // Background is always at index 0
1892
1724
  const bgLayer = currentPoster.layers[0];
1893
1725
  if (bgLayer.type !== 'background') {
@@ -1899,8 +1731,53 @@ async function handleStateTool(name, args) {
1899
1731
  const contentType = requestedType === 'gradient' ? 'solid' : requestedType;
1900
1732
  bgLayer.contentType = contentType;
1901
1733
  if (requestedType === 'gradient') {
1902
- const gradient = buildFillFromParams(params)
1903
- ?? normalizeLinearGradientFill(DEFAULT_LINEAR_GRADIENT_FILL);
1734
+ const gradientStyle = params.gradientStyle === 'radial' ? 'radial' : 'linear';
1735
+ const explicitStops = parseGradientStopsInput(params.gradientStops, gradientStyle === 'radial' ? DEFAULT_RADIAL_GRADIENT_FILL.stops : DEFAULT_LINEAR_GRADIENT_FILL.stops);
1736
+ const gradient = gradientStyle === 'radial'
1737
+ ? normalizeRadialGradientFill({
1738
+ type: 'radial',
1739
+ center: {
1740
+ x: Number.isFinite(params.gradientCenterX)
1741
+ ? params.gradientCenterX
1742
+ : DEFAULT_RADIAL_GRADIENT_FILL.center.x,
1743
+ y: Number.isFinite(params.gradientCenterY)
1744
+ ? params.gradientCenterY
1745
+ : DEFAULT_RADIAL_GRADIENT_FILL.center.y,
1746
+ },
1747
+ radius: Number.isFinite(params.gradientRadius)
1748
+ ? params.gradientRadius
1749
+ : DEFAULT_RADIAL_GRADIENT_FILL.radius,
1750
+ stops: explicitStops ?? [
1751
+ {
1752
+ color: params.gradientStartColor || DEFAULT_RADIAL_GRADIENT_FILL.stops[0].color,
1753
+ opacity: DEFAULT_RADIAL_GRADIENT_FILL.stops[0].opacity,
1754
+ position: DEFAULT_RADIAL_GRADIENT_FILL.stops[0].position,
1755
+ },
1756
+ {
1757
+ color: params.gradientEndColor || DEFAULT_RADIAL_GRADIENT_FILL.stops[1].color,
1758
+ opacity: DEFAULT_RADIAL_GRADIENT_FILL.stops[1].opacity,
1759
+ position: DEFAULT_RADIAL_GRADIENT_FILL.stops[1].position,
1760
+ },
1761
+ ],
1762
+ })
1763
+ : normalizeLinearGradientFill({
1764
+ type: 'linear',
1765
+ angle: Number.isFinite(params.gradientAngle)
1766
+ ? params.gradientAngle
1767
+ : DEFAULT_LINEAR_GRADIENT_FILL.angle,
1768
+ stops: explicitStops ?? [
1769
+ {
1770
+ color: params.gradientStartColor || DEFAULT_LINEAR_GRADIENT_FILL.stops[0].color,
1771
+ opacity: DEFAULT_LINEAR_GRADIENT_FILL.stops[0].opacity,
1772
+ position: DEFAULT_LINEAR_GRADIENT_FILL.stops[0].position,
1773
+ },
1774
+ {
1775
+ color: params.gradientEndColor || DEFAULT_LINEAR_GRADIENT_FILL.stops[1].color,
1776
+ opacity: DEFAULT_LINEAR_GRADIENT_FILL.stops[1].opacity,
1777
+ position: DEFAULT_LINEAR_GRADIENT_FILL.stops[1].position,
1778
+ },
1779
+ ],
1780
+ });
1904
1781
  bgLayer.fill = gradient;
1905
1782
  bgLayer.solidColor = gradient.stops[0].color;
1906
1783
  currentPoster.canvas.backgroundColor = gradient.stops[0].color;
@@ -1945,7 +1822,6 @@ async function handleStateTool(name, args) {
1945
1822
  objectFit: params.objectFit || 'cover',
1946
1823
  };
1947
1824
  }
1948
- persistState();
1949
1825
  return {
1950
1826
  content: [
1951
1827
  {
@@ -1960,7 +1836,11 @@ async function handleStateTool(name, args) {
1960
1836
  };
1961
1837
  }
1962
1838
  case 'add_layer': {
1963
- ensurePoster();
1839
+ if (!currentPoster) {
1840
+ return {
1841
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
1842
+ };
1843
+ }
1964
1844
  const layerType = params.type;
1965
1845
  if (!layerType) {
1966
1846
  return {
@@ -2080,17 +1960,14 @@ async function handleStateTool(name, args) {
2080
1960
  }
2081
1961
  else if (layerType === 'rectangle') {
2082
1962
  const fillColor = normalizeHexColor(params.fillColor) || '#3b82f6';
2083
- const fillOpacity = normalizeOpacity(params.fillOpacity, 1);
2084
1963
  const strokeColor = normalizeHexColor(params.strokeColor) || '#000000';
2085
- const gradientFill = buildFillFromParams(params);
2086
1964
  layer = {
2087
1965
  ...baseLayer,
2088
1966
  type: 'rectangle',
2089
1967
  style: {
2090
- fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
2091
1968
  fillType: 'solid',
2092
1969
  fillColor,
2093
- fillOpacity,
1970
+ fillOpacity: normalizeOpacity(params.fillOpacity, 1),
2094
1971
  strokeColor,
2095
1972
  strokeWidth: normalizeStrokeWidth(params.strokeWidth, 0),
2096
1973
  strokeOpacity: normalizeOpacity(params.strokeOpacity, 1),
@@ -2105,19 +1982,16 @@ async function handleStateTool(name, args) {
2105
1982
  }
2106
1983
  else if (layerType === 'ellipse') {
2107
1984
  const fillColor = normalizeHexColor(params.fillColor) || '#3b82f6';
2108
- const fillOpacity = normalizeOpacity(params.fillOpacity, 1);
2109
1985
  const strokeColor = normalizeHexColor(params.strokeColor) || '#000000';
2110
1986
  const w = cssPercentToSize(params.width, 20);
2111
1987
  const h = cssPercentToSize(params.height, 20);
2112
- const gradientFill = buildFillFromParams(params);
2113
1988
  layer = {
2114
1989
  ...baseLayer,
2115
1990
  type: 'ellipse',
2116
1991
  style: {
2117
- fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
2118
1992
  fillType: 'solid',
2119
1993
  fillColor,
2120
- fillOpacity,
1994
+ fillOpacity: normalizeOpacity(params.fillOpacity, 1),
2121
1995
  strokeColor,
2122
1996
  strokeWidth: normalizeStrokeWidth(params.strokeWidth, 0),
2123
1997
  strokeOpacity: normalizeOpacity(params.strokeOpacity, 1),
@@ -2130,19 +2004,16 @@ async function handleStateTool(name, args) {
2130
2004
  }
2131
2005
  else if (layerType === 'polygon') {
2132
2006
  const fillColor = normalizeHexColor(params.fillColor) || '#3b82f6';
2133
- const fillOpacity = normalizeOpacity(params.fillOpacity, 1);
2134
2007
  const strokeColor = normalizeHexColor(params.strokeColor) || '#000000';
2135
2008
  const w = cssPercentToSize(params.width, 20);
2136
2009
  const h = cssPercentToSize(params.height, 20);
2137
- const gradientFill = buildFillFromParams(params);
2138
2010
  layer = {
2139
2011
  ...baseLayer,
2140
2012
  type: 'polygon',
2141
2013
  style: {
2142
- fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
2143
2014
  fillType: 'solid',
2144
2015
  fillColor,
2145
- fillOpacity,
2016
+ fillOpacity: normalizeOpacity(params.fillOpacity, 1),
2146
2017
  strokeColor,
2147
2018
  strokeWidth: normalizeStrokeWidth(params.strokeWidth, 0),
2148
2019
  strokeOpacity: normalizeOpacity(params.strokeOpacity, 1),
@@ -2156,19 +2027,16 @@ async function handleStateTool(name, args) {
2156
2027
  }
2157
2028
  else if (layerType === 'star') {
2158
2029
  const fillColor = normalizeHexColor(params.fillColor) || '#3b82f6';
2159
- const fillOpacity = normalizeOpacity(params.fillOpacity, 1);
2160
2030
  const strokeColor = normalizeHexColor(params.strokeColor) || '#000000';
2161
2031
  const w = cssPercentToSize(params.width, 20);
2162
2032
  const h = cssPercentToSize(params.height, 20);
2163
- const gradientFill = buildFillFromParams(params);
2164
2033
  layer = {
2165
2034
  ...baseLayer,
2166
2035
  type: 'star',
2167
2036
  style: {
2168
- fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
2169
2037
  fillType: 'solid',
2170
2038
  fillColor,
2171
- fillOpacity,
2039
+ fillOpacity: normalizeOpacity(params.fillOpacity, 1),
2172
2040
  strokeColor,
2173
2041
  strokeWidth: normalizeStrokeWidth(params.strokeWidth, 0),
2174
2042
  strokeOpacity: normalizeOpacity(params.strokeOpacity, 1),
@@ -2211,7 +2079,6 @@ async function handleStateTool(name, args) {
2211
2079
  layer = baseLayer;
2212
2080
  }
2213
2081
  currentPoster.layers.push(layer);
2214
- persistState();
2215
2082
  return {
2216
2083
  content: [
2217
2084
  {
@@ -2227,7 +2094,11 @@ async function handleStateTool(name, args) {
2227
2094
  };
2228
2095
  }
2229
2096
  case 'add_group': {
2230
- ensurePoster();
2097
+ if (!currentPoster) {
2098
+ return {
2099
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
2100
+ };
2101
+ }
2231
2102
  const children = params.children;
2232
2103
  if (!children || !Array.isArray(children) || children.length === 0) {
2233
2104
  return {
@@ -2333,17 +2204,14 @@ async function handleStateTool(name, args) {
2333
2204
  }
2334
2205
  else if (childType === 'rectangle') {
2335
2206
  const fillColor = normalizeHexColor(childParams.fillColor) || '#3b82f6';
2336
- const fillOpacity = normalizeOpacity(childParams.fillOpacity, 1);
2337
2207
  const strokeColor = normalizeHexColor(childParams.strokeColor) || '#000000';
2338
- const gradientFill = buildFillFromParams(childParams);
2339
2208
  childLayers.push({
2340
2209
  ...childBase,
2341
2210
  type: 'rectangle',
2342
2211
  style: {
2343
- fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
2344
2212
  fillType: 'solid',
2345
2213
  fillColor,
2346
- fillOpacity,
2214
+ fillOpacity: normalizeOpacity(childParams.fillOpacity, 1),
2347
2215
  strokeColor,
2348
2216
  strokeWidth: normalizeStrokeWidth(childParams.strokeWidth, 0),
2349
2217
  strokeOpacity: normalizeOpacity(childParams.strokeOpacity, 1),
@@ -2358,19 +2226,16 @@ async function handleStateTool(name, args) {
2358
2226
  }
2359
2227
  else if (childType === 'ellipse') {
2360
2228
  const fillColor = normalizeHexColor(childParams.fillColor) || '#3b82f6';
2361
- const fillOpacity = normalizeOpacity(childParams.fillOpacity, 1);
2362
2229
  const strokeColor = normalizeHexColor(childParams.strokeColor) || '#000000';
2363
2230
  const w = cssPercentToSize(childParams.width, 20);
2364
2231
  const h = cssPercentToSize(childParams.height, 20);
2365
- const gradientFill = buildFillFromParams(childParams);
2366
2232
  childLayers.push({
2367
2233
  ...childBase,
2368
2234
  type: 'ellipse',
2369
2235
  style: {
2370
- fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
2371
2236
  fillType: 'solid',
2372
2237
  fillColor,
2373
- fillOpacity,
2238
+ fillOpacity: normalizeOpacity(childParams.fillOpacity, 1),
2374
2239
  strokeColor,
2375
2240
  strokeWidth: normalizeStrokeWidth(childParams.strokeWidth, 0),
2376
2241
  strokeOpacity: normalizeOpacity(childParams.strokeOpacity, 1),
@@ -2383,19 +2248,16 @@ async function handleStateTool(name, args) {
2383
2248
  }
2384
2249
  else if (childType === 'polygon') {
2385
2250
  const fillColor = normalizeHexColor(childParams.fillColor) || '#3b82f6';
2386
- const fillOpacity = normalizeOpacity(childParams.fillOpacity, 1);
2387
2251
  const strokeColor = normalizeHexColor(childParams.strokeColor) || '#000000';
2388
2252
  const w = cssPercentToSize(childParams.width, 20);
2389
2253
  const h = cssPercentToSize(childParams.height, 20);
2390
- const gradientFill = buildFillFromParams(childParams);
2391
2254
  childLayers.push({
2392
2255
  ...childBase,
2393
2256
  type: 'polygon',
2394
2257
  style: {
2395
- fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
2396
2258
  fillType: 'solid',
2397
2259
  fillColor,
2398
- fillOpacity,
2260
+ fillOpacity: normalizeOpacity(childParams.fillOpacity, 1),
2399
2261
  strokeColor,
2400
2262
  strokeWidth: normalizeStrokeWidth(childParams.strokeWidth, 0),
2401
2263
  strokeOpacity: normalizeOpacity(childParams.strokeOpacity, 1),
@@ -2409,19 +2271,16 @@ async function handleStateTool(name, args) {
2409
2271
  }
2410
2272
  else if (childType === 'star') {
2411
2273
  const fillColor = normalizeHexColor(childParams.fillColor) || '#3b82f6';
2412
- const fillOpacity = normalizeOpacity(childParams.fillOpacity, 1);
2413
2274
  const strokeColor = normalizeHexColor(childParams.strokeColor) || '#000000';
2414
2275
  const w = cssPercentToSize(childParams.width, 20);
2415
2276
  const h = cssPercentToSize(childParams.height, 20);
2416
- const gradientFill = buildFillFromParams(childParams);
2417
2277
  childLayers.push({
2418
2278
  ...childBase,
2419
2279
  type: 'star',
2420
2280
  style: {
2421
- fill: gradientFill ?? { type: 'solid', color: fillColor, opacity: fillOpacity },
2422
2281
  fillType: 'solid',
2423
2282
  fillColor,
2424
- fillOpacity,
2283
+ fillOpacity: normalizeOpacity(childParams.fillOpacity, 1),
2425
2284
  strokeColor,
2426
2285
  strokeWidth: normalizeStrokeWidth(childParams.strokeWidth, 0),
2427
2286
  strokeOpacity: normalizeOpacity(childParams.strokeOpacity, 1),
@@ -2511,7 +2370,6 @@ async function handleStateTool(name, args) {
2511
2370
  ...(params.cornerRadius !== undefined ? { cornerRadius: params.cornerRadius } : {}),
2512
2371
  };
2513
2372
  currentPoster.layers.push(groupLayer);
2514
- persistState();
2515
2373
  return {
2516
2374
  content: [
2517
2375
  {
@@ -2527,7 +2385,11 @@ async function handleStateTool(name, args) {
2527
2385
  };
2528
2386
  }
2529
2387
  case 'modify_layer': {
2530
- ensurePoster();
2388
+ if (!currentPoster) {
2389
+ return {
2390
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
2391
+ };
2392
+ }
2531
2393
  const layerId = params.layerId;
2532
2394
  if (!layerId) {
2533
2395
  return {
@@ -2875,19 +2737,6 @@ async function handleStateTool(name, args) {
2875
2737
  const existing = asFiniteNumber(anyLayer.style.strokeOpacity, 1);
2876
2738
  anyLayer.style.strokeOpacity = normalizeOpacity(params.strokeOpacity, existing);
2877
2739
  }
2878
- // Gradient fill update
2879
- const modifyGradientFill = buildFillFromParams(params);
2880
- if (modifyGradientFill) {
2881
- anyLayer.style.fill = modifyGradientFill;
2882
- anyLayer.style.fillType = 'solid';
2883
- }
2884
- else if (params.fillColor !== undefined || params.fillOpacity !== undefined) {
2885
- // Sync style.fill as solid when only color/opacity changed
2886
- const currentFillColor = typeof anyLayer.style.fillColor === 'string' ? anyLayer.style.fillColor : '#3b82f6';
2887
- const currentFillOpacity = asFiniteNumber(anyLayer.style.fillOpacity, 1);
2888
- anyLayer.style.fill = { type: 'solid', color: currentFillColor, opacity: currentFillOpacity };
2889
- anyLayer.style.fillType = 'solid';
2890
- }
2891
2740
  if (anyLayer.style.fillType === undefined && layer.type !== 'line')
2892
2741
  anyLayer.style.fillType = 'solid';
2893
2742
  if (anyLayer.style.strokeCap === undefined)
@@ -2967,7 +2816,6 @@ async function handleStateTool(name, args) {
2967
2816
  layer.transform.height = 1;
2968
2817
  }
2969
2818
  }
2970
- persistState();
2971
2819
  return {
2972
2820
  content: [
2973
2821
  {
@@ -2982,7 +2830,11 @@ async function handleStateTool(name, args) {
2982
2830
  };
2983
2831
  }
2984
2832
  case 'apply_effect': {
2985
- ensurePoster();
2833
+ if (!currentPoster) {
2834
+ return {
2835
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
2836
+ };
2837
+ }
2986
2838
  const effectId = params.effectId;
2987
2839
  if (!effectId) {
2988
2840
  return {
@@ -2991,7 +2843,6 @@ async function handleStateTool(name, args) {
2991
2843
  }
2992
2844
  if (effectId === 'none') {
2993
2845
  delete currentPoster.effect;
2994
- persistState();
2995
2846
  return {
2996
2847
  content: [
2997
2848
  {
@@ -3239,7 +3090,6 @@ async function handleStateTool(name, args) {
3239
3090
  };
3240
3091
  }
3241
3092
  currentPoster.effect = effect;
3242
- persistState();
3243
3093
  return {
3244
3094
  content: [
3245
3095
  {
@@ -3254,7 +3104,11 @@ async function handleStateTool(name, args) {
3254
3104
  };
3255
3105
  }
3256
3106
  case 'add_postprocess': {
3257
- ensurePoster();
3107
+ if (!currentPoster) {
3108
+ return {
3109
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
3110
+ };
3111
+ }
3258
3112
  const ppType = params.type;
3259
3113
  if (!ppType) {
3260
3114
  return {
@@ -3336,7 +3190,6 @@ async function handleStateTool(name, args) {
3336
3190
  settings,
3337
3191
  };
3338
3192
  currentPoster.postProcesses.push(postProcess);
3339
- persistState();
3340
3193
  // Build response with warning if dither is active
3341
3194
  const response = {
3342
3195
  success: true,
@@ -3359,7 +3212,11 @@ async function handleStateTool(name, args) {
3359
3212
  };
3360
3213
  }
3361
3214
  case 'remove_layer': {
3362
- ensurePoster();
3215
+ if (!currentPoster) {
3216
+ return {
3217
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
3218
+ };
3219
+ }
3363
3220
  const layerId = params.layerId;
3364
3221
  if (!layerId) {
3365
3222
  return {
@@ -3378,7 +3235,6 @@ async function handleStateTool(name, args) {
3378
3235
  };
3379
3236
  }
3380
3237
  currentPoster.layers.splice(layerIndex, 1);
3381
- persistState();
3382
3238
  return {
3383
3239
  content: [
3384
3240
  {
@@ -3393,7 +3249,11 @@ async function handleStateTool(name, args) {
3393
3249
  };
3394
3250
  }
3395
3251
  case 'move_layer': {
3396
- ensurePoster();
3252
+ if (!currentPoster) {
3253
+ return {
3254
+ content: [{ type: 'text', text: 'Error: No poster created. Use create_poster first.' }],
3255
+ };
3256
+ }
3397
3257
  const poster = currentPoster;
3398
3258
  const layerId = params.layerId;
3399
3259
  const direction = params.direction;
@@ -3436,7 +3296,6 @@ async function handleStateTool(name, args) {
3436
3296
  content: [{ type: 'text', text: 'Error: direction must be one of: up, down, top, bottom' }],
3437
3297
  };
3438
3298
  }
3439
- persistState();
3440
3299
  return {
3441
3300
  content: [
3442
3301
  {
@@ -3452,7 +3311,11 @@ async function handleStateTool(name, args) {
3452
3311
  };
3453
3312
  }
3454
3313
  case 'get_state': {
3455
- ensurePoster();
3314
+ if (!currentPoster) {
3315
+ return {
3316
+ content: [{ type: 'text', text: 'No poster created yet. Use create_poster first.' }],
3317
+ };
3318
+ }
3456
3319
  return {
3457
3320
  content: [
3458
3321
  {