@pinepaper.studio/mcp-server 1.6.16 → 1.6.18

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 (90) 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 +142 -4
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/browser/puppeteer-controller.d.ts +61 -1
  11. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  12. package/dist/browser/puppeteer-controller.js +168 -6
  13. package/dist/browser/puppeteer-controller.js.map +1 -1
  14. package/dist/build-info.json +2 -2
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +75 -10
  18. package/dist/index.js.map +1 -1
  19. package/dist/ontology/ontology-validator.d.ts.map +1 -1
  20. package/dist/ontology/ontology-validator.js +9 -5
  21. package/dist/ontology/ontology-validator.js.map +1 -1
  22. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  23. package/dist/ontology/upstream-vocabulary.js +2 -2
  24. package/dist/tools/definitions.d.ts.map +1 -1
  25. package/dist/tools/definitions.js +198 -47
  26. package/dist/tools/definitions.js.map +1 -1
  27. package/dist/tools/engine-lists.d.ts +20 -0
  28. package/dist/tools/engine-lists.d.ts.map +1 -0
  29. package/dist/tools/engine-lists.js +86 -0
  30. package/dist/tools/engine-lists.js.map +1 -0
  31. package/dist/tools/engine-methods.d.ts.map +1 -1
  32. package/dist/tools/engine-methods.js +8 -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 +20 -4
  37. package/dist/tools/engine-surface.js.map +1 -1
  38. package/dist/tools/handlers/beats.d.ts +64 -0
  39. package/dist/tools/handlers/beats.d.ts.map +1 -0
  40. package/dist/tools/handlers/beats.js +83 -0
  41. package/dist/tools/handlers/beats.js.map +1 -0
  42. package/dist/tools/handlers/brand-intake.d.ts +95 -0
  43. package/dist/tools/handlers/brand-intake.d.ts.map +1 -0
  44. package/dist/tools/handlers/brand-intake.js +242 -0
  45. package/dist/tools/handlers/brand-intake.js.map +1 -0
  46. package/dist/tools/handlers.d.ts +23 -11
  47. package/dist/tools/handlers.d.ts.map +1 -1
  48. package/dist/tools/handlers.js +265 -14
  49. package/dist/tools/handlers.js.map +1 -1
  50. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  51. package/dist/tools/minimal-descriptions.js +5 -3
  52. package/dist/tools/minimal-descriptions.js.map +1 -1
  53. package/dist/tools/shape-params.d.ts +2 -2
  54. package/dist/tools/shape-params.js +2 -2
  55. package/dist/tools/stick-vocabulary.d.ts +1 -1
  56. package/dist/tools/stick-vocabulary.js +1 -1
  57. package/dist/tools/toolkits.d.ts.map +1 -1
  58. package/dist/tools/toolkits.js +2 -0
  59. package/dist/tools/toolkits.js.map +1 -1
  60. package/dist/types/code-generator.d.ts +63 -2
  61. package/dist/types/code-generator.d.ts.map +1 -1
  62. package/dist/types/code-generator.js +533 -49
  63. package/dist/types/code-generator.js.map +1 -1
  64. package/dist/types/schemas.d.ts +2199 -640
  65. package/dist/types/schemas.d.ts.map +1 -1
  66. package/dist/types/schemas.js +186 -50
  67. package/dist/types/schemas.js.map +1 -1
  68. package/dist/utils/a11y.d.ts +2 -0
  69. package/dist/utils/a11y.d.ts.map +1 -1
  70. package/dist/utils/a11y.js +1 -1
  71. package/dist/utils/a11y.js.map +1 -1
  72. package/dist/utils/beats.d.ts +58 -0
  73. package/dist/utils/beats.d.ts.map +1 -0
  74. package/dist/utils/beats.js +110 -0
  75. package/dist/utils/beats.js.map +1 -0
  76. package/dist/utils/mcp-log.d.ts +16 -0
  77. package/dist/utils/mcp-log.d.ts.map +1 -0
  78. package/dist/utils/mcp-log.js +10 -0
  79. package/dist/utils/mcp-log.js.map +1 -0
  80. package/dist/utils/progress.d.ts +28 -0
  81. package/dist/utils/progress.d.ts.map +1 -0
  82. package/dist/utils/progress.js +24 -0
  83. package/dist/utils/progress.js.map +1 -0
  84. package/dist/utils/svg-size.d.ts +22 -0
  85. package/dist/utils/svg-size.d.ts.map +1 -0
  86. package/dist/utils/svg-size.js +43 -0
  87. package/dist/utils/svg-size.js.map +1 -0
  88. package/dist/vendor/design/design-style.js +1 -1
  89. package/manifest.json +9 -1
  90. package/package.json +7 -4
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/tools/definitions.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AA+B/C,eAAO,MAAM,cAAc,6kbAyK+G,CAAC;AAW3I,eAAO,MAAM,eAAe,EAAE,IAAI,EA2qRjC,CAAC;AAEF;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAE5D;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,MAAM,EAAE,CAEvC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,EAAE,CAO3D;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9D;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,EAAE,CAQrE"}
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/tools/definitions.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAoC/C,eAAO,MAAM,cAAc,6kbAyK+G,CAAC;AAW3I,eAAO,MAAM,eAAe,EAAE,IAAI,EAk0RjC,CAAC;AAEF;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAE5D;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,MAAM,EAAE,CAEvC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,EAAE,CAO3D;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9D;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,EAAE,CAQrE"}
@@ -11,7 +11,8 @@ import { COMPACT_DESCRIPTIONS } from './compact-descriptions.js';
11
11
  import { MINIMAL_DESCRIPTIONS } from './minimal-descriptions.js';
12
12
  import { STICK_GAITS, STICK_POSES, STICK_SEQUENCES, STICK_EXPRESSIONS, STICK_PROPS } from './stick-vocabulary.js';
13
13
  import { GeneratorNameSchema, ItemTypeSchema } from '../types/schemas.js';
