@pinepaper.studio/mcp-server 1.6.17 → 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 (62) hide show
  1. package/README.md +92 -0
  2. package/dist/browser/puppeteer-controller.d.ts +61 -1
  3. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  4. package/dist/browser/puppeteer-controller.js +168 -6
  5. package/dist/browser/puppeteer-controller.js.map +1 -1
  6. package/dist/build-info.json +2 -2
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +75 -10
  10. package/dist/index.js.map +1 -1
  11. package/dist/ontology/ontology-validator.d.ts.map +1 -1
  12. package/dist/ontology/ontology-validator.js +9 -5
  13. package/dist/ontology/ontology-validator.js.map +1 -1
  14. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  15. package/dist/ontology/upstream-vocabulary.js +2 -2
  16. package/dist/tools/definitions.d.ts.map +1 -1
  17. package/dist/tools/definitions.js +138 -35
  18. package/dist/tools/definitions.js.map +1 -1
  19. package/dist/tools/engine-lists.d.ts +20 -0
  20. package/dist/tools/engine-lists.d.ts.map +1 -0
  21. package/dist/tools/engine-lists.js +86 -0
  22. package/dist/tools/engine-lists.js.map +1 -0
  23. package/dist/tools/engine-methods.d.ts.map +1 -1
  24. package/dist/tools/engine-methods.js +4 -0
  25. package/dist/tools/engine-methods.js.map +1 -1
  26. package/dist/tools/engine-surface.d.ts +1 -1
  27. package/dist/tools/engine-surface.d.ts.map +1 -1
  28. package/dist/tools/engine-surface.js +20 -4
  29. package/dist/tools/engine-surface.js.map +1 -1
  30. package/dist/tools/handlers/brand-intake.d.ts +95 -0
  31. package/dist/tools/handlers/brand-intake.d.ts.map +1 -0
  32. package/dist/tools/handlers/brand-intake.js +242 -0
  33. package/dist/tools/handlers/brand-intake.js.map +1 -0
  34. package/dist/tools/handlers.d.ts +23 -11
  35. package/dist/tools/handlers.d.ts.map +1 -1
  36. package/dist/tools/handlers.js +226 -10
  37. package/dist/tools/handlers.js.map +1 -1
  38. package/dist/tools/minimal-descriptions.js +4 -4
  39. package/dist/tools/minimal-descriptions.js.map +1 -1
  40. package/dist/tools/shape-params.d.ts +2 -2
  41. package/dist/tools/shape-params.js +2 -2
  42. package/dist/tools/stick-vocabulary.d.ts +1 -1
  43. package/dist/tools/stick-vocabulary.js +1 -1
  44. package/dist/types/code-generator.d.ts +40 -2
  45. package/dist/types/code-generator.d.ts.map +1 -1
  46. package/dist/types/code-generator.js +260 -26
  47. package/dist/types/code-generator.js.map +1 -1
  48. package/dist/types/schemas.d.ts +2170 -698
  49. package/dist/types/schemas.d.ts.map +1 -1
  50. package/dist/types/schemas.js +152 -36
  51. package/dist/types/schemas.js.map +1 -1
  52. package/dist/utils/mcp-log.d.ts +16 -0
  53. package/dist/utils/mcp-log.d.ts.map +1 -0
  54. package/dist/utils/mcp-log.js +10 -0
  55. package/dist/utils/mcp-log.js.map +1 -0
  56. package/dist/utils/progress.d.ts +28 -0
  57. package/dist/utils/progress.d.ts.map +1 -0
  58. package/dist/utils/progress.js +24 -0
  59. package/dist/utils/progress.js.map +1 -0
  60. package/dist/vendor/design/design-style.js +1 -1
  61. package/manifest.json +1 -1
  62. package/package.json +7 -4
@@ -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
  // =============================================================================
