@pinepaper.studio/mcp-server 1.6.20 → 1.6.22

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.
Files changed (105) hide show
  1. package/README.de.md +1 -1
  2. package/README.es.md +1 -1
  3. package/README.fr.md +1 -1
  4. package/README.hi.md +1 -1
  5. package/README.ja.md +1 -1
  6. package/README.ko.md +1 -1
  7. package/README.md +96 -5
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/build-info.json +3 -3
  11. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  12. package/dist/ontology/upstream-vocabulary.js +3 -3
  13. package/dist/ontology/upstream-vocabulary.js.map +1 -1
  14. package/dist/ontology/vocabulary.js +1 -1
  15. package/dist/ontology/vocabulary.js.map +1 -1
  16. package/dist/tools/choreograph.d.ts.map +1 -1
  17. package/dist/tools/choreograph.js +10 -0
  18. package/dist/tools/choreograph.js.map +1 -1
  19. package/dist/tools/craft-tools.d.ts +55 -0
  20. package/dist/tools/craft-tools.d.ts.map +1 -0
  21. package/dist/tools/craft-tools.js +139 -0
  22. package/dist/tools/craft-tools.js.map +1 -0
  23. package/dist/tools/definitions.d.ts.map +1 -1
  24. package/dist/tools/definitions.js +117 -7
  25. package/dist/tools/definitions.js.map +1 -1
  26. package/dist/tools/effect-class.d.ts.map +1 -1
  27. package/dist/tools/effect-class.js +6 -0
  28. package/dist/tools/effect-class.js.map +1 -1
  29. package/dist/tools/engine-lists.d.ts +2 -2
  30. package/dist/tools/engine-lists.js +2 -2
  31. package/dist/tools/engine-methods.d.ts.map +1 -1
  32. package/dist/tools/engine-methods.js +4 -0
  33. package/dist/tools/engine-methods.js.map +1 -1
  34. package/dist/tools/engine-surface.d.ts +1 -1
  35. package/dist/tools/engine-surface.d.ts.map +1 -1
  36. package/dist/tools/engine-surface.js +55 -7
  37. package/dist/tools/engine-surface.js.map +1 -1
  38. package/dist/tools/handlers/brand-intake.d.ts.map +1 -1
  39. package/dist/tools/handlers/brand-intake.js +42 -1
  40. package/dist/tools/handlers/brand-intake.js.map +1 -1
  41. package/dist/tools/handlers/generate.d.ts +28 -28
  42. package/dist/tools/handlers.d.ts.map +1 -1
  43. package/dist/tools/handlers.js +161 -10
  44. package/dist/tools/handlers.js.map +1 -1
  45. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  46. package/dist/tools/minimal-descriptions.js +7 -0
  47. package/dist/tools/minimal-descriptions.js.map +1 -1
  48. package/dist/tools/shape-params.d.ts +2 -2
  49. package/dist/tools/shape-params.d.ts.map +1 -1
  50. package/dist/tools/shape-params.js +9 -2
  51. package/dist/tools/shape-params.js.map +1 -1
  52. package/dist/tools/stick-vocabulary.d.ts +1 -1
  53. package/dist/tools/stick-vocabulary.js +1 -1
  54. package/dist/tools/toolkits.d.ts.map +1 -1
  55. package/dist/tools/toolkits.js +9 -0
  56. package/dist/tools/toolkits.js.map +1 -1
  57. package/dist/types/code-generator.d.ts +10 -1
  58. package/dist/types/code-generator.d.ts.map +1 -1
  59. package/dist/types/code-generator.js +605 -107
  60. package/dist/types/code-generator.js.map +1 -1
  61. package/dist/types/schemas.d.ts +1745 -582
  62. package/dist/types/schemas.d.ts.map +1 -1
  63. package/dist/types/schemas.js +144 -4
  64. package/dist/types/schemas.js.map +1 -1
  65. package/dist/utils/asset-paths.d.ts +11 -0
  66. package/dist/utils/asset-paths.d.ts.map +1 -0
  67. package/dist/utils/asset-paths.js +67 -0
  68. package/dist/utils/asset-paths.js.map +1 -0
  69. package/dist/utils/safe-fetch.d.ts +28 -0
  70. package/dist/utils/safe-fetch.d.ts.map +1 -0
  71. package/dist/utils/safe-fetch.js +221 -0
  72. package/dist/utils/safe-fetch.js.map +1 -0
  73. package/dist/vendor/design/bauhaus-generator.d.ts.map +1 -1
  74. package/dist/vendor/design/bauhaus-generator.js +20 -5
  75. package/dist/vendor/design/bauhaus-generator.js.map +1 -1
  76. package/dist/vendor/design/design-style.d.ts +2 -2
  77. package/dist/vendor/design/design-style.d.ts.map +1 -1
  78. package/dist/vendor/design/design-style.js +7 -2
  79. package/dist/vendor/design/design-style.js.map +1 -1
  80. package/dist/vendor/design/subdivision-tool-schema.d.ts +16 -0
  81. package/dist/vendor/design/subdivision-tool-schema.d.ts.map +1 -0
  82. package/dist/vendor/design/subdivision-tool-schema.js +104 -0
  83. package/dist/vendor/design/subdivision-tool-schema.js.map +1 -0
  84. package/dist/vendor/design/textile-tool-schema.d.ts +14 -0
  85. package/dist/vendor/design/textile-tool-schema.d.ts.map +1 -0
  86. package/dist/vendor/design/textile-tool-schema.js +195 -0
  87. package/dist/vendor/design/textile-tool-schema.js.map +1 -0
  88. package/dist/vendor/design/ui-kit-runtime.d.ts +49 -0
  89. package/dist/vendor/design/ui-kit-runtime.d.ts.map +1 -0
  90. package/dist/vendor/design/ui-kit-runtime.js +761 -0
  91. package/dist/vendor/design/ui-kit-runtime.js.map +1 -0
  92. package/dist/vendor/design/ui-kit-tool-schema.d.ts +280 -0
  93. package/dist/vendor/design/ui-kit-tool-schema.d.ts.map +1 -0
  94. package/dist/vendor/design/ui-kit-tool-schema.js +209 -0
  95. package/dist/vendor/design/ui-kit-tool-schema.js.map +1 -0
  96. package/manifest.json +29 -1
  97. package/package.json +7 -4
  98. package/dist/assets/repositories/openclipart.d.ts +0 -41
  99. package/dist/assets/repositories/openclipart.d.ts.map +0 -1
  100. package/dist/assets/repositories/openclipart.js +0 -139
  101. package/dist/assets/repositories/openclipart.js.map +0 -1
  102. package/dist/assets/repositories/svgrepo.d.ts +0 -61
  103. package/dist/assets/repositories/svgrepo.d.ts.map +0 -1
  104. package/dist/assets/repositories/svgrepo.js +0 -276
  105. package/dist/assets/repositories/svgrepo.js.map +0 -1