14
- import { EffectTypeSchema, SimpleAnimationTypeSchema, EasingSchema, GEOMETRY_OPERATIONS, THREAD_STITCHES, } from '../types/schemas.js';
14
+ import { DESIGN_MEDIA, DESIGN_MEDIA_APPLY } from './engine-lists.js';
15
+ import { EffectTypeSchema, SimpleAnimationTypeSchema, EasingSchema, KEYFRAME_EASINGS, EASING_DESCRIPTIONS, KEYFRAME_EASING_JSON_SCHEMA, GEOMETRY_OPERATIONS, THREAD_STITCHES, } from '../types/schemas.js';
15
16
  /**
16
17
  * Vocabulary lists derived from Zod schemas so tool descriptions stay in
17
18
  * sync automatically. The description-schema-parity test
@@ -25,6 +26,7 @@ const SIMPLE_ANIMATION_TYPES_LIST = SimpleAnimationTypeSchema.options.join(', ')
25
26
  // easeInCubic / easeOutCubic / easeOutBounce, which keyframe_animate rejects
26
27
  // (round 8 DD, 1.68) — derived now, so it cannot say what the schema refuses.
27
28
  const KEYFRAME_EASINGS_LIST = EasingSchema.options.join(', ');
29
+ const EASING_LINES = KEYFRAME_EASINGS.map((e) => `- ${e}: ${EASING_DESCRIPTIONS[e]}`).join('\n');
28
30
  // =============================================================================
29
31
  // AI AGENT GUIDE (exported for pinepaper_tool_guide)
30
32
  // =============================================================================
@@ -261,12 +263,12 @@ COMMON PRESETS:
261
263
  - hd-landscape: 1920x1080
262
264
  - hd-portrait: 1080x1920
263
265
 
264
- Max canvas size: 8192x8192 (a 5400x1350 carousel panorama fits). For wedding invitations, event cards, or detailed designs, use at least 1080x1080 or larger.`,
266
+ Max canvas size: 16384 per side (a 12000 px DOOH or projection canvas fits); above 8192 Safari / iOS may not render it, Chrome and Firefox do. For wedding invitations, event cards, or detailed designs, use at least 1080x1080 or larger.`,
265
267
  inputSchema: {
266
268
  type: 'object',
267
269
  properties: {
268
- width: { type: 'number', description: 'Canvas width in pixels (100-8192)' },
269
- height: { type: 'number', description: 'Canvas height in pixels (100-8192)' },
270
+ width: { type: 'number', description: 'Canvas width in pixels (100-16384)' },
271
+ height: { type: 'number', description: 'Canvas height in pixels (100-16384)' },
270
272
  preset: {
271
273
  type: 'string',
272
274
  description: 'Optional preset name',
@@ -732,6 +734,8 @@ Use the collage group's id for palette/collageStyle; use a child letter's id for
732
734
  isDecorative: { type: 'boolean', description: 'Mark as decorative/non-interactive' },
733
735
  },
734
736
  },
737
+ atTime: { type: 'number', minimum: 0, description: 'AUTO-KEY: instead of a static edit, write these properties as keyframes at this time (seconds) — the same as a user dragging with auto-key on. Eased from the previous value; other keys and other properties\' curves are kept. "At 4 s make it orange" is { properties: { fillColor: "orange" }, atTime: 4 }.' },
738
+ easing: { type: 'string', description: 'atTime: easing of the segment arriving at the new key (default linear).' },
735
739
  },
736
740
  required: ['itemId', 'properties'],
737
741
  },
@@ -1152,6 +1156,8 @@ EXAMPLES:
1152
1156
  type: 'number',
1153
1157
  description: 'Maximum height constraint in pixels',
1154
1158
  },
1159
+ maxEdge: { anyOf: [{ type: 'integer', minimum: 1 }, { type: 'string', enum: ['native'] }], description: "The longest edge the studio STORES the image at (px); 'native' keeps every pixel. Default: the studio's cap, which follows the canvas on desktop. The result says when the image was stored smaller than its source." },
1160
+ nativeSize: { type: 'boolean', description: 'Place the image at its source footprint even if it was stored smaller.' },
1155
1161
  mask: {
1156
1162
  type: 'string',
1157
1163
  enum: ['circle', 'rounded', 'hexagon', 'star'],
@@ -1729,6 +1735,49 @@ EXAMPLES:
1729
1735
  required: ['action'],
1730
1736
  },
1731
1737
  },
1738
+ {
1739
+ name: 'pinepaper_beat_cuts',
1740
+ annotations: { title: 'Beat-Snapped Cuts', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1741
+ description: `Cut on the music: analyse a track's beats (or take beat times you already have), fit a beat grid, and return cut points on every beat, every bar, or every N beats — optionally splitting a video or audio clip at each one.
1742
+
1743
+ The song is the backbone of the edit: when the music is generated, cut the picture to its beats and bars and use the whole song. Do not trim the song to fit shots that were planned first.
1744
+
1745
+ - source: the music — an upload id, URL, data: URL or local file path. Or beats: [seconds] to skip the analysis.
1746
+ - every: 'beat' (default) | 'bar' | N beats. beatsPerBar (default 4). range: [from, to] seconds.
1747
+ - bpm: the tempo, if you know it (the grid's phase is still fitted to the music). Otherwise the studio's detection and its usual confusions (double, half, 3:4, 4:3 …) are scored against the music and the best is used; bpmHint: [min, max] keeps the choice in a range. The result lists the candidates, and says lowConfidence with a warning when the tempo is uncertain — pass bpm then.
1748
+ - clipId: split this clip at each cut, in order; cuts outside the clip are skipped and listed.
1749
+
1750
+ Returns cuts (seconds), the grid (bpm, phase, period, confidence) and, with clipId, the pieces' ids.`,
1751
+ inputSchema: {
1752
+ type: 'object',
1753
+ properties: {
1754
+ source: { type: 'string' },
1755
+ beats: { type: 'array', items: { type: 'number' } },
1756
+ bpm: { type: 'number' },
1757
+ bpmHint: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2, description: '[min, max] bpm to choose within.' },
1758
+ includeOnsets: { type: 'boolean', description: 'Also return the detected onsets (for diagnosing a tempo pick).' },
1759
+ every: { anyOf: [{ type: 'string', enum: ['beat', 'bar'] }, { type: 'integer', minimum: 1, maximum: 64 }] },
1760
+ beatsPerBar: { type: 'integer', minimum: 1, maximum: 16 },
1761
+ range: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2 },
1762
+ clipId: { type: 'string' },
1763
+ },
1764
+ },
1765
+ },
1766
+ {
1767
+ name: 'pinepaper_query_mutations',
1768
+ annotations: { title: 'Query Property Keyframes', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1769
+ description: `A property's values over time: the item's keyframe series ({t, frame, value, easing} per key), for one property or all, optionally within [from, to] seconds. The read side of modify_item { atTime } (auto-key): "what changed at 4 s?", "show the colour over time". It reads the item's keyframe track; it does not yet say WHO made each change or the value before it — that needs the studio's mutation log.`,
1770
+ inputSchema: {
1771
+ type: 'object',
1772
+ properties: {
1773
+ itemId: { type: 'string' },
1774
+ property: { type: 'string', description: 'One property; all animated properties when omitted.' },
1775
+ range: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2, description: '[from, to] seconds.' },
1776
+ fps: { type: 'number', description: 'For the frame numbers (default 30).' },
1777
+ },
1778
+ required: ['itemId'],
1779
+ },
1780
+ },
1732
1781
  {
1733
1782
  name: 'pinepaper_accessibility_check',
1734
1783
  annotations: {
@@ -1750,7 +1799,7 @@ EXAMPLE: { checks: ["contrast", "flash"], duration: 6, fps: 20 }`,
1750
1799
  type: 'object',
1751
1800
  properties: {
1752
1801
  checks: { type: 'array', items: { type: 'string', enum: ['contrast', 'flash'] }, description: 'Which checks (default both).' },
1753
- duration: { type: 'number', exclusiveMinimum: 0, maximum: 120, description: 'flash: seconds of the timeline to sample (default 5).' },
1802
+ duration: { type: 'number', exclusiveMinimum: 0, maximum: 120, description: 'Seconds of the timeline to sample (default 5): flash samples it at fps; contrast at 0 s, inside each text lifetime, at keyframe times and every second.' },
1754
1803
  fps: { type: 'integer', minimum: 10, maximum: 60, description: 'flash: samples per second (default 20).' },
1755
1804
  },
1756
1805
  },
@@ -1760,6 +1809,12 @@ EXAMPLE: { checks: ["contrast", "flash"], duration: 6, fps: 20 }`,
1760
1809
  annotations: { title: 'Generate Image (cloud)', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
1761
1810
  description: `Generate images with an AI model in the PinePaper cloud, and optionally place the first one on the canvas. CHARGED per output — price it first with pinepaper_generate_estimate (free).
1762
1811
 
1812
+ PLAN FIRST; GENERATE ONLY WHAT THE EDIT USES. Every output is paid in full whether or not it is used, so generate-then-trim is waste:
1813
+ - Plan the whole timeline (shots, lengths, roles) before the first paid call, and price the whole plan with pinepaper_generate_estimate.
1814
+ - Reuse before generating: an image you already generated is referenced by its ref "/scene-assets/<id>".
1815
+ - Video: choose each shot's length from the lengths the model allows (listModels; Veo takes 4, 6 or 8 s) and re-time the edit to them. Do not generate long and crop.
1816
+ - Music: the song is the backbone. Its length only loosely follows the request, so cut the picture to its beats and bars with pinepaper_beat_cuts and use the whole song, rather than trimming it to fit the picture.
1817
+
1763
1818
  Needs a service-account key in PINEPAPER_API_KEY (pp_sa_…, render:create scope). It is an internal beta enabled per organisation: without it the result says "not enabled for your organisation", and nothing is charged. Every refusal says whether anything was charged.
1764
1819
 
1765
1820
  - model or useCase: a model id from pinepaper_generate_estimate {listModels: true}, or a use case and the cloud picks its default (hero stills, plates, text in the image, character-consistent edits).
@@ -1802,7 +1857,7 @@ The result: assets [{assetId, ref, width, height}] — the sizes the files HAVE
1802
1857
  {
1803
1858
  name: 'pinepaper_generate_estimate',
1804
1859
  annotations: { title: 'Estimate Generation (free)', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1805
- description: `Free: price a pinepaper_generate call before making it, or list the models. { listModels: true } returns every model you may use with its price per output, use cases, licence notes and the defaults per use case; { model | useCase, input } returns the estimate for that request. Needs PINEPAPER_API_KEY, like pinepaper_generate.`,
1860
+ description: `Free: price a pinepaper_generate call before making it, or list the models. Estimate the WHOLE plan (every asset the edit needs, at the lengths it will use) before generating any of it, not call by call. { listModels: true } returns every model you may use with its price per output, use cases, licence notes and the defaults per use case; { model | useCase, input } returns the estimate for that request. Needs PINEPAPER_API_KEY, like pinepaper_generate.`,
1806
1861
  inputSchema: {
1807
1862
  type: 'object',
1808
1863
  properties: {
@@ -2090,6 +2145,13 @@ By default returns a cheap per-frame hash + byte size (token-light), plus unique
2090
2145
 
2091
2146
  Set includeDataUrls: true to also get each frame as a PNG data URL (large — only for a few frames).
2092
2147
 
2148
+ LOOK AT YOUR OWN FILM — sheet: one tiled image of many frames, saved to a file (filePath), plus a review rubric to score it against. Structure checks cannot see what a frame looks like; this can. Pick the frames with times (e.g. the music's beat times), or:
2149
+ - sheet.every + sheet.duration: one frame every N seconds.
2150
+ - sheet.strip {at, count?, step?}: consecutive frames around a moment (a transition, a stutter).
2151
+ - sheet.loopSeam + sheet.duration: the last frame beside the first.
2152
+ tileWidth defaults to 360 px, phone width: if a word cannot be read in its tile, it cannot be read on a phone. Then open the file, score review.criteria 1-10, and go through review.checklist.
2153
+ EXAMPLE: { sheet: { every: 1, duration: 15 } } → one ~15-tile JPEG.
2154
+
2093
2155
  USE WHEN:
2094
2156
  - Verifying an animation is deterministic / reproducible
2095
2157
  - Checking whether a scene actually animates across a time range
@@ -2112,8 +2174,26 @@ EXAMPLE: { times: [0, 0.5, 1, 1.5, 2], seed: 42 }`,
2112
2174
  type: 'boolean',
2113
2175
  description: 'Include each frame as a PNG data URL (large). Default false → hashes only.',
2114
2176
  },
2177
+ sheet: {
2178
+ type: 'object',
2179
+ description: 'Return ONE tiled image saved to a file, plus a critique rubric, instead of per-frame hashes. times is then optional.',
2180
+ properties: {
2181
+ every: { type: 'number', description: 'One frame every N seconds from 0 to duration.' },
2182
+ duration: { type: 'number', description: 'Scene length in seconds (for every and loopSeam).' },
2183
+ strip: {
2184
+ type: 'object',
2185
+ properties: { at: { type: 'number' }, count: { type: 'integer', minimum: 2, maximum: 24 }, step: { type: 'number' } },
2186
+ required: ['at'],
2187
+ description: 'Consecutive frames around a moment: count (default 8), step seconds apart (default 1/30).',
2188
+ },
2189
+ loopSeam: { type: 'boolean', description: 'The last frame beside the first.' },
2190
+ tileWidth: { type: 'integer', minimum: 120, maximum: 1080, description: 'Tile width in px (default 360, phone width).' },
2191
+ columns: { type: 'integer', minimum: 1, maximum: 12 },
2192
+ format: { type: 'string', enum: ['jpeg', 'png'], description: 'Default jpeg (small).' },
2193
+ labels: { type: 'boolean', description: 'Stamp each tile with its time (default true).' },
2194
+ },
2195
+ },
2115
2196
  },
2116
- required: ['times'],
2117
2197
  },
2118
2198
  },
2119
2199
  {
@@ -2128,7 +2208,7 @@ EXAMPLE: { times: [0, 0.5, 1, 1.5, 2], seed: 42 }`,
2128
2208
  description: `Bring VIDEO and AUDIO onto the canvas from a URL, and control playback. Uploaded video is a first-class canvas item (drag, scale, animate, export like any shape); audio rides the timeline.
2129
2209
 
2130
2210
  ACTIONS:
2131
- - upload_video: { url (required), position?, scale?, timeOffset? (timeline start, s) or atPlayhead?: true (start at the current playback time), clipInPoint?, clipOutPoint? (trim, s) } → { id, duration, width, height, name }
2211
+ - upload_video: { url (required), position?, fit? ('contain' default | 'cover' | 'none' = source pixels), scale? (a multiplier on that fit — with fit 'none', on the source pixels), timeOffset? (timeline start, s) or atPlayhead?: true (start at the current playback time), clipInPoint?, clipOutPoint? (trim, s) } → { id, duration, width, height, name }
2132
2212
  - upload_audio: { url (required), volume? (0–1, default 1), loop?, muted?, timeOffset? or atPlayhead? } → { id, registryId, duration, name }. Pass loop EXPLICITLY — loop:true for a music bed, loop:false for a voice-over or sting; when omitted, the studio's own default applies and differs between builds.
2133
2213
  - list: → all media [{ id, kind, duration, … }]
2134
2214
  - remove: { id } → removed boolean
@@ -2149,11 +2229,15 @@ VIDEO EDITING:
2149
2229
  - apply_track_matte: { id, matteItemId, channel? (luma|alpha|red), invert?, strength?, hideMatte?, live? } — the matte item's pixels drive the target's alpha. The classic: a headline filled with footage. live: true keeps the cut tracking an ANIMATING matte (kinetic mask reveal); otherwise it bakes once, destructively (recorded in data.filterChain for round-trip).
2150
2230
  - stop_live_matte: { id } — stop a live matte, keep the last cut.
2151
2231
 
2232
+ MAKE ONE LIKE THIS — a reference video's FORMAT, never its content:
2233
+ - analyze_reference: { id, thumbnails? } — the uploaded reference clip (upload_video it first) → cuts (to the frame), shots (length, colours), pacing, beat, and card: the same facts as plain text. Read the card before planning.
2234
+ - remake_from_reference: { texts, duration?, clear? } — builds a new piece on the last analysed reference: one card per shot, cut at its cut times, in each shot's colours, the headline popping on its beats. texts are YOUR words, one per shot. Nothing of the footage, its words or its sound is copied; the reference clip stays in the project muted. One undo step.
2235
+
2152
2236
  EXAMPLE: { action: 'upload_video', url: 'https://…/clip.mp4', scale: 0.5, timeOffset: 1 }`,
2153
2237
  inputSchema: {
2154
2238
  type: 'object',
2155
2239
  properties: {
2156
- action: { type: 'string', enum: ['upload_video', 'upload_audio', 'list', 'remove', 'set_playback_rate', 'set_clip', 'split', 'add_transition', 'set_time_remap', 'speed_ramp', 'match_cut', 'apply_track_matte', 'stop_live_matte'], description: "Media action" },
2240
+ action: { type: 'string', enum: ['upload_video', 'upload_audio', 'list', 'remove', 'set_playback_rate', 'set_clip', 'split', 'add_transition', 'set_time_remap', 'speed_ramp', 'match_cut', 'apply_track_matte', 'stop_live_matte', 'analyze_reference', 'remake_from_reference'], description: "Media action" },
2157
2241
  transition: { type: 'object', properties: { type: { type: 'string', enum: ['crossfade', 'dip'] }, seconds: { type: 'number', exclusiveMinimum: 0, maximum: 10 }, color: { type: 'string' } }, description: "add_transition: { type ('crossfade' default | 'dip'), seconds, color (dip) }." },
2158
2242
  atPlayhead: { type: 'boolean', description: 'upload_video / upload_audio: start at the current playback time (instead of timeOffset).' },
2159
2243
  remapTrack: { type: 'array', items: { type: 'object', properties: { time: { type: 'number' }, value: { type: 'number' }, easing: { type: 'string' } }, required: ['time', 'value'] }, description: 'set_time_remap: canvas-seconds → source-seconds curve (≥2 points). null clears the remap.' },
@@ -2173,12 +2257,17 @@ EXAMPLE: { action: 'upload_video', url: 'https://…/clip.mp4', scale: 0.5, time
2173
2257
  hideMatte: { type: 'boolean', description: 'apply_track_matte: hide the matte item after applying.' },
2174
2258
  live: { type: 'boolean', description: 'apply_track_matte: keep re-cutting as the matte moves/animates — the kinetic mask reveal. Omitted = bake once, destructively.' },
2175
2259
  url: { type: 'string', description: "Required for upload_video / upload_audio: an http(s) URL, a data: URL, or a PATH TO A LOCAL FILE. Remote URLs and local files are fetched/read by the server (the page may not fetch third-party hosts), so stock music and SFX links work; the HTTP status or content-type is named if one does not. Up to 48 MB." },
2176
- id: { type: 'string', description: 'Media id — required for remove / set_playback_rate / set_clip.' },
2260
+ id: { type: 'string', description: 'Media id — required for remove / set_playback_rate / set_clip / analyze_reference.' },
2261
+ texts: { type: 'array', items: { type: 'string' }, description: 'remake_from_reference: your words, one per shot of the reference.' },
2262
+ duration: { type: 'number', description: 'remake_from_reference: total seconds (default the reference\'s own).' },
2263
+ clear: { type: 'boolean', description: 'remake_from_reference: empty the canvas first (media survives).' },
2264
+ thumbnails: { type: 'boolean', description: 'analyze_reference: include a small image per shot (default false; token-heavy).' },
2177
2265
  rate: { type: 'number', description: 'Playback rate 0.25–4 — set_playback_rate.' },
2178
2266
  inPoint: { type: 'number', description: 'Clip in-point (media-time s) — set_clip.' },
2179
2267
  outPoint: { type: 'number', description: 'Clip out-point (media-time s, > inPoint) — set_clip.' },
2180
2268
  position: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, description: 'Canvas position — upload_video.' },
2181
- scale: { type: 'number', description: 'Scale factor — upload_video.' },
2269
+ scale: { type: 'number', description: "upload_video: a multiplier on the fit (contain by default), not on the source pixels; with fit: 'none', of the source pixels." },
2270
+ fit: { type: 'string', enum: ['contain', 'cover', 'none'], description: "upload_video: size before scale — 'contain' (default), 'cover', or 'none' (source pixels)." },
2182
2271
  timeOffset: { type: 'number', description: 'Timeline start offset (s) — upload_video / upload_audio; set_clip: move the clip to start here.' },
2183
2272
  clipInPoint: { type: 'number', description: 'Trim in-point (s) — upload_video.' },
2184
2273
  clipOutPoint: { type: 'number', description: 'Trim out-point (s) — upload_video.' },
@@ -2263,9 +2352,10 @@ READ THIS BEFORE PROMISING A MEDIUM. Naming a medium does not create its marks.
2263
2352
  Call resolve before telling a user you will render in a medium. "yes, but as an impression" is the true answer for half of them, and a flat approximation misrepresents a medium rather than approximating it.
2264
2353
 
2265
2354
  ACTIONS:
2355
+ - apply: { itemId, medium, options? } — render an item in ANY medium the engine can make: ${Object.keys(DESIGN_MEDIA_APPLY).join(', ')}. One engine entry point; a medium it cannot make (encaustic, absent here) or that is a brush rather than a fill (charcoal, ink) is refused with the reason: pass it on. watercolor options: color, brush, water 0..1, opacity, edge 0..1 (the dark rim), angle, bleed, seed. oil (stylised): color, brush, length, density, toneRange, relief, angle, seed. cutPaper (vector: survives SVG/PDF/video): color, facet, jitter, shadow, shadowColor, shadowOpacity, shadowOffset [dx, dy], seed. The result replaces the source in the stacking order and hides it; applying again replaces; one undo step.
2266
2356
  - list_media: every medium with fidelity, definition and limitation. Read it rather than assuming a fixed set — the catalogue grows (hatch and watercolor are recent additions).
2267
2357
  - resolve: { medium } → { ok, fidelity, reason, markMaker }. ok:false with a reason is a real answer — pass it on rather than trying anyway.
2268
- - list_stitches: the 4 thread stitches with their params.
2358
+ - list_stitches: the thread stitches with their params.
2269
2359
  - apply_thread: render an item in thread. Its own silhouette is the region and its own fill is the thread colour, so an existing drawing becomes stitched rather than needing to be redrawn. The source is HIDDEN, not destroyed.
2270
2360
  - apply_hatch: rule an item with hatching. Same deal — silhouette is the region, source is hidden, re-hatching replaces.
2271
2361
  - list_flow_fields: the 7 fields that bend a hatch line.
@@ -2295,11 +2385,12 @@ EXAMPLE — a stitched leaf:
2295
2385
  inputSchema: {
2296
2386
  type: 'object',
2297
2387
  properties: {
2298
- action: { type: 'string', enum: ['list_media', 'resolve', 'list_stitches', 'apply_thread', 'apply_hatch', 'list_flow_fields', 'list_hatch_options'], description: 'Medium operation' },
2299
- medium: { type: 'string', description: 'resolve: vector | thread | ink | watercolor | hatch | cutPaper | charcoal | oil | encaustic. Six are native (vector, thread, ink, watercolor, hatch, cutPaper), two stylised, one absent — call list_media for the live table rather than trusting this line.' },
2300
- itemId: { type: 'string', description: 'apply_thread / apply_hatch: a closed path, compound path, or a group of them.' },
2388
+ action: { type: 'string', enum: ['list_media', 'resolve', 'list_stitches', 'apply_thread', 'apply_hatch', 'list_flow_fields', 'list_hatch_options', 'apply'], description: 'Medium operation' },
2389
+ medium: { type: 'string', enum: [...DESIGN_MEDIA], description: `resolve / apply: the medium. apply works for ${Object.keys(DESIGN_MEDIA_APPLY).join(', ')}; the rest are refused by the engine with the reason. Call list_media for each one's fidelity and limitation.` },
2390
+ options: { type: 'object', description: 'apply: the medium\'s own options (see the description; color takes hex).' },
2391
+ itemId: { type: 'string', description: 'apply / apply_thread / apply_hatch: a closed path, compound path (holes kept), or a group of them (applied per path).' },
2301
2392
  stitch: { type: 'string', enum: [...THREAD_STITCHES], description: "apply_thread: default longAndShort. These six are the stitches the engine publishes; call 'list_stitches' for each one's description and its own parameters. An unknown name is refused rather than quietly stitched as a default fill." },
2302
- field: { type: 'object', description: "apply_thread: { kind: 'radial'|'spine'|'constant', cx, cy, angle, spine[], across } — default radial from the shape centre." },
2393
+ field: { type: 'object', description: "apply_thread: { kind: 'radial'|'spine'|'constant', cx, cy, angle, spine[], across } — default radial from the shape centre. spine is a list of points, each {x, y} or [x, y], e.g. [{x:400,y:200},{x:400,y:520}]; this tool normalises [x, y] to {x, y} before the engine sees it." },
2303
2394
  stitchLen: { type: 'number', description: 'apply_thread: nominal stitch length px (default 18).' },
2304
2395
  rowGap: { type: 'number', description: 'apply_thread: row spacing px; defaults from thread width so rows abut.' },
2305
2396
  variance: { type: 'number', description: 'apply_thread: length jitter 0..1 (default 0.35) — the long-and-short shading.' },
@@ -2548,6 +2639,13 @@ RECIPE — a playable maze: create_tilemap with wall fills → batch_create colo
2548
2639
  },
2549
2640
  {
2550
2641
  name: 'pinepaper_design_system',
2642
+ annotations: {
2643
+ title: 'Design System',
2644
+ readOnlyHint: false,
2645
+ destructiveHint: false,
2646
+ idempotentHint: false,
2647
+ openWorldHint: false,
2648
+ },
2551
2649
  description: `The design vocabulary, as data you can read — and eighteen styles that can build a scene.
2552
2650
 
2553
2651
  TWO DIFFERENT THINGS, kept apart on purpose:
@@ -2584,6 +2682,13 @@ A partial composition reports as a failure with the elements that did not create
2584
2682
  },
2585
2683
  {
2586
2684
  name: 'pinepaper_stick',
2685
+ annotations: {
2686
+ title: 'Stick Figure Kit',
2687
+ readOnlyHint: false,
2688
+ destructiveHint: false,
2689
+ idempotentHint: false,
2690
+ openWorldHint: false,
2691
+ },
2587
2692
  description: `The vendored stick-figure kit — a rigged figure posed, walking, travelling, holding a prop, with a garment and hair, plus the set it stands on and among.
2588
2693
 
2589
2694
  A different construction from pinepaper_character, which places a figure from the DESIGN GRAPH by concept ("pp:Pigeon"). This is the stick kit specifically.
@@ -2649,6 +2754,13 @@ The geometry is vendored from mcp-cloud, which makes this a three-repo artifact:
2649
2754
  },
2650
2755
  {
2651
2756
  name: 'pinepaper_story',
2757
+ annotations: {
2758
+ title: 'Story to Scene',
2759
+ readOnlyHint: false,
2760
+ destructiveHint: false,
2761
+ idempotentHint: false,
2762
+ openWorldHint: false,
2763
+ },
2652
2764
  description: `A piece of prose becomes a scene.
2653
2765
 
2654
2766
  - distill: reduce an article to its story beats and draw NOTHING. This is the half worth having on its own — a caller can read what the distiller made of the text, edit the beats, and only then assemble. Discovering the reading by looking at a finished scene is the expensive way round.
@@ -2669,6 +2781,13 @@ The geometry is vendored from mcp-cloud, which makes this a three-repo artifact:
2669
2781
  },
2670
2782
  {
2671
2783
  name: 'pinepaper_interchange',
2784
+ annotations: {
2785
+ title: 'Interchange Formats',
2786
+ readOnlyHint: false,
2787
+ destructiveHint: false,
2788
+ idempotentHint: false,
2789
+ openWorldHint: false,
2790
+ },
2672
2791
  description: `The formats other tools read and write — Lottie, dotLottie, GLB, BVH, PNG sequence.
2673
2792
 
2674
2793
  pinepaper_agent_export covers the PLATFORM formats: png, svg, mp4, webm, gif, pdf, sized for Instagram or YouTube. These are the interchange ones — a Lottie an app plays, a GLB a 3D tool opens, a BVH a rig imports, a PNG sequence an editor ingests. Import is here too, because a format you can only write is half a bridge.
@@ -2687,6 +2806,13 @@ Three of these refuse quietly in the engine: exportGLB with no perspective objec
2687
2806
  },
2688
2807
  {
2689
2808
  name: 'pinepaper_sound',
2809
+ annotations: {
2810
+ title: 'Sound Synthesis & Soundtrack',
2811
+ readOnlyHint: false,
2812
+ destructiveHint: false,
2813
+ idempotentHint: false,
2814
+ openWorldHint: false,
2815
+ },
2690
2816
  description: `Synthesis — and the drawing that is the same object.
2691
2817
 
2692
2818
  The engine carries a full audio graph, and none of it was reachable. pinepaper_audio_beats ANALYSES audio that already exists; this MAKES audio.
@@ -2720,6 +2846,7 @@ render_soundtrack mixes every placed sound to a WAV and writes it to a file. It
2720
2846
  noiseFreq: { type: 'number', description: 'define_percussion / define_sfx: noise band centre in Hz.' },
2721
2847
  hz: { type: 'number', description: 'define_percussion / define_sfx: base frequency in Hz.' },
2722
2848
  pitch: { type: 'object', description: 'define_percussion / define_sfx: { from, tau } pitch glide. The glide IS the drum — omit it and a kick comes out a beep.' },
2849
+ duration: { type: 'number', description: 'render_soundtrack: seconds to render (options.duration also works; this wins if both are given). Without either, the studio uses the timeline length (3 s when there is none).' },
2723
2850
  sampleRate: { type: 'number', description: 'render_soundtrack: samples per second (default 48000).' },
2724
2851
  bitDepth: { type: 'number', enum: [16, 32], description: 'render_soundtrack: 16 (default) or 32-bit float.' },
2725
2852
  note: { type: 'string', description: "play_tone: scientific pitch, e.g. 'A4'." },
@@ -2739,6 +2866,13 @@ render_soundtrack mixes every placed sound to a WAV and writes it to a file. It
2739
2866
  },
2740
2867
  {
2741
2868
  name: 'pinepaper_motion',
2869
+ annotations: {
2870
+ title: 'Group & Field Motion',
2871
+ readOnlyHint: false,
2872
+ destructiveHint: false,
2873
+ idempotentHint: false,
2874
+ openWorldHint: false,
2875
+ },
2742
2876
  description: `The generators' Animation knob, pointed at anything — the same motion engine every generator's own animation runs on, reachable for any group or list of items.
2743
2877
 
2744
2878
  Two kinds, and the second is the one nothing else here can do:
@@ -2770,6 +2904,13 @@ This is not pinepaper_animate: that applies a loop preset to ONE item. This move
2770
2904
  },
2771
2905
  {
2772
2906
  name: 'pinepaper_path',
2907
+ annotations: {
2908
+ title: 'Path Operations',
2909
+ readOnlyHint: false,
2910
+ destructiveHint: true,
2911
+ idempotentHint: false,
2912
+ openWorldHint: false,
2913
+ },
2773
2914
  description: `Destructive path operations — the ones that REPLACE or ADD items rather than restyling them.
2774
2915
 
2775
2916
  - boolean: unite | subtract | intersect | exclude | divide. CONSUMES its operands and leaves one result, so the input ids stop existing. The first id is the base; the rest apply to it in order.
@@ -2832,7 +2973,7 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2832
2973
  anyOf: [{ type: 'boolean' }, { type: 'object' }],
2833
2974
  description: 'create: include the walkable character (boolean or config).',
2834
2975
  },
2835
- patch: { type: 'object', description: 'configure: partial world spec, validated against describe.' },
2976
+ patch: { type: 'object', description: 'configure: partial world spec, validated against describe. Colours (env.zenith, env.horizon, env.fogColor, env.sunColor, env.ambient, env.lowColor … env.propColorB) take hex ("#03050c") or [r, g, b] in 0..1.' },
2836
2977
  actorId: { type: 'string', description: 'Actor id.' },
2837
2978
  x: { type: 'number', description: 'add_actor: world x.' },
2838
2979
  z: { type: 'number', description: 'add_actor: world z.' },
@@ -2843,18 +2984,18 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2843
2984
  camera: { type: 'object', description: "set_camera: { mode: follow|fixed|orbit, target?, radius?, speed?, eye?, lookAt? }." },
2844
2985
  object: { type: 'object', description: 'add_object: { x, z, height?, color?, y? }. metalness/roughness are accepted and IGNORED here — a plain object is drawn by the prop shader, which declares no such uniform. They render on the mesh path only.' },
2845
2986
  pathId: { type: 'string', description: 'extrude_path / lathe_path: the canvas path to turn into geometry. The path itself is unchanged.' },
2846
- mesh: { type: 'object', description: 'extrude_path / lathe_path options: { id?, depth?, unitsPerPixel?, caps?, segments?, arc?, flatness?, x?, y?, z?, rotY?, rotYDegrees?, scale?, doubleSided?, color?, metalness?, roughness? }. color is hex or [r,g,b] (default light grey — without it the mesh rendered black); it becomes the shader\'s diffuse uniform. arc is DEGREES; rotY is RADIANS (fed straight to cos/sin in the shader) — pass rotYDegrees to write degrees. These names are the ones the engine records as the mesh provenance, so a captured meshProvenance.opts spreads straight back in.' },
2987
+ mesh: { type: 'object', description: 'extrude_path / lathe_path options: { id?, depth? (WORLD units, default 1), unitsPerPixel? (canvas px → world units, default 0.01), upright? (extrude: stand the drawing up, extruded along Z; default flat, a floor plan extruded along Y), placement? (extrude: \'centered\' default: centred on x/z, base at y; \'canvas\': the outline\'s canvas position), caps?, segments?, arc?, flatness?, x?, y?, z?, rotY?, rotYDegrees?, scale?, doubleSided?, color?, metalness?, roughness? }. color is hex or [r,g,b] (default light grey — without it the mesh rendered black); it becomes the shader\'s diffuse uniform. arc is DEGREES; rotY is RADIANS (fed straight to cos/sin in the shader) — pass rotYDegrees to write degrees. These names are the ones the engine records as the mesh provenance, so a captured meshProvenance.opts spreads straight back in.' },
2847
2988
  meshId: { type: 'string', description: 'remove_mesh: the mesh id.' },
2848
- light: { type: 'object', description: 'add_light / set_light: { id?, x?, y?, z?, color?, intensity?, range? }. At most 8 point lights; the ninth is refused by name because the shader array is fixed-size.' },
2989
+ light: { type: 'object', description: 'add_light / set_light: { id?, x?, y?, z?, color? (hex or linear [r, g, b], may exceed 1), intensity?, range? }. At most 8 point lights; the ninth is refused by name because the shader array is fixed-size.' },
2849
2990
  lightId: { type: 'string', description: 'set_light / remove_light: the light id.' },
2850
- material: { type: 'object', description: 'add_material / set_material: { id?, color?, emissive?, metalness?, roughness?, emissiveIntensity?, clearcoat?, clearcoatRoughness?, sheenColor?, sheenRoughness? }. A shared surface — one edit restyles every object using it. metalness/roughness render on the mesh path only.' },
2991
+ material: { type: 'object', description: 'add_material / set_material: colours (color, emissive, sheenColor) take hex or [r, g, b] in 0..1. { id?, color?, emissive?, metalness?, roughness?, emissiveIntensity?, clearcoat?, clearcoatRoughness?, sheenColor?, sheenRoughness? }. A shared surface — one edit restyles every object using it. metalness/roughness render on the mesh path only.' },
2851
2992
  materialId: { type: 'string', description: 'set_material / remove_material: the material id.' },
2852
2993
  source: { type: 'string', description: 'import_obj: the OBJ file TEXT. import_gltf: a URL or a data: URI of the .glb bytes.' },
2853
2994
  importOptions: { type: 'object', description: 'import_obj / import_gltf: placement and scale options, passed through.' },
2854
2995
  clip: { type: 'string', description: 'set_mesh_clip: which imported animation clip a skinned mesh plays.' },
2855
2996
  crossfade: { type: 'number', description: 'set_mesh_clip: seconds to blend out of the previous clip (blended in LOCAL pose space).' },
2856
2997
  navTarget: { type: 'string', enum: ['2d', '3d'], description: "set_nav_target: which layer the pointer drives." },
2857
- point: { type: 'object', description: 'ground_height / canvas_to_ground / world_to_canvas: { x, y, z? }.' },
2998
+ point: { type: 'object', description: 'ground_height: { x, z } in WORLD coordinates (y is read as z if z is absent). canvas_to_ground: { x, y } in canvas pixels. world_to_canvas: { x, y, z? } in world coordinates.' },
2858
2999
  origin: { type: 'array', items: { type: 'number' }, description: 'raycast: ray origin [x, y, z].' },
2859
3000
  direction: { type: 'array', items: { type: 'number' }, description: 'raycast: ray direction [x, y, z], need not be normalised.' },
2860
3001
  multiplier: { type: 'number', description: 'dolly_camera: >1 away, <1 closer.' },
@@ -2877,7 +3018,9 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2877
3018
  annotations: {
2878
3019
  title: 'Character (place and direct a figure from the graph)',
2879
3020
  readOnlyHint: false,
3021
+ destructiveHint: false,
2880
3022
  idempotentHint: false,
3023
+ openWorldHint: false,
2881
3024
  },
2882
3025
  description: `Place a CHARACTER from the design graph and DIRECT it — no geometry, no bones, no poses.
2883
3026
 
@@ -3425,7 +3568,7 @@ applyFunction: \`
3425
3568
  annotations: {
3426
3569
  title: 'Execute Custom Code',
3427
3570
  readOnlyHint: false,
3428
- destructiveHint: false,
3571
+ destructiveHint: true,
3429
3572
  idempotentHint: false,
3430
3573
  openWorldHint: true,
3431
3574
  },
@@ -3599,12 +3742,9 @@ The authoritative list for a given item is pinepaper_get_animatable_properties
3599
3742
 
3600
3743
  EASING — IT SHAPES THE SEGMENT ARRIVING AT THE KEYFRAME IT IS ON.
3601
3744
  The move from keyframe A to keyframe B is eased by B's easing, not A's. Easing on the FIRST keyframe does nothing (no segment arrives at it) — a Ken Burns with easing only on its first key plays linear. Put the easing on the key you are moving TO.
3602
- - linear: Constant speed
3603
- - easeIn: Slow start
3604
- - easeOut: Slow end
3605
- - easeInOut: Slow start and end
3606
- - bounce: Bounce effect
3607
- - elastic: Elastic overshoot
3745
+ ${EASING_LINES}
3746
+ - or a custom cubic-bezier [x1, y1, x2, y2] — x in 0..1, y may exceed 0..1 for overshoot. keyframe_animate and camera_animate only.
3747
+ Every easing renders exactly in video, GIF and APNG. Lottie and CSS get the closest single Bézier: springPlayful loses its wobble, and the in-out quart/quint/expo/circ curves are within 2–4 %. SMIL is exact for springs and those four.
3608
3748
 
3609
3749
  CLIP WINDOW (optional — Premiere/Canva-style clip behavior):
3610
3750
  - timeOffset: Shift the whole clip in canvas time (seconds). The animation only runs while canvas time is in [timeOffset, timeOffset + (clipOutPoint - clipInPoint)].
@@ -3632,9 +3772,8 @@ EXAMPLE — Reveal at 2s, skip first second of keyframe data, end at 4s of keyfr
3632
3772
  additionalProperties: true,
3633
3773
  },
3634
3774
  easing: {
3635
- type: 'string',
3636
- enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'bounce', 'elastic'],
3637
- description: 'Shapes the segment ARRIVING at this keyframe (from the previous one). Ignored on the first keyframe.',
3775
+ ...KEYFRAME_EASING_JSON_SCHEMA,
3776
+ description: 'Shapes the segment ARRIVING at this keyframe (from the previous one). Ignored on the first keyframe. A name, or a custom cubic-bezier [x1, y1, x2, y2] — x in 0..1, y may exceed 0..1 for overshoot.',
3638
3777
  },
3639
3778
  },
3640
3779
  required: ['time', 'properties'],
@@ -3802,7 +3941,7 @@ MASK TYPES: rectangle, circle, ellipse, star, triangle, hexagon, heart, rounded`
3802
3941
  properties: {
3803
3942
  time: { type: 'number', description: 'Normalized time (0-1)' },
3804
3943
  properties: { type: 'object', description: 'Mask properties (x, y, width, height, radius, scale, rotation, opacity)' },
3805
- easing: { type: 'string', enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'bounce', 'elastic'] },
3944
+ easing: { type: 'string', enum: [...KEYFRAME_EASINGS] },
3806
3945
  },
3807
3946
  required: ['time', 'properties'],
3808
3947
  },
@@ -3813,7 +3952,7 @@ MASK TYPES: rectangle, circle, ellipse, star, triangle, hexagon, heart, rounded`
3813
3952
  properties: {
3814
3953
  startTime: { type: 'number', description: 'Start time in seconds', default: 0 },
3815
3954
  duration: { type: 'number', description: 'Duration in seconds', default: 0.8 },
3816
- easing: { type: 'string', enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'bounce'], default: 'easeOut' },
3955
+ easing: { type: 'string', enum: [...KEYFRAME_EASINGS], default: 'easeOut' },
3817
3956
  reversed: { type: 'boolean', description: 'Reverse animation (hide instead of reveal)', default: false },
3818
3957
  loop: { type: 'boolean', description: 'Loop the animation', default: false },
3819
3958
  },
@@ -3930,7 +4069,7 @@ Needs a studio that publishes the item-side table; without one, the tool says so
3930
4069
  idempotentHint: true,
3931
4070
  openWorldHint: false,
3932
4071
  },
3933
- description: `[Utility] Get list of available easing functions for mask animations.`,
4072
+ description: `[Utility] List the easing names the studio accepts: one table serves keyframes, masks, relations and the camera. Reads the live engine's list when it reports one.`,
3934
4073
  inputSchema: {
3935
4074
  type: 'object',
3936
4075
  properties: {},
@@ -4054,8 +4193,8 @@ EXAMPLE (curved arc between waypoints):
4054
4193
  pitch: { type: 'number', description: '3D tilt in degrees — rendered where the studio supports camera tilt (result.tiltRenders).' },
4055
4194
  yaw: { type: 'number', description: '3D rotation in degrees — rendered where the studio supports camera tilt (result.tiltRenders).' },
4056
4195
  easing: {
4057
- type: 'string',
4058
- enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'bounce', 'elastic'],
4196
+ ...KEYFRAME_EASING_JSON_SCHEMA,
4197
+ description: 'A name, or a custom cubic-bezier [x1, y1, x2, y2] — x in 0..1, y may exceed 0..1 for overshoot.',
4059
4198
  },
4060
4199
  pathOut: {
4061
4200
  type: 'array',
@@ -4927,7 +5066,7 @@ flags as a badly-composed scene, and they do not survive an artboard change.`,
4927
5066
  name: 'pinepaper_brand_kit',
4928
5067
  annotations: {
4929
5068
  title: 'Brand Kit', readOnlyHint: false, destructiveHint: false,
4930
- idempotentHint: true, openWorldHint: false,
5069
+ idempotentHint: true, openWorldHint: true,
4931
5070
  },
4932
5071
  description: `Apply a brand kit — colours, fonts and roles — across the scene.
4933
5072
 
@@ -4935,6 +5074,7 @@ ACTIONS:
4935
5074
  - plan: Report what WOULD change (count, per-role breakdown, WCAG contrast audit) WITHOUT touching
4936
5075
  the canvas. Call this first on an unfamiliar scene; a whole-scene recolour is hard to eyeball afterwards.
4937
5076
  - apply: Commit it. A history snapshot is taken FIRST, so one undo puts everything back.
5077
+ - from_url: { url, screenshots? } — read the product's REAL brand from its own site before animating anything: screenshots down the page, the logo (header image or SVG, else the site icon), and a proposed kit from the colours and fonts the site's CSS computes (buttons and links weigh most). Saved to files; the result lists them. Read in an isolated browser context: the studio is not touched. Review the kit against the screenshots, then plan / apply it. Use the brand you are given, never an invented one.
4938
5078
 
4939
5079
  KIT SHAPE — only name and colors.primary are required:
4940
5080
  { "name": "Acme", "colors": { "primary": "#e11d48", "secondary": "#0ea5e9",
@@ -4949,7 +5089,9 @@ Both actions return a WCAG contrast audit, so you can see failing pairs before s
4949
5089
  inputSchema: {
4950
5090
  type: 'object',
4951
5091
  properties: {
4952
- action: { type: 'string', enum: ['plan', 'apply'], description: 'plan (dry run) or apply' },
5092
+ action: { type: 'string', enum: ['plan', 'apply', 'from_url'], description: 'plan (dry run), apply, or from_url (read a site into a proposed kit)' },
5093
+ url: { type: 'string', description: "from_url: the product's site, http(s)." },
5094
+ screenshots: { type: 'integer', minimum: 1, maximum: 6, description: 'from_url: screenshots down the page (default 3).' },
4953
5095
  kit: {
4954
5096
  type: 'object',
4955
5097
  description: 'Brand kit. Requires name and colors.primary; other roles are inferred.',
@@ -4975,7 +5117,7 @@ Both actions return a WCAG contrast audit, so you can see failing pairs before s
4975
5117
  },
4976
5118
  selectionOnly: { type: 'boolean', description: 'Restrict to current selection' },
4977
5119
  },
4978
- required: ['action', 'kit'],
5120
+ required: ['action'],
4979
5121
  },
4980
5122
  },
4981
5123
  {
@@ -6984,7 +7126,7 @@ EXAMPLES:
6984
7126
  annotations: {
6985
7127
  title: 'Execute Generator',
6986
7128
  readOnlyHint: false,
6987
- destructiveHint: false,
7129
+ destructiveHint: true,
6988
7130
  idempotentHint: false,
6989
7131
  openWorldHint: false,
6990
7132
  },
@@ -7825,7 +7967,7 @@ EXAMPLES:
7825
7967
  annotations: {
7826
7968
  title: 'Build Scene — Items, Animations, Effects in One Call',
7827
7969
  readOnlyHint: false,
7828
- destructiveHint: false,
7970
+ destructiveHint: true,
7829
7971
  idempotentHint: false,
7830
7972
  openWorldHint: false,
7831
7973
  },
@@ -7835,7 +7977,7 @@ NOTE: For diagrams (flowcharts, UML), use pinepaper_create_diagram_shape + pinep
7835
7977
 
7836
7978
  ⚠️ EVERY operation executes LIVE on the canvas. Calling this twice DOUBLES all items. Call ONCE per pipeline.
7837
7979
 
7838
- ⚠️ SIZING: All positions/sizes MUST fit within the canvas. start_job returns canvasSize. Keep items within 5%-95% of canvas width/height. Max canvas: 8192x8192.
7980
+ ⚠️ SIZING: All positions/sizes MUST fit within the canvas. start_job returns canvasSize. Keep items within 5%-95% of canvas width/height. Max canvas: 16384 per side (above 8192, Safari / iOS may fail).
7839
7981
 
7840
7982
  ONTOLOGY PREFLIGHT: Before code generation, this tool validates itemType, relationType, effectType, and generatorName against the canonical PinePaper vocabulary. A typo (e.g. "circel") returns a structured VALIDATION_ERROR with the operation index, the offending field, the canonical valid list, and a suggested fix — fix and retry rather than guessing. Pass skipValidation: true to bypass when intentionally using experimental vocabulary.
7841
7983
 
@@ -7922,8 +8064,8 @@ EXAMPLE — Animated sky scene with timed reveals:
7922
8064
  description: 'Operation type',
7923
8065
  },
7924
8066
  // Canvas setup
7925
- width: { type: 'number', description: 'Canvas width for set_canvas_size (100-8192)' },
7926
- height: { type: 'number', description: 'Canvas height for set_canvas_size (100-8192)' },
8067
+ width: { type: 'number', description: 'Canvas width for set_canvas_size (100-16384)' },
8068
+ height: { type: 'number', description: 'Canvas height for set_canvas_size (100-16384)' },
7927
8069
  preset: { type: 'string', description: 'Canvas preset for set_canvas_size' },
7928
8070
  backgroundColor: { type: 'string', description: 'For set_background: hex color' },
7929
8071
  generatorName: {
@@ -8146,7 +8288,16 @@ VERIFY MOTION BEFORE YOU RENDER. An export takes seconds to minutes and shows yo
8146
8288
  alphaQuantizer: { type: 'integer', minimum: 0, maximum: 63, description: 'webm with transparent: alpha-stream quality, 0 = lossless … 63.' },
8147
8289
  loop: { anyOf: [{ type: 'boolean' }, { type: 'integer', minimum: 0, maximum: 1000 }], description: 'gif / apng: true = loop forever, false / 0 / 1 = play once, n = play n times (email clients often want a few plays, not forever). Refused on video formats — see SEAMLESS LOOPS.' },
8148
8290
  broadcast: { type: 'boolean', description: 'mp4 only: broadcast-safe — BT.709, limited range (16-235), tagged bt709, constant bitrate with an 8 Mbps floor at 720p+ (4 below). result.video reports what the encoder did.' },
8291
+ frames: { type: 'integer', minimum: 1, maximum: 36000, description: 'Animated formats: exactly this many frames instead of duration. A duration makes ceil(duration x fps) frames (7.91667 s at 24 fps is 191, not 190), so use frames for frame-exact segments.' },
8149
8292
  deterministic: { type: 'boolean', description: 'mp4 / webm: byte-identical files for the same scene (pins the container timestamps), so exports can be checksummed or cached by hash.' },
8293
+ motionBlur: {
8294
+ type: 'object',
8295
+ description: 'mp4 / webm: real motion blur. Each frame is rendered `subframes` times across the shutter and averaged, so fast movement smears instead of strobing. Costs subframes× the render time: run estimateOnly first (it reports renders and renderCost). The result carries motionBlur (what the exporter applied).',
8296
+ properties: {
8297
+ subframes: { type: 'integer', minimum: 1, maximum: 32, description: 'Renders per frame (default 8; 1 = no blur).' },
8298
+ shutter: { type: 'number', minimum: 0, maximum: 1, description: 'Fraction of the frame interval the shutter is open (default 0.5).' },
8299
+ },
8300
+ },
8150
8301
  broadcastHeadroom: { type: 'integer', minimum: 0, maximum: 40, description: 'mp4 with broadcast only, default 12: luma codes kept clear at both ends, so encoder ringing stays legal (12 measured clean on hard edges; 4 = ~100x fewer out-of-range samples, 12 = ~1000x; never zero). Fidelity raises luma_out_of_range with the legaliser command when any remain.' },
8151
8302
  bitrate: { type: 'integer', minimum: 100000, maximum: 200000000, description: 'mp4 / webm: target bits/s, replacing the quality-derived one. The browser encoder treats it as a CEILING; result.video.achievedBitrate is what it produced.' },
8152
8303
  minBitrate: { type: 'integer', minimum: 100000, maximum: 200000000, description: 'mp4 / webm: a floor; a miss is a fidelity warning naming the re-encode a delivery spec needs.' },
@@ -8172,7 +8323,7 @@ VERIFY MOTION BEFORE YOU RENDER. An export takes seconds to minutes and shows yo
8172
8323
  properties: {
8173
8324
  x: { type: 'number' }, y: { type: 'number' },
8174
8325
  width: { type: 'number', exclusiveMinimum: 0 }, height: { type: 'number', exclusiveMinimum: 0 },
8175
- outputWidth: { type: 'integer', minimum: 1, maximum: 8192 }, outputHeight: { type: 'integer', minimum: 1, maximum: 8192 },
8326
+ outputWidth: { type: 'integer', minimum: 1, maximum: 16384 }, outputHeight: { type: 'integer', minimum: 1, maximum: 16384 },
8176
8327
  excludeForeign: { type: 'boolean', description: 'Default true: an item reaching into the region but centred outside it (a neighbouring card overflow) is left out of this image; result.excludedItems names them. false renders everything that touches the region.' },
8177
8328
  },
8178
8329
  required: ['x', 'y', 'width', 'height'],
@@ -8204,8 +8355,8 @@ VERIFY MOTION BEFORE YOU RENDER. An export takes seconds to minutes and shows yo
8204
8355
  duration: {
8205
8356
  type: 'number',
8206
8357
  minimum: 0.5,
8207
- maximum: 60,
8208
- description: 'Video duration in seconds for mp4/webm/gif (default 5). Max 60 for mp4/webm; GIF is capped at 15 because it has no bitrate target, so size scales with frames × dimensions — use mp4/webm for longer clips. Static formats ignore this. If you set play_timeline to N seconds, pass the same N here.',
8358
+ maximum: 600,
8359
+ description: 'Video duration in seconds for mp4/webm/gif (default 5). Max 600 for mp4/webm (past about a minute the file is paged back from the studio\'s export store); GIF is capped at 15 because it has no bitrate target, so size scales with frames × dimensions — use mp4/webm for longer clips. The file has ceil(duration × fps) frames — for an exact count use frames instead. Static formats ignore this. If you set play_timeline to N seconds, pass the same N here.',
8209
8360
  },
8210
8361
  },
8211
8362
  // Nothing is required: platform defaults to 'auto', which is the canvas's