@@ -1738,6 +1740,8 @@ EXAMPLES:
1738
1740
  annotations: { title: 'Beat-Snapped Cuts', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
1739
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.
1740
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
+
1741
1745
  - source: the music — an upload id, URL, data: URL or local file path. Or beats: [seconds] to skip the analysis.
1742
1746
  - every: 'beat' (default) | 'bar' | N beats. beatsPerBar (default 4). range: [from, to] seconds.
1743
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.
@@ -1805,6 +1809,12 @@ EXAMPLE: { checks: ["contrast", "flash"], duration: 6, fps: 20 }`,
1805
1809
  annotations: { title: 'Generate Image (cloud)', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
1806
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).
1807
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
+
1808
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.
1809
1819
 
1810
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).
@@ -1847,7 +1857,7 @@ The result: assets [{assetId, ref, width, height}] — the sizes the files HAVE
1847
1857
  {
1848
1858
  name: 'pinepaper_generate_estimate',
1849
1859
  annotations: { title: 'Estimate Generation (free)', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1850
- 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.`,
1851
1861
  inputSchema: {
1852
1862
  type: 'object',
1853
1863
  properties: {
@@ -2135,6 +2145,13 @@ By default returns a cheap per-frame hash + byte size (token-light), plus unique
2135
2145
 
2136
2146
  Set includeDataUrls: true to also get each frame as a PNG data URL (large — only for a few frames).
2137
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
+
2138
2155
  USE WHEN:
2139
2156
  - Verifying an animation is deterministic / reproducible
2140
2157
  - Checking whether a scene actually animates across a time range
@@ -2157,8 +2174,26 @@ EXAMPLE: { times: [0, 0.5, 1, 1.5, 2], seed: 42 }`,
2157
2174
  type: 'boolean',
2158
2175
  description: 'Include each frame as a PNG data URL (large). Default false → hashes only.',
2159
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
+ },
2160
2196
  },
2161
- required: ['times'],
2162
2197
  },
2163
2198
  },
2164
2199
  {
@@ -2194,11 +2229,15 @@ VIDEO EDITING:
2194
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).
2195
2230
  - stop_live_matte: { id } — stop a live matte, keep the last cut.
2196
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
+
2197
2236
  EXAMPLE: { action: 'upload_video', url: 'https://…/clip.mp4', scale: 0.5, timeOffset: 1 }`,
2198
2237
  inputSchema: {
2199
2238
  type: 'object',
2200
2239
  properties: {
2201
- 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" },
2202
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) }." },
2203
2242
  atPlayhead: { type: 'boolean', description: 'upload_video / upload_audio: start at the current playback time (instead of timeOffset).' },
2204
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.' },
@@ -2218,7 +2257,11 @@ EXAMPLE: { action: 'upload_video', url: 'https://…/clip.mp4', scale: 0.5, time
2218
2257
  hideMatte: { type: 'boolean', description: 'apply_track_matte: hide the matte item after applying.' },
2219
2258
  live: { type: 'boolean', description: 'apply_track_matte: keep re-cutting as the matte moves/animates — the kinetic mask reveal. Omitted = bake once, destructively.' },
2220
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." },
2221
- 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).' },
2222
2265
  rate: { type: 'number', description: 'Playback rate 0.25–4 — set_playback_rate.' },
2223
2266
  inPoint: { type: 'number', description: 'Clip in-point (media-time s) — set_clip.' },
2224
2267
  outPoint: { type: 'number', description: 'Clip out-point (media-time s, > inPoint) — set_clip.' },
@@ -2309,9 +2352,10 @@ READ THIS BEFORE PROMISING A MEDIUM. Naming a medium does not create its marks.
2309
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.
2310
2353
 
2311
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.
2312
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).
2313
2357
  - resolve: { medium } → { ok, fidelity, reason, markMaker }. ok:false with a reason is a real answer — pass it on rather than trying anyway.
2314
- - list_stitches: the 4 thread stitches with their params.
2358
+ - list_stitches: the thread stitches with their params.
2315
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.
2316
2360
  - apply_hatch: rule an item with hatching. Same deal — silhouette is the region, source is hidden, re-hatching replaces.
2317
2361
  - list_flow_fields: the 7 fields that bend a hatch line.
@@ -2341,11 +2385,12 @@ EXAMPLE — a stitched leaf:
2341
2385
  inputSchema: {
2342
2386
  type: 'object',
2343
2387
  properties: {
2344
- action: { type: 'string', enum: ['list_media', 'resolve', 'list_stitches', 'apply_thread', 'apply_hatch', 'list_flow_fields', 'list_hatch_options'], description: 'Medium operation' },
2345
- 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.' },
2346
- 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).' },
2347
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." },
2348
- 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." },
2349
2394
  stitchLen: { type: 'number', description: 'apply_thread: nominal stitch length px (default 18).' },
2350
2395
  rowGap: { type: 'number', description: 'apply_thread: row spacing px; defaults from thread width so rows abut.' },
2351
2396
  variance: { type: 'number', description: 'apply_thread: length jitter 0..1 (default 0.35) — the long-and-short shading.' },
@@ -2594,6 +2639,13 @@ RECIPE — a playable maze: create_tilemap with wall fills → batch_create colo
2594
2639
  },
2595
2640
  {
2596
2641
  name: 'pinepaper_design_system',
2642
+ annotations: {
2643
+ title: 'Design System',
2644
+ readOnlyHint: false,
2645
+ destructiveHint: false,
2646
+ idempotentHint: false,
2647
+ openWorldHint: false,
2648
+ },
2597
2649
  description: `The design vocabulary, as data you can read — and eighteen styles that can build a scene.
2598
2650
 
2599
2651
  TWO DIFFERENT THINGS, kept apart on purpose:
@@ -2630,6 +2682,13 @@ A partial composition reports as a failure with the elements that did not create
2630
2682
  },
2631
2683
  {
2632
2684
  name: 'pinepaper_stick',
2685
+ annotations: {
2686
+ title: 'Stick Figure Kit',
2687
+ readOnlyHint: false,
2688
+ destructiveHint: false,
2689
+ idempotentHint: false,
2690
+ openWorldHint: false,
2691
+ },
2633
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.
2634
2693
 
2635
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.
@@ -2695,6 +2754,13 @@ The geometry is vendored from mcp-cloud, which makes this a three-repo artifact:
2695
2754
  },
2696
2755
  {
2697
2756
  name: 'pinepaper_story',
2757
+ annotations: {
2758
+ title: 'Story to Scene',
2759
+ readOnlyHint: false,
2760
+ destructiveHint: false,
2761
+ idempotentHint: false,
2762
+ openWorldHint: false,
2763
+ },
2698
2764
  description: `A piece of prose becomes a scene.
2699
2765
 
2700
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.
@@ -2715,6 +2781,13 @@ The geometry is vendored from mcp-cloud, which makes this a three-repo artifact:
2715
2781
  },
2716
2782
  {
2717
2783
  name: 'pinepaper_interchange',
2784
+ annotations: {
2785
+ title: 'Interchange Formats',
2786
+ readOnlyHint: false,
2787
+ destructiveHint: false,
2788
+ idempotentHint: false,
2789
+ openWorldHint: false,
2790
+ },
2718
2791
  description: `The formats other tools read and write — Lottie, dotLottie, GLB, BVH, PNG sequence.
2719
2792
 
2720
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.
@@ -2733,6 +2806,13 @@ Three of these refuse quietly in the engine: exportGLB with no perspective objec
2733
2806
  },
2734
2807
  {
2735
2808
  name: 'pinepaper_sound',
2809
+ annotations: {
2810
+ title: 'Sound Synthesis & Soundtrack',
2811
+ readOnlyHint: false,
2812
+ destructiveHint: false,
2813
+ idempotentHint: false,
2814
+ openWorldHint: false,
2815
+ },
2736
2816
  description: `Synthesis — and the drawing that is the same object.
2737
2817
 
2738
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.
@@ -2786,6 +2866,13 @@ render_soundtrack mixes every placed sound to a WAV and writes it to a file. It
2786
2866
  },
2787
2867
  {
2788
2868
  name: 'pinepaper_motion',
2869
+ annotations: {
2870
+ title: 'Group & Field Motion',
2871
+ readOnlyHint: false,
2872
+ destructiveHint: false,
2873
+ idempotentHint: false,
2874
+ openWorldHint: false,
2875
+ },
2789
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.
2790
2877
 
2791
2878
  Two kinds, and the second is the one nothing else here can do:
@@ -2817,6 +2904,13 @@ This is not pinepaper_animate: that applies a loop preset to ONE item. This move
2817
2904
  },
2818
2905
  {
2819
2906
  name: 'pinepaper_path',
2907
+ annotations: {
2908
+ title: 'Path Operations',
2909
+ readOnlyHint: false,
2910
+ destructiveHint: true,
2911
+ idempotentHint: false,
2912
+ openWorldHint: false,
2913
+ },
2820
2914
  description: `Destructive path operations — the ones that REPLACE or ADD items rather than restyling them.
2821
2915
 
2822
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.
@@ -2879,7 +2973,7 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2879
2973
  anyOf: [{ type: 'boolean' }, { type: 'object' }],
2880
2974
  description: 'create: include the walkable character (boolean or config).',
2881
2975
  },
2882
- 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.' },
2883
2977
  actorId: { type: 'string', description: 'Actor id.' },
2884
2978
  x: { type: 'number', description: 'add_actor: world x.' },
2885
2979
  z: { type: 'number', description: 'add_actor: world z.' },
@@ -2890,18 +2984,18 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2890
2984
  camera: { type: 'object', description: "set_camera: { mode: follow|fixed|orbit, target?, radius?, speed?, eye?, lookAt? }." },
2891
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.' },
2892
2986
  pathId: { type: 'string', description: 'extrude_path / lathe_path: the canvas path to turn into geometry. The path itself is unchanged.' },
2893
- 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.' },
2894
2988
  meshId: { type: 'string', description: 'remove_mesh: the mesh id.' },
2895
- 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.' },
2896
2990
  lightId: { type: 'string', description: 'set_light / remove_light: the light id.' },
2897
- 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.' },
2898
2992
  materialId: { type: 'string', description: 'set_material / remove_material: the material id.' },
2899
2993
  source: { type: 'string', description: 'import_obj: the OBJ file TEXT. import_gltf: a URL or a data: URI of the .glb bytes.' },
2900
2994
  importOptions: { type: 'object', description: 'import_obj / import_gltf: placement and scale options, passed through.' },
2901
2995
  clip: { type: 'string', description: 'set_mesh_clip: which imported animation clip a skinned mesh plays.' },
2902
2996
  crossfade: { type: 'number', description: 'set_mesh_clip: seconds to blend out of the previous clip (blended in LOCAL pose space).' },
2903
2997
  navTarget: { type: 'string', enum: ['2d', '3d'], description: "set_nav_target: which layer the pointer drives." },
2904
- 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.' },
2905
2999
  origin: { type: 'array', items: { type: 'number' }, description: 'raycast: ray origin [x, y, z].' },
2906
3000
  direction: { type: 'array', items: { type: 'number' }, description: 'raycast: ray direction [x, y, z], need not be normalised.' },
2907
3001
  multiplier: { type: 'number', description: 'dolly_camera: >1 away, <1 closer.' },
@@ -2924,7 +3018,9 @@ RECIPE — a character walking through a forest: create {spec:'forest'} → impo
2924
3018
  annotations: {
2925
3019
  title: 'Character (place and direct a figure from the graph)',
2926
3020
  readOnlyHint: false,
3021
+ destructiveHint: false,
2927
3022
  idempotentHint: false,
3023
+ openWorldHint: false,
2928
3024
  },
2929
3025
  description: `Place a CHARACTER from the design graph and DIRECT it — no geometry, no bones, no poses.
2930
3026
 
@@ -3472,7 +3568,7 @@ applyFunction: \`
3472
3568
  annotations: {
3473
3569
  title: 'Execute Custom Code',
3474
3570
  readOnlyHint: false,
3475
- destructiveHint: false,
3571
+ destructiveHint: true,
3476
3572
  idempotentHint: false,
3477
3573
  openWorldHint: true,
3478
3574
  },
@@ -3646,12 +3742,9 @@ The authoritative list for a given item is pinepaper_get_animatable_properties
3646
3742
 
3647
3743
  EASING — IT SHAPES THE SEGMENT ARRIVING AT THE KEYFRAME IT IS ON.
3648
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.
3649
- - linear: Constant speed
3650
- - easeIn: Slow start
3651
- - easeOut: Slow end
3652
- - easeInOut: Slow start and end
3653
- - bounce: Bounce effect
3654
- - 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.
3655
3748
 
3656
3749
  CLIP WINDOW (optional — Premiere/Canva-style clip behavior):
3657
3750
  - timeOffset: Shift the whole clip in canvas time (seconds). The animation only runs while canvas time is in [timeOffset, timeOffset + (clipOutPoint - clipInPoint)].
@@ -3679,9 +3772,8 @@ EXAMPLE — Reveal at 2s, skip first second of keyframe data, end at 4s of keyfr
3679
3772
  additionalProperties: true,
3680
3773
  },
3681
3774
  easing: {
3682
- type: 'string',
3683
- enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'bounce', 'elastic'],
3684
- 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.',
3685
3777
  },
3686
3778
  },
3687
3779
  required: ['time', 'properties'],
@@ -3849,7 +3941,7 @@ MASK TYPES: rectangle, circle, ellipse, star, triangle, hexagon, heart, rounded`
3849
3941
  properties: {
3850
3942
  time: { type: 'number', description: 'Normalized time (0-1)' },
3851
3943
  properties: { type: 'object', description: 'Mask properties (x, y, width, height, radius, scale, rotation, opacity)' },
3852
- easing: { type: 'string', enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'bounce', 'elastic'] },
3944
+ easing: { type: 'string', enum: [...KEYFRAME_EASINGS] },
3853
3945
  },
3854
3946
  required: ['time', 'properties'],
3855
3947
  },
@@ -3860,7 +3952,7 @@ MASK TYPES: rectangle, circle, ellipse, star, triangle, hexagon, heart, rounded`
3860
3952
  properties: {
3861
3953
  startTime: { type: 'number', description: 'Start time in seconds', default: 0 },
3862
3954
  duration: { type: 'number', description: 'Duration in seconds', default: 0.8 },
3863
- easing: { type: 'string', enum: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'bounce'], default: 'easeOut' },
3955
+ easing: { type: 'string', enum: [...KEYFRAME_EASINGS], default: 'easeOut' },
3864
3956
  reversed: { type: 'boolean', description: 'Reverse animation (hide instead of reveal)', default: false },
3865
3957
  loop: { type: 'boolean', description: 'Loop the animation', default: false },
3866
3958
  },
@@ -3977,7 +4069,7 @@ Needs a studio that publishes the item-side table; without one, the tool says so
3977
4069
  idempotentHint: true,
3978
4070
  openWorldHint: false,
3979
4071
  },
3980
- 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.`,
3981
4073
  inputSchema: {
3982
4074
  type: 'object',
3983
4075
  properties: {},
@@ -4101,8 +4193,8 @@ EXAMPLE (curved arc between waypoints):
4101
4193
  pitch: { type: 'number', description: '3D tilt in degrees — rendered where the studio supports camera tilt (result.tiltRenders).' },
4102
4194
  yaw: { type: 'number', description: '3D rotation in degrees — rendered where the studio supports camera tilt (result.tiltRenders).' },
4103
4195
  easing: {
4104
- type: 'string',
4105
- 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.',
4106
4198
  },
4107
4199
  pathOut: {
4108
4200
  type: 'array',
@@ -4974,7 +5066,7 @@ flags as a badly-composed scene, and they do not survive an artboard change.`,
4974
5066
  name: 'pinepaper_brand_kit',
4975
5067
  annotations: {
4976
5068
  title: 'Brand Kit', readOnlyHint: false, destructiveHint: false,
4977
- idempotentHint: true, openWorldHint: false,
5069
+ idempotentHint: true, openWorldHint: true,
4978
5070
  },
4979
5071
  description: `Apply a brand kit — colours, fonts and roles — across the scene.
4980
5072
 
@@ -4982,6 +5074,7 @@ ACTIONS:
4982
5074
  - plan: Report what WOULD change (count, per-role breakdown, WCAG contrast audit) WITHOUT touching
4983
5075
  the canvas. Call this first on an unfamiliar scene; a whole-scene recolour is hard to eyeball afterwards.
4984
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.
4985
5078
 
4986
5079
  KIT SHAPE — only name and colors.primary are required:
4987
5080
  { "name": "Acme", "colors": { "primary": "#e11d48", "secondary": "#0ea5e9",
@@ -4996,7 +5089,9 @@ Both actions return a WCAG contrast audit, so you can see failing pairs before s
4996
5089
  inputSchema: {
4997
5090
  type: 'object',
4998
5091
  properties: {
4999
- 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).' },
5000
5095
  kit: {
5001
5096
  type: 'object',
5002
5097
  description: 'Brand kit. Requires name and colors.primary; other roles are inferred.',
@@ -5022,7 +5117,7 @@ Both actions return a WCAG contrast audit, so you can see failing pairs before s
5022
5117
  },
5023
5118
  selectionOnly: { type: 'boolean', description: 'Restrict to current selection' },
5024
5119
  },
5025
- required: ['action', 'kit'],
5120
+ required: ['action'],
5026
5121
  },
5027
5122
  },
5028
5123
  {
@@ -7031,7 +7126,7 @@ EXAMPLES:
7031
7126
  annotations: {
7032
7127
  title: 'Execute Generator',
7033
7128
  readOnlyHint: false,
7034
- destructiveHint: false,
7129
+ destructiveHint: true,
7035
7130
  idempotentHint: false,
7036
7131
  openWorldHint: false,
7037
7132
  },
@@ -7872,7 +7967,7 @@ EXAMPLES:
7872
7967
  annotations: {
7873
7968
  title: 'Build Scene — Items, Animations, Effects in One Call',
7874
7969
  readOnlyHint: false,
7875
- destructiveHint: false,
7970
+ destructiveHint: true,
7876
7971
  idempotentHint: false,
7877
7972
  openWorldHint: false,
7878
7973
  },
@@ -8195,6 +8290,14 @@ VERIFY MOTION BEFORE YOU RENDER. An export takes seconds to minutes and shows yo
8195
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.' },
8196
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.' },
8197
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
+ },
8198
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.' },
8199
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.' },
8200
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.' },