@@ -5,6 +5,7 @@
5
5
  * Supports i18n for localized error and success messages.
6
6
  * Supports browser execution via Puppeteer.
7
7
  */
8
+ import { CRAFT_FRAME_JS, generateDrawWeave, generateDrawKnots, generateDrawSashiko, generateDrawSubdivision, generateUiKit, uiKitCallProblems, } from './craft-tools.js';
8
9
  import { codeGenerator, generateQueryMutationsCode, SHEET_RUBRIC } from '../types/code-generator.js';
9
10
  import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, RelightInputSchema, ChoreographInputSchema, EmitterInputSchema, MorphSequenceInputSchema, OriginalCharacterInputSchema, LookInputSchema, RenderHookInputSchema, ShaderGraphInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, CharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, RenderBatchInputSchema, AccessibilityCheckInputSchema, QueryMutationsInputSchema, ComposeInputSchema, CropImageInputSchema, PathOpInputSchema, MotionInputSchema, SoundInputSchema, InterchangeInputSchema, StickInputSchema, DesignSystemInputSchema, StoryInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
10
11
  // Diagram schemas
@@ -27,7 +28,7 @@ ImportImageInputSchema,
27
28
  // Scene management schemas
28
29
  ManageScenesInputSchema, ScenePlaybackInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema,
29
30
  // New consolidated tool schemas
30
- SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, BackgroundInputSchema, QueryInputSchema, QueryCapabilitiesInputSchema, DeformInputSchema, SpriteSheetInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, PlaceOnSurfaceInputSchema, StyledSceneInputSchema, ErrorCodes, } from '../types/schemas.js';
31
+ SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, BackgroundInputSchema, QueryInputSchema, QueryCapabilitiesInputSchema, DeformInputSchema, HandDrawnInputSchema, PenFigureInputSchema, CreateCharacterInputSchema, CharacterActionInputSchema, CharacterPoseInputSchema, SpriteSheetInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, PlaceOnSurfaceInputSchema, DrawWeaveInputSchema, DrawKnotsInputSchema, DrawSashikoInputSchema, DrawSubdivisionInputSchema, UiKitInputSchema, StyledSceneInputSchema, ErrorCodes, } from '../types/schemas.js';
31
32
  import { ZodError, z } from 'zod';
32
33
  import { AsyncLocalStorage } from 'node:async_hooks';
33
34
  import { effectClass } from './effect-class.js';
@@ -57,6 +58,8 @@ import { buildZip } from '../utils/zip.js';
57
58
  import { buildScc } from '../utils/scc.js';
58
59
  import { checkContrast, checkFlashes, parseCssColor } from '../utils/a11y.js';
59
60
  import { buildHtml5Ad, buildPlayable, externalRequests, pinSceneBackground } from '../utils/ad-package.js';
61
+ import { safeFetch, checkUrl, UnsafeUrlError } from '../utils/safe-fetch.js';
62
+ import { confineAssetPath } from '../utils/asset-paths.js';
60
63
  /**
61
64
  * Registry of per-domain handler maps. Tools listed here short-circuit the
62
65
  * main switch at dispatch time (see handleToolCallInner). New domains are
@@ -175,9 +178,11 @@ const MAX_REMOTE_IMAGE_BYTES = 32 * 1024 * 1024;
175
178
  export async function fetchImageAsDataUrl(url) {
176
179
  let res;
177
180
  try {
178
- res = await fetch(url, { redirect: 'follow' });
181
+ res = await safeFetch(url);
179
182
  }
180
183
  catch (e) {
184
+ if (e instanceof UnsafeUrlError)
185
+ return { error: e.message };
181
186
  const why = e instanceof Error ? e.message : 'network request failed';
182
187
  return { error: `could not reach ${url} — ${why}. This fetch runs in the MCP server, not the page, so it is not a CSP problem: check the host resolves and any proxy is reachable.` };
183
188
  }
@@ -231,14 +236,18 @@ export async function resolveImageSource(input) {
231
236
  return { src: raw };
232
237
  if (/^https?:/i.test(raw))
233
238
  return fetchImageAsDataUrl(raw);
234
- const path = raw.startsWith('file://') ? fileURLToPath(raw) : raw;
235
- const ext = extname(path).toLowerCase();
239
+ const asked = raw.startsWith('file://') ? fileURLToPath(raw) : raw;
240
+ const ext = extname(asked).toLowerCase();
236
241
  if (!IMAGE_EXTENSIONS.has(ext)) {
237
242
  return {
238
243
  error: `"${input}" is neither an http(s) URL nor a path to an image file. `
239
244
  + `Recognised image extensions: ${[...IMAGE_EXTENSIONS].join(', ')}.`,
240
245
  };
241
246
  }
247
+ const confined = await confineAssetPath(asked);
248
+ if ('error' in confined)
249
+ return confined;
250
+ const path = confined.path;
242
251
  let bytes;
243
252
  try {
244
253
  const info = await stat(path);
@@ -286,9 +295,11 @@ export async function resolveMediaSource(input) {
286
295
  if (/^https?:/i.test(raw)) {
287
296
  let res;
288
297
  try {
289
- res = await fetch(raw, { redirect: 'follow', headers: { 'User-Agent': 'PinePaper-MCP (+https://pinepaper.studio)' } });
298
+ res = await safeFetch(raw, { headers: { 'User-Agent': 'PinePaper-MCP (+https://pinepaper.studio)' } });
290
299
  }
291
300
  catch (e) {
301
+ if (e instanceof UnsafeUrlError)
302
+ return { error: e.message };
292
303
  const why = e instanceof Error ? e.message : 'network request failed';
293
304
  return { error: `could not reach ${raw} — ${why}. This fetch runs in the MCP server, not the page, so it is not a CSP problem: check the host resolves and any proxy is reachable.` };
294
305
  }
@@ -308,11 +319,15 @@ export async function resolveMediaSource(input) {
308
319
  return tooBig(raw, buf.length);
309
320
  return { src: `data:${type};base64,${buf.toString('base64')}` };
310
321
  }
311
- const path = raw.startsWith('file://') ? fileURLToPath(raw) : raw;
312
- const type = MEDIA_MIME[extname(path).toLowerCase()];
322
+ const asked = raw.startsWith('file://') ? fileURLToPath(raw) : raw;
323
+ const type = MEDIA_MIME[extname(asked).toLowerCase()];
313
324
  if (!type) {
314
325
  return { error: `"${input}" is neither an http(s) URL, a data: URL, nor a path to an audio/video file. Recognised extensions: ${Object.keys(MEDIA_MIME).join(', ')}.` };
315
326
  }
327
+ const confined = await confineAssetPath(asked);
328
+ if ('error' in confined)
329
+ return confined;
330
+ const path = confined.path;
316
331
  try {
317
332
  const info = await stat(path);
318
333
  if (!info.isFile())
@@ -983,6 +998,15 @@ export async function customCodeRollback(controller, error) {
983
998
  * isolated context, so code mode cannot do it and says so.
984
999
  */
985
1000
  async function brandFromUrl(url, shots, options) {
1001
+ // Same address rules as every server-side fetch, before a browser is started.
1002
+ // The page's own requests are checked as it loads (see intakeBrand).
1003
+ try {
1004
+ await checkUrl(url);
1005
+ }
1006
+ catch (e) {
1007
+ const why = e instanceof Error ? e.message : String(e);
1008
+ return errorResult(e instanceof UnsafeUrlError ? ErrorCodes.INVALID_PARAMS : ErrorCodes.EXECUTION_ERROR, e instanceof UnsafeUrlError ? why : `could not reach ${url}: ${why}`, {}, { toolName: 'pinepaper_brand_kit' });
1009
+ }
986
1010
  const effectiveExecMode = options.executionMode ?? getExecutionMode();
987
1011
  if (effectiveExecMode === 'code' || !options.executeInBrowser) {
988
1012
  return errorResult(ErrorCodes.EXECUTION_ERROR, 'brand_kit from_url reads the site in the MCP server\'s browser, so it needs browser mode; it cannot run as generated code.', {}, { toolName: 'pinepaper_brand_kit' });
@@ -1099,6 +1123,24 @@ async function captureSheet(input, options) {
1099
1123
  }],
1100
1124
  };
1101
1125
  }
1126
+ /**
1127
+ * The canvas the craft tools fill when the call gives no bounds. Asked of the
1128
+ * studio, so a portrait or square canvas is filled rather than a 1280x720
1129
+ * corner of it; undefined (the generators' own default) without a studio.
1130
+ */
1131
+ async function craftFrame(options) {
1132
+ const ctl = options.browserController;
1133
+ if (!options.executeInBrowser || !ctl || !ctl.connected)
1134
+ return undefined;
1135
+ try {
1136
+ const r = await ctl.executeCode(CRAFT_FRAME_JS, false);
1137
+ const f = r.success ? r.result : null;
1138
+ return f && f.width > 0 && f.height > 0 ? f : undefined;
1139
+ }
1140
+ catch {
1141
+ return undefined;
1142
+ }
1143
+ }
1102
1144
  export async function executeOrGenerate(code, description, options, toolName,
1103
1145
  /**
1104
1146
  * Values too large to live inside the generated code. Written to
@@ -1788,6 +1830,17 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1788
1830
  const code = codeGenerator.generateDeform(input);
1789
1831
  return executeOrGenerate(code, `Deform: ${input.action}`, options, 'pinepaper_deform');
1790
1832
  }
1833
+ case 'pinepaper_hand_drawn': {
1834
+ const input = HandDrawnInputSchema.parse(args);
1835
+ const code = codeGenerator.generateHandDrawn(input);
1836
+ return executeOrGenerate(code, `Hand-drawn style: ${input.style ?? input.mode ?? 'sketchy'}`, options, 'pinepaper_hand_drawn');
1837
+ }
1838
+ case 'pinepaper_pen_figure': {
1839
+ const input = PenFigureInputSchema.parse(args);
1840
+ const code = codeGenerator.generatePenFigure(input);
1841
+ const what = input.action === 'list' ? 'List the pen figures' : `Draw ${typeof input.figure === 'string' ? input.figure : 'a figure'} with a pen`;
1842
+ return executeOrGenerate(code, what, options, 'pinepaper_pen_figure');
1843
+ }
1791
1844
  case 'pinepaper_sprite_sheet': {
1792
1845
  const input = SpriteSheetInputSchema.parse(args);
1793
1846
  const code = codeGenerator.generateSpriteSheet(input);
@@ -1856,6 +1909,60 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1856
1909
  const description = `Places ${input.concept} as ${plan.parts.length} parts with ${plan.tracks} keyframe tracks`;
1857
1910
  return executeOrGenerate(code, description, options, 'pinepaper_character');
1858
1911
  }
1912
+ case 'pinepaper_create_character': {
1913
+ const input = CreateCharacterInputSchema.parse(args);
1914
+ // If layered bundle provided, use importLayeredCharacter
1915
+ if (input.bundle) {
1916
+ const bundleInfo = input.bundle.info || input.bundle;
1917
+ const bundleImages = input.bundle.images || {};
1918
+ const code = codeGenerator.generateImportLayeredCharacter({ info: bundleInfo, images: bundleImages });
1919
+ return executeOrGenerate(code, `Import layered character: ${input.name || input.characterId || 'bundle'}`, options, 'pinepaper_create_character');
1920
+ }
1921
+ // If concept/preset matches a planCharacter concept, plan it
1922
+ const conceptName = input.concept || input.preset;
1923
+ if (conceptName) {
1924
+ const formattedConcept = conceptName.startsWith('pp:') ? conceptName : `pp:${conceptName.charAt(0).toUpperCase() + conceptName.slice(1)}`;
1925
+ const plan = planCharacter({
1926
+ concept: formattedConcept,
1927
+ at: { x: input.x, y: input.y },
1928
+ scale: input.scale,
1929
+ });
1930
+ if (plan.ok) {
1931
+ const code = generateCharacterCode(plan, {
1932
+ generateCreateItem: (i) => codeGenerator.generateCreateItem(i),
1933
+ generateKeyframeAnimate: (i) => codeGenerator.generateKeyframeAnimate(i),
1934
+ }, {
1935
+ createItem: (a) => CreateItemInputSchema.parse(a),
1936
+ keyframeAnimate: (a) => KeyframeAnimateInputSchema.parse(a),
1937
+ });
1938
+ return executeOrGenerate(code, `Places character ${formattedConcept}`, options, 'pinepaper_create_character');
1939
+ }
1940
+ }
1941
+ const code = codeGenerator.generateCreateCharacter(input);
1942
+ return executeOrGenerate(code, `Create character: ${input.name || input.characterId || 'Character'}`, options, 'pinepaper_create_character');
1943
+ }
1944
+ case 'pinepaper_character_action': {
1945
+ const input = CharacterActionInputSchema.parse(args);
1946
+ const act = String(input.action || 'walk').toLowerCase();
1947
+ const FACES = ['blink', 'frown', 'smile', 'surprise', 'talk', 'wink'];
1948
+ if (FACES.includes(act)) {
1949
+ return {
1950
+ content: [{
1951
+ type: 'text',
1952
+ text: `"${act}" is a facial expression — use pinepaper_character_pose with { expression: "${act}" }. Actions: walk, run, locomotion, wave, dance, jump, idle`,
1953
+ }],
1954
+ isError: true,
1955
+ };
1956
+ }
1957
+ const code = codeGenerator.generateCharacterAction(input);
1958
+ return executeOrGenerate(code, `Character action: ${act}`, options, 'pinepaper_character_action');
1959
+ }
1960
+ case 'pinepaper_character_pose': {
1961
+ const input = CharacterPoseInputSchema.parse(args);
1962
+ const expr = String(input.expression || 'smile').toLowerCase();
1963
+ const code = codeGenerator.generateCharacterPose(input);
1964
+ return executeOrGenerate(code, `Character pose/expression: ${expr}`, options, 'pinepaper_character_pose');
1965
+ }
1859
1966
  case 'pinepaper_batch_create': {
1860
1967
  const input = BatchCreateInputSchema.parse(args);
1861
1968
  const code = codeGenerator.generateBatchCreate(input);
@@ -2061,6 +2168,44 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
2061
2168
  const input = StyledSceneInputSchema.parse(args);
2062
2169
  return executeOrGenerate(codeGenerator.generateStyledScene(input), `Styled scene${input.style ? `: ${input.style}` : ''}`, options, 'pinepaper_styled_scene');
2063
2170
  }
2171
+ // The cloud's craft tools, shared (craft-tools.ts says what differs locally).
2172
+ case 'pinepaper_draw_weave': {
2173
+ const input = DrawWeaveInputSchema.parse(args);
2174
+ const code = generateDrawWeave(input, await craftFrame(options));
2175
+ return executeOrGenerate(code, `Draw a ${input.weave ?? 'plain'} weave`, options, 'pinepaper_draw_weave');
2176
+ }
2177
+ case 'pinepaper_draw_knots': {
2178
+ const input = DrawKnotsInputSchema.parse(args);
2179
+ if (!input.points?.length) {
2180
+ return {
2181
+ content: [{ type: 'text', text: 'pinepaper_draw_knots: give points — [{x, y}, …], one per knot. The tool places knots where you say and draws nothing without them.' }],
2182
+ isError: true,
2183
+ };
2184
+ }
2185
+ return executeOrGenerate(generateDrawKnots(input), `Draw ${input.points.length} embroidered knot(s)`, options, 'pinepaper_draw_knots');
2186
+ }
2187
+ case 'pinepaper_draw_sashiko': {
2188
+ const input = DrawSashikoInputSchema.parse(args);
2189
+ const code = generateDrawSashiko(input, await craftFrame(options));
2190
+ return executeOrGenerate(code, `Draw ${input.pattern ?? 'seigaiha'} sashiko`, options, 'pinepaper_draw_sashiko');
2191
+ }
2192
+ case 'pinepaper_draw_subdivision': {
2193
+ const input = DrawSubdivisionInputSchema.parse(args);
2194
+ const code = generateDrawSubdivision(input, await craftFrame(options));
2195
+ return executeOrGenerate(code, `Draw a ${input.preset ?? 'mondrian'} subdivision`, options, 'pinepaper_draw_subdivision');
2196
+ }
2197
+ case 'pinepaper_ui_kit': {
2198
+ const input = UiKitInputSchema.parse(args);
2199
+ const problems = uiKitCallProblems(input);
2200
+ const code = problems.length ? null : generateUiKit(input);
2201
+ if (!code) {
2202
+ return {
2203
+ content: [{ type: 'text', text: `pinepaper_ui_kit: ${problems.join(' ') || 'nothing to draw'}` }],
2204
+ isError: true,
2205
+ };
2206
+ }
2207
+ return executeOrGenerate(code, `UI kit: ${input.action}`, options, 'pinepaper_ui_kit');
2208
+ }
2064
2209
  case 'pinepaper_place_on_surface': {
2065
2210
  const input = PlaceOnSurfaceInputSchema.parse(args);
2066
2211
  const code = codeGenerator.generatePlaceOnSurface(input);
@@ -2575,13 +2720,15 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2575
2720
  let svgWarnings = [];
2576
2721
  if (input.url) {
2577
2722
  try {
2578
- const res = await fetch(input.url, { redirect: 'follow' });
2723
+ const res = await safeFetch(input.url);
2579
2724
  if (!res.ok) {
2580
2725
  return errorResult(ErrorCodes.EXECUTION_ERROR, `the server refused ${input.url} — HTTP ${res.status} ${res.statusText || ''}`.trim());
2581
2726
  }
2582
2727
  svgText = await res.text();
2583
2728
  }
2584
2729
  catch (e) {
2730
+ if (e instanceof UnsafeUrlError)
2731
+ return errorResult(ErrorCodes.INVALID_PARAMS, e.message);
2585
2732
  const why = e instanceof Error ? e.message : 'network request failed';
2586
2733
  return errorResult(ErrorCodes.EXECUTION_ERROR, `could not reach ${input.url} — ${why}. This fetch runs in the MCP server rather than the page, so it is not a CSP problem.`);
2587
2734
  }
@@ -2593,7 +2740,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
2593
2740
  }
2594
2741
  const code = codeGenerator.generateImportSVG(svgText, svgText ? undefined : input.url, input.position, input.scale, input.source);
2595
2742
  const description = svgWarnings.length
2596
- ? `Imports SVG onto the canvas (${svgWarnings.length} unreachable image(s) removed)`
2743
+ ? `Imports SVG onto the canvas (${svgWarnings.length} unreachable image(s) removed: ${svgWarnings.map((w) => w.message).join('; ')})`
2597
2744
  : 'Imports SVG onto the canvas';
2598
2745
  return executeOrGenerate(code, description, options, 'pinepaper_import_svg');
2599
2746
  }
@@ -3161,9 +3308,11 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
3161
3308
  // third-party host under the studio's CSP and this process can.
3162
3309
  let res;
3163
3310
  try {
3164
- res = await fetch(input.url, { redirect: 'follow' });
3311
+ res = await safeFetch(input.url);
3165
3312
  }
3166
3313
  catch (e) {
3314
+ if (e instanceof UnsafeUrlError)
3315
+ return errorResult(ErrorCodes.INVALID_PARAMS, e.message);
3167
3316
  const why = e instanceof Error ? e.message : 'network request failed';
3168
3317
  return errorResult(ErrorCodes.EXECUTION_ERROR, `could not reach ${input.url} — ${why}. This fetch runs in the MCP server rather than the page, so it is not a CSP problem.`);
3169
3318
  }
@@ -4054,6 +4203,8 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
4054
4203
  'pinepaper_query_capabilities',
4055
4204
  // Deformation, sprite sheets, interaction
4056
4205
  'pinepaper_deform',
4206
+ 'pinepaper_hand_drawn',
4207
+ 'pinepaper_pen_figure',
4057
4208
  'pinepaper_sprite_sheet',
4058
4209
  'pinepaper_interaction',
4059
4210
  // Widget export