尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證pinepaper.studio MCP server
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
Create animated vector graphics with AI using the Model Context Protocol
The animation engine agents can drive.
This poster is one 16 KB SVG — 100+ animations, no scripts. View source on it.
Try PinePaper Studio ·
Docs ·
npm
Everything above and below moves — these are animated SVGs exported straight from PinePaper tool calls, no video files, no GIFs. Open this README on GitHub and watch.檢視完整 README ↗Create animated vector graphics with AI using the Model Context Protocol
English · 简体中文 · 日本語 · 한국어 · Español · Português (BR) · Français · Deutsch · हिन्दी
Everything above and below moves — these are animated SVGs exported straight from PinePaper tool calls, no video files, no GIFs. Open this README on GitHub and watch.
PinePaper MCP Server enables AI assistants to create and animate graphics in PinePaper Studio via the Model Context Protocol (MCP). Works with any AI that supports MCP tool calling (Claude, GPT, Gemini, local models, etc.).
The server exposes 178 tools across drawing, animation, diagrams, maps, typography, physics, image editing, data visualization, and export. Using natural language, you can:
Local is free and complete. Every one of the 178 tools works when you run this server yourself. There is no reduced tier and nothing held back.
What it needs:
| Node | 22.13 or newer (any supported Node release: 22, 24 or 26) |
| Disk | With Puppeteer (below), Chrome is downloaded on install — roughly 320 MB per version |
| Memory | a Chrome process plus the Studio canvas, so budget ~1 GB while a job runs |
That is fine on a laptop and awkward on a small VPS, a locked-down work machine, or a container you would rather keep thin.
cloud.pinepaper.studio runs the same server for you — same tools, same version, over HTTP with no install and no browser on your machine. It exists for three cases: you have no MCP client, you cannot install one, or your machine cannot spare the browser.
Hosting costs money, so the hosted option is paid — it runs on credits. See cloud.pinepaper.studio for current pricing. Run it locally if you can, but it is not identical, and the differences run both ways.
The hosted service runs a tested set of LLMs, repairs generated code before it reaches the canvas — which a local server structurally cannot do for itself — and encodes video server-side, in an ffmpeg container that owns the job durably. Locally, an export runs in your own browser: it needs Chrome and Puppeteer, it is bounded by your machine, and a long render occupies that browser for its duration.
What the local server has instead is the whole tool surface. Tools that need the editor's UI — selection, drag-and-drop, mode toggles — work here against a visible browser and are excluded from headless operation. Which model to trust is a question you can answer yourself rather than take on trust: a benchmark runs one prompt across several models and puts the results side by side, any run shareable. It is in invite-only beta.
Claude Desktop: download pinepaper-mcp-server.mcpb and open it. Desktop installs the server, with no config file to edit. It drives your installed Google Chrome; its settings choose another Chromium-based browser, the export folder and the tool list. Each release lists the file's SHA-256.
For other MCP clients: this package does not install Puppeteer, so a plain
npx @pinepaper.studio/mcp-server has no browser, and every tool returns the
code it would run instead of running it (the result says so). To execute in a
real browser, install Puppeteer in the same npx call. A separate
npm i puppeteer in your project is not visible to npx's copy of the server:
{
"mcpServers": {
"pinepaper": {
"command": "npx",
"args": ["-y", "-p", "puppeteer", "-p", "@pinepaper.studio/mcp-server", "pinepaper-mcp"]
}
}
}
Security switches: off by default. Two capabilities are a security boundary and stay off until you, the person running the server, turn them on. An agent cannot turn them on from a tool call.
Switch Turns on PINEPAPER_ALLOW_CODE=1Running JavaScript the agent writes: pinepaper_execute_custom_code,pinepaper_p5_draw,pinepaper_render_hook(register),pinepaper_register_custom_relation, and the custom relations saved in a scene file (pinepaper_import_sceneregisterCustomRelations).PINEPAPER_ALLOW_LOCAL_FILES=1Importing images, audio, video and scenes from your disk (the working folder and PINEPAPER_ASSET_DIRS). URLs and the export folder always work.With a switch off, those calls are refused with the switch named, and everything else works. Turn one on only if you trust what your agent reads, since a web page or a scene file can steer what it writes. In the Claude Desktop extension they are the "Allow custom code" and "Allow local files" settings. In a config file, add them to the server's
env:"env": { "PINEPAPER_ALLOW_CODE": "1", "PINEPAPER_ALLOW_LOCAL_FILES": "1" }
For code-only use (no Chrome, no 320 MB download), use
"args": ["-y", "@pinepaper.studio/mcp-server"] and set
"env": { "PINEPAPER_EXECUTION_MODE": "code" }. A global install works too:
npm i -g @pinepaper.studio/mcp-server puppeteer, then "command": "pinepaper-mcp".
Every graphic below is an animated SVG produced through this server's tool surface — the arguments shown with each result are what an AI agent passes to the named tool. They aren't shell commands; Run these yourself below shows the three ways to execute them.
// The scene IS a graph: two items, two declared edges —
// the canvas view and the graph view are the same data.
{ "sourceId": "$dot",
"relationType": "moves_along_path",
"relationOptions": { "path": "$p1", // an ellipse path
"duration": 6, "easing": "easeInOut", "loop": true } }
{ "itemId": "$square", "animationType": "rotate",
"options": { "speed": 0.18 } }
// Five rails, five named easings, one loop — each dot is
// a moves_along_path down its rail with a different easing
for (const easing of ['linear', 'easeIn', 'easeOut',
'easeInOut', 'pingpong']) {
add_relation($dot, 'moves_along_path', {
equation: { kind: 'parametric', xExpr: '0', yExpr: 't',
min: -1, max: 1, scale: 58 },
duration: 2.6, easing, loop: true });
}
// The engine solves the curve; the exporter bakes the
// motion to native SVG keyframes. pinepaper_add_relation:
{ "sourceId": "$dot", "relationType": "moves_along_path",
"relationOptions": { "equation": {
"kind": "parametric",
"xExpr": "cos(2*t)*cos(t)",
"yExpr": "cos(2*t)*sin(t)",
"scale": 108, "cx": 280, "cy": 118 },
"duration": 8, "loop": true } }
// Status chip: pale panel, slate bar, green dot blinking
{ "itemType": "rectangle", "properties": { "width": 264,
"height": 72, "fillColor": "#e8eff5" } }
{ "itemType": "circle", "properties": { "radius": 9,
"fillColor": "#2e9b4e" } }
{ "itemId": "$1", "animationType": "fade",
"options": { "speed": 1.0 } } // only the dot blinks
All five showcase files (including the banner) live in assets/ — tiny (4–11 KB), dependency-free, loop forever, and render anywhere SVG renders: GitHub READMEs, docs sites, dashboards, emails that allow SVG. They follow one editorial design system (serif mastheads, hairline rules, slate ink on paper white, framed canvas stages, typed-edge graph diagrams) supplied to the agent as context — share a design guideline with your agent and the tool calls come out on-system.
GitHub can't run scripts inside a README, so the interactive demos live in the editor — one click, no install. Each is a shipped template where the relation graph does all the state handling (tabs, accordions, menus — no event-handler code):
on_click_fire + exclusive_group + on_enter_set_visibilityon_event_toggleThe same graph drives visuals, keyboard access, and screen-reader roles (WCAG 2.1 AA) — see pinepaper://docs/relations from your MCP client.
The snippets above are MCP tool-call arguments — they execute when an AI agent invokes the tool. Three ways to make that happen:
1 · Ask your agent (any MCP client). With this server configured, paste a prompt like:
Create a blue circle and make it ride a diamond-shaped path with easeInOut, looping. Add an orange rotating square beside it. Then export the scene as animated SVG.
Your agent picks the tools (pinepaper_create_item, pinepaper_add_relation, pinepaper_export_svg) and runs them.
2 · Hand your agent a complete batch. This is a full, valid pinepaper_agent_batch_execute argument — an agent (or an MCP inspector) can execute it verbatim; $0/$1 reference the created items in order:
{
"operations": [
{ "type": "create", "itemType": "circle",
"properties": { "x": 300, "y": 260, "radius": 10, "fillColor": "#2e5e8f" } },
{ "type": "relation", "relationType": "moves_along_path", "sourceId": "$0",
"relationOptions": { "path": [ { "x": 180, "y": 260 }, { "x": 300, "y": 180 },
{ "x": 420, "y": 260 }, { "x": 300, "y": 340 } ],
"duration": 6, "easing": "easeInOut", "loop": true } },
{ "type": "create", "itemType": "rectangle",
"properties": { "x": 520, "y": 260, "width": 60, "height": 60, "fillColor": "#f0a030" } },
{ "type": "animate", "itemId": "$1", "animationType": "rotate" }
]
}
3 · No MCP, no agent — just a browser. Open pinepaper.studio/editor, open the browser console, and paste (verified working as-is):
const app = window.PinePaper;
const dot = app.create('circle', { x: 300, y: 260, radius: 10, fillColor: '#2e5e8f' });
app.addRelation(dot.data.id, null, 'moves_along_path', {
path: [ {x:180,y:260}, {x:300,y:180}, {x:420,y:260}, {x:300,y:340} ],
duration: 6, easing: 'easeInOut', loop: true,
});
const sq = app.create('rectangle', { x: 520, y: 260, width: 60, height: 60, fillColor: '#f0a030' });
app.animate(sq, { animationType: 'rotate' });
The same code an agent generates is the code you can paste — the canvas is yours either way, undo included.
Supply-chain scanners flag this package, and they are not wrong about the mechanism. Socket and tools like it mark it as malware-adjacent because it evaluates JavaScript at runtime. The detection is correct about the capability and wrong about the intent, so here is exactly what happens.
Every tool emits JavaScript — that is the architecture, not an exception. A tool call is compiled into a snippet written against the PinePaper and Paper.js APIs; the snippet is the product. pinepaper_execute_custom_code is simply the case where the agent writes the snippet instead of the server generating it, which is what lets a model draw something no other tool has a name for. Removing execution would not harden this package, it would delete it.
Code the agent writes is off unless you turn it on. The tools that run JavaScript the agent wrote (pinepaper_execute_custom_code, pinepaper_p5_draw, pinepaper_render_hook register, pinepaper_register_custom_relation, and pinepaper_import_scene's registerCustomRelations) refuse, naming the switch, unless the server is started with PINEPAPER_ALLOW_CODE=1. Reading files from your disk for an import (images, audio, video, scenes) needs PINEPAPER_ALLOW_LOCAL_FILES=1; files in the export folder, the server's own results, are always readable. In the Claude Desktop extension both are settings, off by default. An agent takes instructions from whatever it reads, a web page or a scene file included, so whether that text may become code is the decision of whoever starts the server.
Where the code runs. In a browser page on your own machine, against your own canvas. It does not run in the server process, and nothing is sent anywhere else to be executed.
Two modes, and one of them never executes anything:
| Mode | What happens |
|---|---|
code (PINEPAPER_EXECUTION_MODE=code) | The tool returns the JavaScript and stops. Nothing runs. You read it and paste it if you want it. |
puppeteer (default) | The server launches Chrome and runs the snippet in the page via page.evaluate(). |
If you do not want an agent executing anything, code mode is a first-class path, not a degraded one — the same snippet, handed to you instead of run.
What guards execution. On current engine builds the snippet goes through app.runGenerated(code, { source: 'agent' }) — a governor with seeded determinism, loop and item budgets, and a machine-readable report — rather than a bare eval. Older engine builds fall back to eval.
Found a vulnerability? Report it privately, as SECURITY.md describes, not in a public issue.
What to weigh before running it.
--no-sandbox and --disable-setuid-sandbox. That is routine for headless automation and it does weaken Chrome's own process sandbox. If that matters where you are running it, use code mode or put the server in a container.npx call, a global install, a project install) or from a folder you name in PINEPAPER_PUPPETEER_FROM; never from the folder it happens to be started in. puppeteer 22 or newer is required.ffprobe and ffmpeg: checking a saved video against what the studio encoded, and joining the cached segments of a perScene export (ffmpeg -f concat). Only the Claude Desktop extension runs them, through a small runner it bundles that allows those two binaries alone, via execFile (no shell, argument lists the server writes, absolute paths). From npm, the check is skipped and said so, and a perScene export runs whole.PINEPAPER_ALLOW_LOCAL_FILES=1, and then only from the working directory, the export folder, and folders listed in PINEPAPER_ASSET_DIRS (the export folder is readable without the switch). PINEPAPER_ALLOW_PRIVATE_NETWORK=1 allows local and LAN hosts. Metadata addresses stay refused even with it set.>=18 was never
really true. On an older Node, upgrade before updating.src/utils/env.ts), and SECURITY.md lists what each capability of the
package is for and where it lives.PINEPAPER_PUPPETEER_FROM
now loads puppeteer from the folder you name with createRequire. The one
exception is the optional ffmpeg add-on that only the Claude Desktop
extension bundles.PINEPAPER_LOCALE that is not a supported language falls back to English.
Before, the value became part of a file path.animationOptions: { intensity, speed, delay } on an
animate op applied none of them. Relations, effects and generators take
params, masks take preset and options, generators take region,
animate takes intensity / speed / delay / waveform / origin,
and a modify op takes its changes beside itemId.speed on animate, preset on a
mask, itemId on a generator) is reported instead of passing as known.animate op names any animationOptions key the studio does not read,
with the likely intended name (animationIntensty → animationIntensity?).
The list comes from the studio's own code, so it follows the studio as it
changes.keyframe_animate op gives the same warnings as
pinepaper_keyframe_animate: a key that leaves out a property another key
animates, loop in an export, easing on the first key, and x keyed beside
fontSize on an older studio. The batch op used to give none of them.[min, max] range only on keys that take one number
in every studio build (count, aspect, strokeWidth, jitterHz, the
fades). gravity, drag, friction, opacity, sizeEnd and jitter are
sampled per particle on the current studio, and an older one reports a range
it can't draw itself.on_event_step_active works without a targetId on the current studio. On
an older studio, which runs an event reaction only when it has a target, a
target-less one used to report success and never step; it is now refused
there, with the fix in the message (point it at any member of the group).exclusive_group only through exclusive_group relations between them.
Being active has no look of its own: on_enter_* / on_exit_* reactions on
each member draw it, in exports too. only sets the step order.An idle server pauses the studio page after 30 s with no tool call
(PINEPAPER_IDLE_PAUSE_SECONDS, 0 to turn it off), and the next call
resumes it. A 400-item scene's live preview used about half a core to one
and a half cores with nobody watching; paused, it uses almost none. Exports
come out identical either way.
Replies are much smaller for an agent editing a scene. JSON results are sent
compact; pinepaper_lint_scene { only: "edit" } returns just the ops that
change the scene, lists a group bound by a name pattern once instead of once
per member, and includes a custom relation's source only with
withSource: true; a batch reply leaves out entries that only repeat what you
sent (verbose: true lists every op); and the edit toolkit's batch
description covers editing, not building. One edit pass over a 20-dot scene
went from 44 KB to 15 KB.
New relation on_event_step_active, for next and previous arrows: an event
moves the active member of an exclusive_group forward or back by delta
(default +1), wrapping at the ends unless wrap: false.
pinepaper_lint_scene lists only the custom relations the scene uses, and
names the rest in registeredUnused (one registered before a
pinepaper_clear_canvas no longer shows up as part of the scene).
Housekeeping for supply-chain scanners, with no change in behaviour: the
hosted-API client's request helper is named for what it is, three long text
constants are typed as plain strings, and the p5 drawing generator checks
PINEPAPER_ALLOW_CODE itself as well as the tool handler.
mp4, webm, or
apng with transparent: false) warns that the frames are filled with the
studio's page colour. The warning used to say black, but the current studio
fills them white.skewX and skewY keyframes are documented: the current studio animates
them (degrees, up to ±89). A shear key, which is not a channel on any
studio, is named in ignoredProperties with a pointer to skewX / skewY
instead of being accepted silently.x and
fontSize, bloom, ink keepColor and removing a look are now fixed in the
studio itself. The 1.6.29 warnings and workarounds for them only appear on
an older studio that still needs them.pinepaper_modify_item no longer calls scale, scaleX or scaleY
ignored on the current studio.zod. The guarded fetch that protects
URL imports from reaching private and loopback addresses now runs on Node's
own http / https, with every connection still pinned to the addresses
that passed the check. undici is no longer installed.package.json names the issue tracker and homepage.scale, scaleX and scaleY on pinepaper_create_item are applied (a
squashed circle stays squashed). Before, the studio ignored them at create.pinepaper_morph_sequence takes hideTarget (per state or for the whole
call): false keeps the reference item visible.pinepaper_design_medium ink with keepColor and no color draws its marks
in the item's own colour, not the brush default.skewX, skewY or shear, which the studio does not animate,
are named in ignoredProperties. A sheared motion is a transform track.x keyed
beside fontSize on one text item (it drifts in export), a bloom image
filter (its halo stops at the image edge), and removing a look (geometry is
not restored).pinepaper_execute_custom_code, pinepaper_p5_draw, pinepaper_render_hook
(register), pinepaper_register_custom_relation and pinepaper_import_scene's
registerCustomRelations need a server started with PINEPAPER_ALLOW_CODE=1;
importing an image, audio, video or scene from your disk needs
PINEPAPER_ALLOW_LOCAL_FILES=1. Without the switch the call is refused and
names it. Files in the export folder stay readable, so saving and restoring a
scene works as before. URL imports are unchanged. In the Claude Desktop
extension both are settings ("Allow custom code", "Allow local files").ffprobe (checking a saved video)
and ffmpeg (joining a perScene export) now run only in the Claude
Desktop extension, which bundles them. From npm, a perScene export runs as
one whole export and says why.pinepaper_lint_scene says how to change what it lists: an edit section
with, for every relation, the batch op that changes it; for a group bound by
a name pattern ("dot-*"), one entry with the op that changes the group and
the ops that add or remove a member; and for a custom motion, the call that
redefines it. An editing agent can work from that alone.edit toolkit (PINEPAPER_TOOLKIT=edit, or pinepaper_set_toolkit):
pinepaper_lint_scene, pinepaper_agent_batch_execute,
pinepaper_register_custom_relation and pinepaper_agent_export, with the
lane's short descriptions, for changing a scene that already exists.pinepaper_animate has the ripple and undulate field motions: a wave
that sweeps through a group's children from an origin (centre, a side or a
corner), with a waveform (sine, triangle, square, sawtooth, spike, pulse).
The list of loop animations now comes from the studio itself, so a new one
can be named the day it ships.moves_along_path equation is read with y up, so on screen it is mirrored
against a curve written in screen coordinates. The result now says so and
points to flipY: false, and the docs say the item moves at constant speed
along the route.wordSpacing, textDecoration or textTransform, which the
studio does not animate, are named in ignoredProperties with a warning
instead of being accepted silently.pinepaper_choreograph says when a beat other than shake is given
intensity (only shake reads it), and names the beat's own size option.pinepaper_pattern modify with only new params works on a pattern whose
members were given as a plain list. Before, it was refused.deterministic export option is described as it behaves: WebM files
come out byte-identical, but an MP4 can differ in its encoded bytes, so
compare MP4s by the receipt's frameDigest.hold
or step key, that font weight steps rather than ramps, that phase_wave
patterns on one item add up, and that two ring or orbit patterns on one
item conflict. Emitter gravity, drag, friction, opacity, sizeEnd
and jitter are documented as taking a [min, max] range.modify with a top-level position ({x, y} or [x, y]) moves
the item, as position places one on create. Before, it reported
modified: true and the item stayed where it was. A modify with nothing
to change is refused by name.pinepaper_agent_export { perScene: true } exports a scene chain as cached
segments and joins them, so an export after editing one scene renders only
what that edit touched. The cache key is everything a segment depends on:
the scenes in it, their timing, the export settings and the studio build.
Hard cuts render scene by scene. Crossfades and cut sounds render as ranges
of the chain, frame for frame what a whole export shows. The sound is the
whole program, rendered once, so cached segments never carry a stale level
and there is no seam (a silent program gets a silent track, as a whole
export does). It needs ffmpeg on PATH; without it, or for a
looping chain, the chain is exported whole and the result says why.pinepaper_agent_export { startTime } exports a range of the timeline
(mp4 / webm): each frame is the whole export's frame at that time.localizes_sound, a relation from a sound to an item: the sound's stereo
position follows the item across the frame, in the preview and in exported
audio.sourceId: "dot-*" binds
every item named like it, in name order, each with index and count in
its params. Send the same op again after adding items and they join, with
index and count updated. update: true changes the params of a
relation already bound.pinepaper_lint_scene lists every relation bound (from, to, type,
params) and every custom relation definition.pinepaper_export_scene { full: true } saves the definitions with the
scene. pinepaper_import_scene registers them before loading when passed
registerCustomRelations: true. Their code runs in the studio, so this
is opt-in; without it, the result names them.sped, or a
parameter from another relation) is named in a warning, with the likely
intended key and the list the relation does read. Before, it was accepted
and ignored in silence.lane toolkit (now 8 tools) includes
pinepaper_register_custom_relation, and says when motion should be a
relation instead of keyframes: time_expression for one item, a custom
relation bound to a name pattern for many.pinepaper_world3d create { control: true }: a viewer walks the 3D
character with the keyboard (arrows or WASD, Space jumps, Shift runs) when
the scene plays in the studio. The result says whether the studio turned it
on. With character: false there is nothing to walk, so the call is refused.connects_to, attached_to_tail,
head_points_to) say what they do: they record which item and port each
end of a connector belongs to, and only connectors made with
pinepaper_connect follow their items. Before, they read as if they would
move any line.pinepaper_clear_canvas
does the same before it empties the canvas. Before, every export from such
a page warned to start a new job, which the lane toolkit cannot do. The
warning now names pinepaper_clear_canvas and says the file may differ.time_expression formula the engine cannot read is refused, with the
reason and the grammar: Math.sin(...), an unknown name, a wrong
capitalisation, random(), or a function given the wrong number of
arguments. Before, the call reported success and nothing moved. The tool
descriptions now list the grammar, read from the engine.pinepaper_register_custom_relation is
accepted in pinepaper_agent_batch_execute, as it already was in
pinepaper_add_relation. Before, the batch's vocabulary check refused it.
A relation op may leave out targetId when the motion needs no other item.-p puppeteer in the npx call, a global install
or a project install). The Desktop extension includes what it needs.
puppeteer 22 or newer is required. With pnpm hoist=false or Yarn PnP
strict, set PINEPAPER_PUPPETEER_FROM to the project folder that has it.animated: false in its export
result, although the frames moved. When the studio says not animated, the
export now compares frames from across the video. If they differ, it reports
animated: true and says how it knows (fidelity.checked.motion). The
frozen-video check, which runs only on animated exports, now covers these
scenes too.pinepaper_pattern kinds ring and orbit: members spaced evenly round a
turning circle, with a fixed center or a target item the centre follows.
The list of kinds now comes from the studio, so a kind it adds appears here
too.zod and undici) instead of 78. It behaves the
same on the wire: the same protocol versions (2024-10-07 to 2025-11-25),
errors, progress, cancellation and logging, checked against recordings of
the previous server. createServer() still accepts any MCP transport,
including the SDK's.eval or runs git, and it reads
environment variables in one place.core toolkit, about half
the context of the previous default, with every craft tool the server
recommends. Other tools load when needed with pinepaper_set_toolkit
{"load": ["map"]} (a group) or a tool's name, and the list updates at
once. Claude Desktop, Cursor and Windsurf used to get all 178 tools.
PINEPAPER_TOOLKIT=agent or full restores a larger list.~/Downloads/PinePaper instead of a temporary folder,
so Desktop users can find them (PINEPAPER_EXPORT_DIR still overrides it,
and machines without a Downloads folder keep the temporary folder). Each
saved file also comes back as a link the client can open, and
resources/read returns files up to 10 MB from the export folder..mcpb) can be built with bun run build:mcpb: one file to open, no config to edit. It uses your installed
Chrome for the canvas tools. Without full puppeteer, the server now also
works with puppeteer-core and your installed Chrome, or another browser
set in PINEPAPER_CHROME_PATH.pinepaper_pattern no longer warns "NO CHANGE" after a create or
modify that has not moved anything yet, such as a wave at time 0 or one
whose window starts later.$N names the item that $N resolved to,
not "$N".animate and relation operations quote their animationType and
relationType correctly in the generated code.bitrate_exceeded with the bitrate the file actually has, and how
to land under the target.on_event_fire_if is documented: its predicate is an ExpressionIR object
(comparisons, and/or/not, reads of an item's data, active or visible
state), and it fires the event in params.event, not its target. A
predicate written as a string, or a relation with no event to fire, is
refused with the shape that works, wherever the relation comes from:
pinepaper_add_relation, a batch relation step, pinepaper_create_scene,
pinepaper_instantiate_ontology, or an on_event_add_relation that adds
one later. Before, the relation silently never fired.[min, max] range for a key that takes one number
(such as gravity, opacity or count) now gets a warning: the key draws
one value for every particle, not a spread. Before, there was no warning.pinepaper_import_image takes a name, so a batch patch can address an
imported image by name.{}, when the scene lint has warnings.create that carries a key it does not read
now gets an IGNORED ARGUMENTS note naming the step and the key. Before,
the key had no effect and nothing said so. The common mix-ups get a hint:
params on a relation step (it takes ), and / on
a step (changes go inside ).pinepaper_pattern: one stored declaration moves many items, with no
keyframes on each. phase_wave sends a wave through the members, stagger
offsets them in turn. Members are chosen by id, by name pattern or by tag.
create, modify, remove, list and get manage patterns afterwards.PINEPAPER_TOOLKIT=lane:
pinepaper_agent_batch_execute, pinepaper_keyframe_animate,
pinepaper_pattern, pinepaper_agent_export and pinepaper_lint_scene,
with descriptions written to be read on their own.ring, grid, orbit, stagger and patch.
ring and grid lay out many items in one operation. Each item is made
the same way create makes it and fills the next $N slot.orbit binds an orbiting item to a centre, making the orbiting item if
you pass itemType.stagger plays the same keyframes on several items, one after another.patch changes an item by its name.timeline: GSAP-style tweens written as JSON (to, from,
at, stagger, ease, repeat, yoyo, labels), turned into ordinary
keyframe tracks, so the result exports, saves and edits like hand-written
keyframes. A tween that cannot be built is refused before anything runs,
with the path of the bad value. The result says what each tween became.pinepaper_agent_export returns the saved file's sha256 and size. When
the studio reports per-frame hashes, it also returns a frameDigest, so two
renders can be compared in one field. When ffprobe is installed, the saved
video is checked against what the studio encoded, and any field that
disagrees is named (PINEPAPER_FFPROBE=off turns this off).pinepaper_execute_custom_code and pinepaper_agent_batch_execute return
the scene lint's warnings (lintWarnings), such as a title that runs out of
the frame. Every failed step is listed in failures as
{ step, itemId, reason }.keyframes[1].ease). Keys that a batch operation reads
directly (content, fontSize, x, y, timeOffset and the like) are
not reported.modify or patch addressed by a name that several items hold is
refused, and the result lists their ids. It used to change whichever of them
was made first.pinepaper_choreograph with sound: true now plays a launch sound at the
start of a bounce, as it already did for a hop.pinepaper_emitter accepts velocities and offsets written as
[{ x, y }, …] as well as [[x, y], …]. Before, the object form was
dropped and the particles had no velocity. The reply now warns when a
burst's count differs from the number of velocities.letterSpacing set on a text item in pinepaper_create_item or
pinepaper_modify_item is applied. It was reported as ignored at creation
and only took effect through keyframes.pinepaper_import_scene reads only from the working directory, the export
folder and PINEPAPER_ASSET_DIRS, like image and media imports. A path
outside them gets the same refusal whether or not the file exists, so the
error no longer shows what is on disk. Restoring a scene that
pinepaper_export_scene saved still works: it saves to the export folder.pinepaper://docs/readme resource accepts only a language code after
the slash (such as /ja or /pt-BR). Anything else is refused.body-parser, fast-uri,
ip-address, hono and zod (now zod 4)._meta["pinepaper.studio/ignoredArguments"].pinepaper_keyframe_animate accepts mode: 'merge' (the same as
append: true), which the studio's own warning recommends, on the tool and
in batch operations.pinepaper_import_image applies shadowColor, shadowBlur and
shadowOffset.pinepaper_design_system compose: cyberpunk scanlines are drawn as lines,
the HUD frame keeps its angled corners, and neon glows carry over to shapes
and text. Bauhaus and Memphis circles are drawn as circles. Text that lands
on a shape at a canvas size the style was not laid out for is reported in
layoutCollisions.jitter and spin, say what
quality controls on MP4 and WebM, and say how timing works on an equation
route in moves_along_path.pinepaper_choreograph anchors each move at where the actor is on the beat,
not wherever the playhead was left, and a roll on a picture turns by its
drawn size.moves_along_path keeps its duration.platformSize, platform_size_differs_from_canvas).fillOpacity is no longer reported as ignored, and tabular figures report
whether they really applied.pinepaper_brand_kit from_url, no longer fetch from
loopback, private, link-local or reserved addresses, or from a cloud
metadata address. Every redirect is checked the same way. To import from a
local or LAN dev server, set PINEPAPER_ALLOW_PRIVATE_NETWORK=1.PINEPAPER_ASSET_DIRS. Symlinks are
followed before the check. A path outside those folders is refused, and the
error names the folder to add.Five tools for textile, composition and interface pieces:
pinepaper_draw_weave: woven cloth as vector threads (plain, twill,
basket, herringbone).pinepaper_draw_knots: embroidered French knots, bullion rolls and anchor
tacks, placed where you say.pinepaper_draw_sashiko: Japanese running-stitch patterns (seigaiha,
asanoha, hishi, kaki no hana) on ground cloth.pinepaper_draw_subdivision: Mondrian, Swiss, Bauhaus and minimal
rectangle compositions, repeatable by seed.pinepaper_ui_kit: a phone lock screen with notifications that stack in
and a prompt that types, plus notification, prompt box, toggle, ad and
calendar cards. It draws in your active pinepaper_brand_kit brand, set in
Geist.Each one fills the canvas you have, and draws ordinary items you can select and edit.
pinepaper_hand_drawn gives a path a hand-drawn look. sketchy roughens and
bows its outline, repeatable by seed. reveal draws the stroke on over
duration seconds, alongside any keyframes the item already has. For a line
that boils from frame to frame, it points you to pinepaper_design_medium
ink.
pinepaper_pen_figure draws a mathematical figure with a pen: the tip travels
a Lorenz attractor, a Lissajous curve, a Hilbert curve or a harmonograph, and
the ink appears behind it, in the editor and in every export. action: 'list'
returns the catalogue. A figure is fitted into the box you give it, so it is
the same figure on any canvas size.
shader and field items from pinepaper_create_item draw in the studio
and in every export, not only in a cloud render. The note and export
warning that said a local export shows a flat plate are gone.text for content and textAlign (or align) for
justification, in pinepaper_create_item, pinepaper_modify_item and
batch creates.pinepaper_agent_batch_execute reads color, text, x and y written
directly on an operation. Its keyframe_animate refuses an item that is not
on the canvas, and takes append, timeOffset, clipInPoint,
clipOutPoint and timeUnits with the same meaning as
pinepaper_keyframe_animate.pinepaper_choreograph holds the actor still until its first beat, and no
longer calls a move off screen because the canvas is unbounded.pinepaper_morph_sequence hides the shapes it makes as references, and
pinepaper_physics add_body looks its item up the way the other tools do.$N that reaches past the items a batch has created fails and names the
id the previous batch gave it; a standalone $N resolves to the last
batch's item.letterSpacing reports that it is not animatable, rather than
succeeding with no effect.pinepaper_create_item reads a numeric lifetime as ttl, also when
bornAt is given beside it.keyframe_animate operations on one item in a batch merge into one
track that carries every property at every key, so one operation's key no
longer breaks another's motion.pinepaper_choreograph turns a list of beats into a performance for any
item: pop, drop, hop, bounce, roll, fly, peek, shake, squash, enter and exit,
each with anticipation, squash and stretch, and a settle. The item stays on
its ground line while it squashes. sound: true puts a sound on every pop,
launch and landing, quieter as the bounces shrink.
A click can play a sound (on_event_play_sound), an event can fire at set
times (pinepaper_event at), and a scene chain can put a sound on every
cut (cutSound).
pinepaper_look restyles one item or the whole scene as a named look, such as
8-bit, blueprint, ink sketch or print halftone. Ids, animation and relations
are kept, and it can be removed exactly. Each look carries the engine's own
grade of how convincing it is, and a weaker one is reported as an
approximation.
pinepaper_original_character builds a character from a base (person, robot,
cat, bird, blob) and a variant: palette, hair, eyes, accessories, proportions
and a look. It can generate a whole cast from seeds, and the characters can
look at the cursor, react to events and turn when dragged.
pinepaper_morph_sequence morphs one element through a series of shapes with
no cuts, and can move the camera and place a sound on the same timing.
pinepaper_brand_kit set applies a kit and makes it active: every shape
and text created afterwards takes its colours and fonts unless the call says
otherwise.
pinepaper_emitter makes confetti bursts and trails as a saved, frame-exact
item. pinepaper_render_hook adds custom per-frame drawing that a saved
scene carries. A full pinepaper_export_scene names anything it cannot save.
A text item takes a counter: a KPI rolling from 1.67% to 5.58%, a year
counter from 30,000 BCE to 2026, a timer. It is exact on every exported
frame, and its width stays steady while it counts.
create_item and modify_item take screenSpace (an overlay that stays
fixed while the camera moves) and stepTiming (animate on twos).
Keyframes take interpolation: 'hold' for a cut with no in-between.
A call that reports success but changes nothing now says so in its result, and a call that names an item that is not on the canvas fails. The tool descriptions point models to the highest-level tool first instead of a background generator with simple shapes.
pinepaper_import_mermaid returns the canvas ids of the shapes it drew, so
pinepaper_auto_layout can move them.pinepaper_image_filter documents the watercolour and painterly filters
for photos, and reports a refused filter as a failure.pinepaper_export_scene takes full: true: it saves a restorable project
file (animation, relations, rigging, scene chains) and returns its path.
pinepaper_import_scene restores it. Both report counts of items, animated
items, keyframes and relations, so you can check nothing was lost. Without
full, pinepaper_export_scene returns a summary for inspection, and now
says that it cannot be restored.
pinepaper_place_on_surface places a design onto a phone, monitor or
billboard in a photo: fitted to the screen's perspective, with a hand in front
of the screen kept in front, and a little glare and light spill. It finds the
screen itself, or takes its four corners. The result becomes part of the photo,
so zooms and camera moves carry it.
pinepaper_styled_scene draws a whole animated scene in one call: paper
cut-out, ink, watercolour, 1-bit dither, flowing contours or ASCII broadcast.
The composition can be changed (sky, hills, river, camera, colours), and
calling it again with another style redraws the same scene in the new look.
Export it for the length the result reports.
A host that records each canvas step and runs it at render time can pass
deferred: true: the agent is then told the step was recorded, rather than
handed a script to paste. Whenever the server hands back code instead of
running it (deferred, or --mode code), the code travels in
_meta["pinepaper.studio/code"] for the host to record.
Every result carries _meta["pinepaper.studio/timing"]: the whole call, and
the studio's share of it, for tracing slow requests.
pinepaper_relight lights the 3D world layer with up to eight point and
directional lights. The lighting shows in the preview and in PNG, PDF, MP4
and WebM exports, and the reply says how much time it adds to an export.
pinepaper_shader_graph runs node graphs (relighting, depth of field, bloom)
over an item or the whole scene. pinepaper_design_medium gains
sdf_stroke, a smooth pressure-sensitive brush stroke from a list of points.
pinepaper_text_style cursive takes animate: true and writes the words on
over duration seconds, with no keyframes to add. A nib chooses the pen:
'broad' for calligraphic thick and thin strokes, 'monoline' for one even
line. Unknown cursive options are now refused rather than ignored.
pinepaper_design_medium apply with medium: 'ink' or 'charcoal'
restyles an existing stroke, title or group in place, with a hand-drawn line
that boils. The item keeps its id, animation and relations.
pinepaper_search_assets no longer searches SVGRepo or OpenClipart. Neither
answers automated searches any more, so they returned nothing. Asking for
either by name, or importing one of their asset ids, says so.
pinepaper_add_filter offers every filter the studio has, including dither,
CMYK and dot halftones, edge detection, HSL and colour tint, each with its
parameter ranges.opacity tints the
land. Highlight, colouring and get_highlighted report the regions that
were actually coloured and name any that are not on the map;
get_at_point returns the region under a point; add_marker adds the
marker it reports.pinepaper_agent_start_job fixes an unbounded canvas at the size it
reports, so a PNG export is the full canvas rather than a crop of the
artwork.Keyframes take three spring easings: spring (settles with no overshoot),
springSnappy (quick, one small overshoot) and springPlayful (bouncy, a few
visible wobbles).
The standard easings are here too: sine, quad, cubic, quart, quint, expo,
circ and back as easeIn…, easeOut… and easeInOut… (for example
easeOutExpo, easeInOutQuart), plus in and out bounce and elastic.
pinepaper_get_available_easings lists every name.
pinepaper_keyframe_animate and pinepaper_camera_animate also take a custom
cubic-bezier [x1, y1, x2, y2]: x within 0..1, y may go outside it for
overshoot.
Every easing renders exactly in video, GIF and APNG. Lottie and CSS get the
closest single curve, so springPlayful loses its wobble there and the in-out
quart, quint, expo and circ curves are within a few percent.
pinepaper_capture_frames takes sheet: many frames tiled into one small
image, saved to a file, with a rubric to score it against (hook, phone
readability, motion, variety, composition, brand, sound sync). Choose the
frames by time, every N seconds, a strip around a moment, or the loop's last
frame beside its first. Tiles are 360 px wide by default, the width of a
phone.
pinepaper_media takes analyze_reference: give it an uploaded reference
video and it measures the format: where it cuts, how long each shot runs,
each shot's colours, how fast it opens, and its beat. remake_from_reference
then builds a new piece on that structure in your own words. Nothing of the
reference's footage, words or sound is copied.
pinepaper_brand_kit takes from_url: give it a product's site and it saves
screenshots and the logo, and proposes a brand kit from the colours and fonts
the site actually uses, ready to review and apply. The site is read in a
separate browser context; your canvas is not touched.
pinepaper_design_medium takes apply: render any shape in watercolour, oil
paint, cut paper, thread or hatching, with that medium's own options. A medium
the studio cannot make, or one that is a brush rather than a fill, is refused
with the reason.
pinepaper_agent_export takes motionBlur for MP4 and WebM: each frame is
rendered several times across the shutter and averaged, so fast movement
smears the way it does on camera instead of strobing. It multiplies render
time; estimateOnly reports by how much before you commit to it.
pinepaper_generate and pinepaper_generate_estimate now tell agents to plan
the whole timeline and price the whole plan before the first paid call, to
reuse images already generated, and to pick video lengths the model supports
rather than generating long and cropping. pinepaper_beat_cuts treats a
generated song as the backbone of the edit, used whole.
pinepaper_agent_export always saves PNG, JPG and WebP to a file and
returns its path. Small images used to come back inline as base64, which
could exceed a client's response limit.pinepaper_execute_custom_code: when the code fails part-way, the items it
created are removed, so the scene is not left half-built. The error says
what was removed. Edits to items that already existed are not reverted. The
4-second loop limit is this tool's, per call: split long work across several
calls.pinepaper_design_medium apply_thread: the spine points format is
documented ({x, y} or [x, y]).pinepaper_world3d: colours take hex ("#03050c") everywhere: the world's
sky, fog, light and ground colours, and material and light colours. A
refused configure now says why, naming the key. ground_height takes
{x, z}. create lists the configurable parameters by name.pinepaper_refresh_page
and every other tool relaunch the browser when no page answers, restore the
canvas size, and say in the result that the canvas was reset.pinepaper_world3d extrude_path takes upright (stand a drawing up, like
a sign) and placement (centered, the default, stands the mesh on the
ground at the point you give). JPG and WebP stills of a 3D world scene show
the world rather than a white frame.pinepaper_modify_item takes atTime: the change is written as a keyframe at
that moment, the same as dragging with auto-key on, eased from the previous
value and without disturbing the rest of the animation.
pinepaper_query_mutations shows a property's values over time.
pinepaper_beat_cuts analyses a music track, fits a beat grid and returns cut
points on every beat, every bar or every N beats — and can split a clip at each.
When the tempo is uncertain it says so and lists the likely alternatives;
bpm or bpmHint settles it.
pinepaper_agent_export takes frames for an exact frame count (a duration
rounds up), for segments that must line up.pinepaper_import_image takes maxEdge (or 'native') and nativeSize, and
says when the studio stored an image smaller than its source.pinepaper_media upload_video takes fit (contain, cover, none), and
scale is documented as relative to it.pinepaper_accessibility_check judged text only at the current playhead, so
timed captions could pass unchecked; it now samples the timeline, and says
when no text was visible rather than passing.pinepaper_agent_export accepts fps and scale for APNG.pinepaper_media gains split (cut a clip in two at a time),
add_transition (a crossfade or a dip between two clips that meet), and
set_clip with timeOffset to move a clip, not only trim it. Uploads take
atPlayhead: true to start at the current playback time. A studio that cannot
do one of these says so rather than reporting success.
pinepaper_generate creates images with AI models in the PinePaper cloud and
can place the first one on the canvas; pinepaper_generate_estimate prices a
request and lists the models, free. It is charged per image, needs a service
account key (PINEPAPER_API_KEY), and is enabled per organisation during the
beta — without it the tools say so, and nothing is charged.
Words are never drawn by the model: pass them as input.text (headline,
subhead, caption, call to action, label) and they are added as real, editable
PinePaper text over the image, laid out inside the title-safe area and given a
dark backing only where the image behind them is too bright to read.
pinepaper_keyframe_animate takes append: true to add keys to an item's
existing animation — a button press after an entrance. Without it, a second
call replaces the animation, and the result now says when that happened.
pinepaper_agent_export takes deterministic: true for MP4 and WebM: the same
scene gives the same file, so exports can be checksummed or cached by hash. If
a run could not be pinned, the result says not to rely on the hash.
pinepaper_import_asset with a url failed on every call after fetching the
file successfully.pinepaper_sound render_soundtrack timed out on longer soundtracks; it now
has the same time budget as other exports.pinepaper_audio_beats answered success with no result for larger audio, and
could not read a local file path. Both work now.pinepaper_camera_animate without keyframes failed with a raw JSON error; it
now says what keyframes needs.pinepaper_precomp: create returned an internal id that no tool accepted,
and add / remove could not work at all. All three now round-trip the
precomp's item id, and a failed remove is reported.pinepaper_media set_time_remap, speed_ramp, match_cut and the track
matte actions accept the media id that upload returns, as documented.modify_item content on text whose content is keyframed now warns that the
keyframes override it.The "origami pine" is the new PinePaper mark. The package now ships it as its
icon (icon.svg, icon.png), and MCP bundle hosts that read manifest.json
show it.
pinepaper_agent_export with format: "webm" takes transparent: true (and
alphaQuantizer, 0–63) for a video with a real alpha channel, where the studio
supports it. The result confirms the alpha stream; if none was written, the
result says so instead of handing back an opaque file.
A PDF deck (pdf.pages) embeds the scene's fonts for its searchable text layer
and reports them in result.pdf.fonts. On studios that support it, each page
gets its own scene's fonts. Text hidden at the moment of rendering (outside its
bornAt / ttl window, or invisible) is named in the result, since it is in
neither the picture nor the text layer.
npx setups could never execute in a browserPuppeteer is an optional install, and under npx a separate npm i puppeteer
is not visible to the server. The README's new Quick start gives the npx
configuration that includes it; the error message leads with the same
advice.
fontWeight, strokePosition and other text-only properties passed to a
non-text item are reported as not applied again.
Many of the features below rely on newer studio capabilities. Where a studio lacks one, the result says so — the call never quietly claims what it could not do.
html5-ad export: a display-network zip (index.html with the
ad.size meta and a clickTag click target — the whole ad or a CTA's box)
plus a backup PNG beside it. The result checks the zip against a size budget
(ad.maxBytes, default 150 KB) and lists any external requests.playable export: a single-file MRAID playable whose CTA
(ad.ctaItemId) opens the store link through mraid.open, leaving the rest
of the ad interactive.pinepaper_render_batch: one creative per row of data — each row's item
changes or template params, then an export — with per-row files, warnings and
failures.scc captions: CEA-608 (broadcast) from the same timed text as SRT / VTT,
with notes on anything cut, dropped or timed late.pinepaper_accessibility_check: text contrast (WCAG AA) and flashing
(more than three flashes a second) as a quick screen before delivery.fit on text (create_item, modify_item, batch):
{maxWidth, maxHeight?, minFontSize?, maxFontSize?, wrap?, hold?} sizes the
text to the box, and later content changes refit it — the variant /
translation workflow. It holds the anchored edge (top-* holds the top);
fit: null stops fitting. The result's textFit gives the size, line count
and whether it fits.fidelity lists text_overflow for text past
its box or off the frame — the check to run before rendering a batch.direction (auto | ltr | rtl) for Arabic, Hebrew and other RTL
paragraphs; auto is reported with the way it resolved.tabularFigures for counters and prices that should not jitter, and
strokePosition: 'outside' for thick caption outlines that keep the
letterform.pinepaper_font gained a load action) now reach the text, loaded before
it is drawn.apng — animation with full transparency, for overlays and stickers.jpg / webp stills for byte-budgeted ad specs; srt / vtt
captions from the scene's timed text; multi-page PDF, one page per saved
scene, with a searchable text layer and print options (paper
size, orientation, bleed, trim marks, dpi).region exports a part of the canvas — carousel slices, sheets of cards.
A cell leaves out a neighbour's overflowing items and names the items that
cross its edge.time renders a still at a chosen moment rather than wherever the
playhead is.loop play count and a maxBytes budget (re-encoded smaller, with
each attempt reported).broadcast: true (BT.709, limited range, constant
bitrate), bitrate / minBitrate, and broadcastHeadroom. The result
reports the achieved bitrate and the measured luma; when the file would fail
broadcast QC, fidelity says so and gives the command that fixes it.print-a4-landscape and print-letter-landscape.pinepaper_lasso gained cut: cut a subject out along a polygon, no custom code.region.id; the same generator in
several boxes gives several instances.modify_item takes skewX / skewY and an affine matrix (mockups).smoothing: 'off'), a globe disable action,
keyed audio gain, and widget HTML lang, dir and alt.bornAt / ttl now work as a visibility window in local renders, so shots
hand over cleanly on the boundary.serverInfo.version names the exact build (1.6.14+<sha>).create_item and modify_item now name any property they could not act on,
with a suggestion for common misspellings. Beyond that, these used to be
dropped and now work: no-fill (null / 'transparent' / 'none'), dashes,
caps and joins on every shape, color on closed paths, a top-level anchor,
path coordinates kept when no position is given, mesh colour on 3D shapes,
template_params naming undeclared params, chart data in the documented
shape, and gradients that cannot be read (now refused, not drawn as nothing).
moves_along_path follows the path you name (or refuses); relations to an
id not on the canvas are refused.data: and server-side URLs upload in production,
and clearing the canvas removes uploaded media too.camera_animate no longer claims pitch / yaw as 3D perspective, and its
duration is optional.pinepaper_sound gained a sequence action taking [{t, spec|preset, note?, duration?}]. Every cue used to be its own call — a 112-cue bed cost 112 round
trips — and nothing about the work needed one each. Failures come back by
index, because a bed where three of a hundred cues didn't sound is neither a
success nor a failure.
It built shapes itself instead of asking the engine — a hand-written branch per
type covering circle, rectangle, star and text, and a 30px circle for
everything else. So ellipse, triangle, polygon, path, line and
arc silently came out as a small blue circle, and any property that branch
didn't name was dropped (which is why an ellipse ignored width/height while
the same values worked through agent_batch_execute).
It now creates items the same way every other tool does, so all 28 item types
work, along with every property they accept. Two related corrections: scene
relations accepted 8 types while the catalogue documented ~90 — following the
documentation failed validation — and scene animations gained startTime, so a
scene can be staggered instead of everything moving from frame zero.
The docs implied a layers[] array; the engine reads info.parts keyed by tag.
An array imports nothing. The description also implied pinepaper_animate
could blink — only an expresses relation drives blink/smile/talk, and asking
animate for it is a silent no-op.
pinepaper_agent_export with format: "pdf" died with "parameter 1 is not of
type 'Blob'" — an error naming FileReader and nothing about PDFs. The engine
resolves a record {blob, width, height, format} and this passed the whole
record where a Blob was expected. It now accepts either shape, reports the
page geometry the record carries, and when something really is not a file it
says so about the PDF rather than about FileReader.
pinepaper_validate answers {success: true, ok: false, diagnostics: [...]} —
the call worked and the scene has problems, which is the whole point of the
tool. An internal guard treated ok: false as a tool failure, so the result
became "the studio reported failure without naming a reason" and the
diagnostics were discarded. A refusal that carries diagnostics now names them.
It used to appear only when there was something to lose, so that an empty list could not be misread as "this format is lossless". That traded one misreading for a worse one — you could not tell "checked, nothing lost" from "never checked". All three states are explicit now, and the caveat lives in a note rather than in the absence.
pinepaper_agent_export also points at pinepaper_capture_frames as the cheap
way to confirm a scene is actually moving before committing to a render.
solveOde state variables are positional — t, y0, y1, y2 …
matching initialState. There is no x or y, so the natural way to write
a Lorenz system failed with "Undefined symbol y". The working form is now in
the description.moves_along_path with params.equation only works on the relation's
signal path. Easing bounce is piecewise and leaves that path, which
silently discards the equation and leaves the item where it started.grid.blocked in pathfind is a mask, one entry per cell, not a
list of blocked indices. Both are arrays of numbers, so the wrong one used to
produce a plausible "no path" instead of an error; the length is now checked.The browser was launched with --disable-gpu, carried in as a headless
convention. With it, webgl1 and webgl2 both report false in the page —
so pinepaper_world3d, drawShaderArt, drawFormulaArt and every shader aura
(liquid_metal, caustics, heatmap, gem_smoke, electric_arc, vortex)
could not run. Worse, some of them reported success: true and rendered
nothing.
The flag is gone, so Chrome uses the real GPU where there is one, and
--enable-unsafe-swiftshader covers machines without: Chrome no longer falls
back to software GL on its own, so without it a headless server gets no WebGL
at all rather than a slow one. Measured on the same machine, only the flags
differing: false/false before, WebGL 1 and 2 both available after.
A stress-test pass across six designs found the same failure repeatedly — a parameter the schema takes, validates, and drops before it reaches the engine. All of them now either work or say why not:
colorScale: "greens" was sent
where the engine indexes a two-stop ['#from', '#to'] ramp, so it indexed a
string. minValue/maxValue/showLegend weren't mapped either.style.fill.pinepaper_map load documented an options parameter the schema threw
away, so {style: "dark"} never arrived.time was capped at 0–1 while the engine reads seconds
— a keyframe at 2.0s was a validation error.visual.startTime was stripped;
one call places a cue now.text_effect gained startAt and hideAfter — every effect played from
t=0, so in a multi-shot video they all fired on the opening frame.import_asset by URL is implemented rather than refused.drawShaderArt documented fragmentSource without mentioning that it is
ignored unless you pass preset: "custom", and that the shader is GLSL ES
3.00 (in v_uv / out outColor), not gl_FragColor.[object Object] as the author, and claimed
"Various Licenses" for icons whose licence it had simply failed to read.Where the engine genuinely cannot do the thing, the tool now refuses by name instead of accepting the parameter and ignoring it: right-to-left text has no engine support, and a connector id you supply is dropped by the engine, so the result tells you which id you actually got.
pinepaper_world3d add_object with color: "#ef4444" reported success and
then made every later MP4 export fail with a WebGL error, because the
colour goes to a shader uniform that needs three floats. Hex is what every
other tool here takes, so the conversion happens for you now — as does
[255, 0, 0], the other common form.
pinepaper_keyframe_animate on an id that doesn't exist returned success and
animated nothing — the engine warns through a console this tool cannot read.
It now refuses and names the id.
1.6.12 stopped an imported image from tainting the canvas, and in doing so made
URL imports impossible on pinepaper.studio. The studio sends
connect-src 'self' …, so the page may not fetch any third-party host — the
old code got around this by loading the image instead, which is exactly what
tainted the canvas.
The fetch now happens in the MCP server, which has no such restriction, and the
page is handed a data: URL. Nothing to fetch, nothing to taint, and the
picture actually arrives. pinepaper_import_svg resolves its document the same
way, so embedded remote images are kept rather than dropped.
Large photos are handed over beside the generated code rather than inside it. A 3000×800 image inlined as a data URL made ~630KB of JavaScript, which the engine's code governor could not rewrite — it gave up and took the return value with it, so the import reported success with no item id while smaller photos worked fine.
pinepaper_text_style and pinepaper_execute_custom_code were callable but
absent from tools/list in the default toolkit — while pinepaper_text_effect,
pinepaper_design_medium, pinepaper_keyframe_animate and
pinepaper_execute_generator all tell an agent to use them. So the whole
15-style caption system was reachable only by someone who already knew the name.
Profiles filter by tag, so these fell out because of the group they belong to,
not because anyone decided they were unnecessary.
The default toolkit now lists everything its own descriptions point at, and
pinepaper_text_style accepts list as well as list_styles, because
pinepaper_text_effect spells the same action list.
Its description also now carries what a real render found: over photographs,
pass a palette — chrome, cutout and glitch are off-brief at the default
and neon needs one on dark images — and glitch at 80px monospace overflows
a 1920 canvas at 36 characters.
Three tools reported a pixel count that came from how the browser happened to be rendering, not from what you asked for:
framing: "camera" produced 2234×1472 whatever the canvas was — a
1920×1080 board and a 3840×2160 board both landed there.format: "png" ignored platform completely: both youtube-thumbnail
(1280×720) and auto on a 1920×1080 canvas returned 3000×1688, which is
the board scaled by 150/96 DPI.pinepaper_agent_end_job reported the canvas as 2233×1472.None of those ratios is the device pixel ratio, so the number could not be
divided back out afterwards — which is why it forced ffmpeg and sips
workarounds rather than a quick correction. All three now read the canvas size
you set, and PNG reports the dimensions it actually produced.
size on an effect is a multiplier, not pixelsEvery effect scales from the item's own bounds and size multiplies that, so
size: 30 asks for a disc thousands of pixels across and gets one, reporting
success. Typical values are 1.5 to 3. The parameter was not in the tool's
schema at all, so "pixels" was the only natural reading; it is documented now.
pinepaper_cutout_style called app.imageTools.applyCutoutStyle, which has
never existed — so it reported success with the real error nested inside. It
now uses the engine's actual cutout API and resolves the item id first.
pinepaper_lasso has no headless form at all: the lasso is a mouse tool whose
extraction is driven by strokes a user draws. It now says so and points at
pinepaper_extract_object, instead of failing with
undefined is not a function.
An error carried the whole generated script in its details — ~12KB of
JavaScript in front of a one-line failure. It now carries the script's size and
a switch (PINEPAPER_ECHO_CODE=1) to get it back.
pinepaper_agent_end_job returned a 263,000-character screenshot inline, which
is over the tool-result limit of the clients reading it, so an agent could not
read its own verification step. The threshold for saving to a file instead was
set against the bridge's limits rather than the caller's, and is now low enough
that this cannot happen.
Nine tools called engine methods that have never existed on any studio —
pan_map, zoom_map, export_map, add_map_labels, animate_map_wave,
region-at-point, history get_state, trigger_action, and the relation lookup
used by scene analysis. The last one was the quiet one: it was guarded, so it
never threw — it just reported every scene as having no relations, always.
Where the engine has the same capability under another name, they now call it.
Where it has no such capability at all — there is no lat/lon pan, no numeric
zoom level, and no region hit test — the tool now says so by name and points at
what does work, instead of failing with undefined is not a function.
Two separate bugs meant no photo could be placed into a scene over MCP.
pinepaper_import_image failed on every URL with a syntax error before it ran — an apostrophe in one of its own error messages closed a string early. And pinepaper_import_svg with an external <image href> reported success, created nothing, and then broke every later export in the session with "Tainted canvases may not be exported", including scenes that never touched the image; only a page reload recovered it.
SVGs now have their remote images fetched and inlined before importing. One that can't be fetched has its element removed and the removal named in imageWarnings — a missing picture is visible and recoverable, a tainted canvas is neither.
A tool that takes a parameter, validates it, and drops it is worse than one that refuses it, because the caller has no way to tell. This release removes every instance found:
radiusX/radiusY — the pair documented in this package's own examples — and built the default size instead. They now map to the axes the engine reads.pinepaper_stick accepted eight: propSide: 'right' put the prop in the left hand (the engine compares against 'L'/'R'), walk: true walked nowhere, expression beats keyed name were dropped entirely, and facing did nothing on a figure — it is now refused by name rather than ignored.pinepaper_load_map accepted seven the engine never reads. Style keys nest under styles and are spelled differently there; enableHover/enableClick are interactive/selectable.hoverStroke was documented by the engine and implemented nowhere; it now reaches the renderer.pinepaper_agent_end_job also reported the canvas as 2233×1472 for a 1920×1080 artboard — it was reading the backing store, whose ratio to the artboard is not the device pixel ratio and so cannot be divided back out. It reports the artboard now.
anchor — say which corner your x/y meantPosition has always been the bounding-box centre. Anything that computed a layout box authored the top-left, so passing those coordinates displaced every item by half its own size. anchor: 'top-left' (also top-right, bottom-left, bottom-right, center) states which corner you meant, applied after size and rotation so it uses the item's real box. A misspelled anchor is now refused rather than silently falling back to centre.
pinepaper_stick gained gait (9 named walks — trudge leans away from where it's going, sneak crouches), poses as a track over time, and named sequences. Every valid name is read from the engine's own kit, so the list cannot drift.
Exports now report what this scene loses to the format you chose — a PNG of an animated scene is one frame, a GIF drops the soundtrack — on both estimateOnly and a real export. It appears only when there is something to lose: its absence is a statement about your scene, not a claim that the format is lossless.
pinepaper_list_generators reported 3 generators when the engine has 74, and dropped the parameters it promised to list. The batch tool advertised 15 of those 74, so 59 working generators were invisible to anyone reading it. All lists now come from one source.
Loading a template that carries clipped character parts now warns when the studio dropped them, and says not to save over the template — the file is still intact until you do.
triangle was documented as taking color and nothing else, while the engine accepts width, height, and either kind ('right', 'equilateral', 'obtuse'…) or exact interior angles and builds the triangle by the law of sines. polygon accepts an explicit vertex list for irregular shapes. ellipse and heart were color-only. Six of the eight speech bubbles take cornerRadius. And every item type accepts a label — a string, or a full config with position, offset, font and colour — so captioning no longer needs a second call and hand-computed coordinates.
Two silent fallbacks are now stated: an angle set the engine rejects gives you the default isosceles with no error, and the vertex-list polygon uses absolute coordinates and ignores position.
platform defaults to auto, but auto wasn't a real preset — the lookup fell through to the web preset, so an export that named no platform silently rendered 800×600 over whatever size your canvas actually was. Measured: three exports of a 960×540 canvas all came back 800×600, which reads from outside as a dimension mismatch with nothing saying the canvas had been replaced.
auto now means the canvas's own size, read at export time, with the preset kept only as a fallback for older studios. Naming a platform still uses its preset exactly as before — and platform is no longer required, so you can export at your canvas size without picking a preset that overrides it.
fps and scale were accepted but undiscoverableBoth parameters worked and were documented in the tool's own text, but were missing from the machine-readable schema — which is the only part a model actually reads. So callers were told to use them and had no way to find them. Published now, along with quality's description admitting that it carries a frame rate (15/30/60) as well as compression.
device-mockup, recursive-subdivision and textile-weave existed upstream but had never been copied into the package.
pinepaper_modify_item selected an item by id and then edited the selection. When the id didn't resolve, the selection didn't change — so the edit landed on whatever was selected before, and the call reported success. It now addresses the item directly and tells you when the id isn't there. The same bug in pinepaper_agent_batch_execute's modify operation is fixed with it.
Decorative font files are fetched when you hover the style grid in the editor — a gesture no automated caller makes — so a style asking for one of those faces silently drew a fallback and reported success. Styles now load the faces first, and say so when a face still isn't ready instead of leaving it to be discovered in the export.
The generated code used a top-level await outside an async function, which some studio builds refuse to parse — so the import never ran at all on those. It also now reports why a URL failed: a refused connection, an HTTP status, or a response that's a web page rather than an SVG.
set_canvas_size reported the size you asked for even when the studio refused it — an unknown preset or an out-of-range dimension left the canvas untouched. Scene chains' toggle_loop called a studio method that has never existed, so a chain built without looping went on not looping. And pinepaper_get_items and friends reported rotation: 0 for items that are visibly rotated.
embroidery_tapestry and woven_textile existed in the design vocabulary but not in the list the tools return. The list is now derived from the vocabulary itself, so it can't fall behind again.
get_animatable_properties answers for an itemPass an itemId and it reports what that item can animate, from the studio's own table — including which properties are discrete. content and blendMode hold their value until the next keyframe rather than blending toward it, and treating content as tweenable is what makes a word cascade land half a beat early. Without an itemId it still answers the older mask-type question.
The tool descriptions now explain the draw contract: you can define a generator as an app method through pinepaper_execute_custom_code and invoke it with app.executeGenerator, with no registration step at all.
More importantly they explain the decision that registration is. app.registerItem(item, type, props) is what makes something editable — the person using the studio can click it and change its colour, and tools can modify, animate, keyframe and relate it. An unregistered path is pixels: it renders and exports identically and can never be touched again without redrawing. Built-in generators register some of what they paint and not the rest — drawSunsetScene 6 of its 27 objects, drawPattern none of 15 — which is a memory trade, and one you make yourself when you write your own generator.
The 1.6.9 note said letter-collage animation needed a newer studio, and 1.6.10 said the same of the font check and fallbacks actions. Both studio updates have since shipped, so both work.
Every diagram tool — shapes, connectors, ports, auto-layout, diagram mode — called app.diagramManager, which has never existed in the studio. All nine failed with "Cannot read properties of undefined", which read as "diagrams are broken" when the studio side was fine. Fixed, along with three argument bugs behind it: shape type is passed separately from its config, add_ports takes a port-type name, and auto-layout takes items rather than ids and is awaited.
update_connector now applies colour and width directly and refuses the rest by name, pointing you at remove-and-recreate. It previously reported success over a connector it had not changed.
set_name, get_required_chars, get_status, create_space and export all called renamed methods and failed. show_studio now says plainly that opening the panel has no engine entry point — the other fifteen actions work without it.
pinepaper_import_image now accepts a local file path (absolute, relative, or file://) as well as http(s) and data URLs. A browser page cannot open file://, so the server reads the file and hands the page the data.
Remote failures now report the reason — HTTP status, refused connection, or a URL that returned a web page instead of an image — instead of a bare "Failed to load image from URL".
Any tool whose underlying operation failed could return success with the error buried in the payload. A failed operation is now an error. This is the general form of the export fix in 1.6.9, and it covers every tool.
Map, physics, rigging, sprite-sheet and Lottie tools could report the studio as too old for a capability it has, if called within the first second or so of connecting. They now wait for the relevant subsystem to finish loading.
pinepaper_batch_modifyIt passed item ids in the field meant for live items, so every modification was rejected, and then misread the result as a list. It now reports how many items changed and names any it skipped, with the reason.
add_bone and inline poses document degrees, and the solver read the number as radians — so a documented 90° came out as 90 radians and characters came apart on the first pose. Angles are now converted.
Two things the tool never told you, now in its description: a bone angle is relative to its parent, and a bone has no position — it starts at its parent's tip, so length and angle are what place it.
pinepaper_precomp and pinepaper_import_mermaidprecomp passed ids where live items were required, and now names any id that does not resolve instead of quietly leaving it out. import_mermaid returned raw canvas objects that could not be serialised, so a working import reported a failure; it now returns ids, labels and bounds — and an import that creates nothing is no longer reported as a success.
The animation type was discarded on every call (fixed in 1.6.9) and the studio was skipping the relation that drives it. Both halves are in place. Needs a studio build from 2026-09-20 or later.
A successful tool call no longer reads your own request back to you. On a 50-item batch create that was 8,740 characters of echoed source against 983 characters of actual result — and it grew with every item you spelled out, which is what made enumerating through tools far more expensive than writing the equivalent loop.
The generated code is still returned where it is the point: PINEPAPER_EXECUTION_MODE=code, and every error, which carries the code as evidence. PINEPAPER_ECHO_CODE=1 restores it everywhere if you are debugging.
pinepaper_execute_custom_code documents the trapsWriting JavaScript opts out of the schema protection tool callers get, and six engine behaviours fail silently when you do: app.create returns a numeric Paper.js id where everything else wants the item.data.registryId string, an unknown item type creates nothing, addAnimation ignores a JSON-string argument and an unresolvable id, keyframes live at item.data.keyframes, and transformOrigin is ignored so rotation always pivots on the item centre. All six are now in the tool description.
content is keyframeable, and it holds rather than tweening — pinepaper_keyframe_animate now says so, and points at the cheaper route for a cascade. For a word cascade it points at app.textSequence(item, words, { interval }): one item cycling its words in one call, instead of one item per word with opacity tracks faking the cut.Two different races wore the same symptom, and only one of them was already handled.
Some subsystems are code-split and arrive about a second after boot; those are now waited for by name, which matters because the studio's bulk loader deliberately never fails — waiting on it meant "the prefetch finished", not "the thing I need is here". Others, like Font Studio, are built during startup itself, so no amount of module loading helps them; those now wait for the editor to finish initialising. Both waits are bounded and apply only to calls that actually touch such a subsystem.
set_font_axes declared the three standard axes and silently discarded anything else, so asking for an axis the tool didn't name reached the studio as an empty request — which then reported nothing rejected, and the tool reported success over axes that were never applied. Axes are passed through as written now; the studio says which it took and which it did not, including OpenType tags like wght. And nothing applied while something was rejected is an error rather than a success.
quality no longer hides what it changesquality sets three things: draft = 15fps/72dpi, standard = 30fps/150dpi, high = 60fps/300dpi. So asking for high doubled your frame count and render time against standard, with nothing saying so. That's now in the tool description, and fps is a parameter — as scale already was for resolution — so you can raise compression quality without also doubling the frames.
pinepaper_agent_batch_execute wrapped each operation in try/catch and treated not throwing as succeeding. Most failures here do not throw — a missing preset, an unloaded subsystem, an unmet precondition all come back as a returned error — so the operation reported the problem correctly and the batch said the scene was built.
A failed operation now fails the batch, and the result names which one and why, so a failure in a sixty-operation batch does not mean reading sixty entries. apply_effect also discarded the engine's verdict entirely and returned its own; it passes failures through now.
pinepaper_font gains three actions that answer questions the tool could not answer before:
list_available — every font family the studio can render, optionally filtered by category, each marked with whether its file has loaded yet.check — is a family really there, and can it draw your string? Those are different questions: a font with four glyphs is installed and will still render most characters in a fallback face, and the second answer is the one that decides the pixels. Pass the text you mean to draw.fallbacks — which text items on the canvas are silently rendering in something other than the family they asked for.The rest of that tool authors a typeface. These three are about using one, which previously had no answer short of reading the server's source.
check and fallbacks need a studio build from 2026-09-20 or later and say so plainly if yours is older; list_available works today.
pinepaper_execute_custom_code could not do anything asynchronousYour snippet was wrapped in a synchronous function. A top-level await is a syntax error inside one, so any snippet that awaited something failed to parse — and async work started without await was simply dropped, with the tool reporting success before it happened.
That is the whole async half of the engine — export, image import, rigging bake, layout — unreachable from the one tool that exists for reaching what the other tools do not cover. Snippets now run in an async wrapper, so await works and the result is the resolved value.
pinepaper_get_items, get_by_id and get_dimensions reported rotation: 0 for items that are visibly rotated. Creating an item with a rotation bakes the angle into its geometry, so the property really is 0 — and reporting that is why rotation got filed as broken against an engine that rotates correctly. All three now report the true angle.
Every engine call this server emits is now checked against the studio's actual API before release, including calls made through sub-objects. That check found and fixed nine more dead calls beyond the ones reported, including one in the agent guide that was teaching a method that does not exist.
pinepaper_export_storeRecover an export the studio is still holding, instead of re-rendering it.
list — what is held: {id, format, size, createdAt}, newest first. IDs survive a page reload.save — page it out to a file and release it. Returns a filePath, and is safe to repeat.release — drop it without saving.Reach for it when an export fails with Failed to write data to data pipe — that is the browser's transport, not the encoder, so the bytes are often already stored — or when an error names an exportId.
Retention: held exports last until released, or until a later export needs the space, and eviction drops the oldest first. Save or release each export before starting the next one.
scale on pinepaper_agent_exportRender video at a fraction of the platform preset (0.1–1).
scale: 0.5 with quality: "draft" is a fast look-check before committing to a full render.mp4/webm/gif) — passing it to png/svg/pdf/wav is rejected rather than ignored. Omitting it leaves exports exactly as before.list_motion on pinepaper_design_systemReturns each licensed design system's motion curves and its durations; previously only the curves were reachable. Durations come back in milliseconds and in seconds.
These are the design systems' motion scales — Material's duration ladder, and so on. Aesthetic styles such as bauhaus_geometric carry no motion data of their own, so pair a system's motion with a style.
list_styles now returns style tokenspinepaper_design_system list_styles returns each style's palette, background, font stacks, type sizes and variants, not just its name. Building in a style by hand no longer means reading the generator source to find its colours.
compose placed everything off-centrecompose positioned items by their top-left corner while the canvas positions by centre, so every composed scene was displaced and left-aligned headlines ran off the edge of the canvas. Composed scenes now land where the layout intends.
pinepaper_agent_export could return success when the export itself had failed, so a failed render passed silently and a batch could lose chunks without saying so. A failed export is now an error, and names a recovery path when the failure looks like a transport problem.
Video exports running longer than about ten seconds failed with a PP_TIMEOUT that looked like a problem with the scene.
PINEPAPER_EXPORT_TIMEOUT sets the budget (default 300000 ms). For exports over five minutes, raise PINEPAPER_TIMEOUT to match.PINEPAPER_GOVERNOR=off is an escape hatch for older studio builds.Exporting immediately after connecting failed with a message blaming the studio's version. The export now waits for the studio's export subsystem to finish loading.
A read failure partway through paging a large export left the file unrecoverable. Reads now retry at a smaller size, and anything still held can be recovered with pinepaper_export_store.
Connecting could burn the full timeout against a studio that was already usable, and the connection had no configuration.
PINEPAPER_WAIT_UNTIL — what a navigation waits for (default domcontentloaded; set networkidle2 for the previous behaviour).PINEPAPER_TIMEOUT — navigation and readiness timeout in ms (default 30000).PINEPAPER_PROXY — routes Chrome through a proxy, for origins that reject headless Chrome. Setting it also relaxes certificate checking, which an intercepting proxy requires.Connections now wait for the studio's API rather than for its canvas library, so "connected" means tool calls will work.
pinepaper_animate_letter_collage ignored its animation typeThe requested animation type was discarded on every call. This fix also needs a studio update — the other half is an engine change that has not shipped yet, so collage animation stays static until it does.
pinepaper_create_item documents properties: { rotation: 45 }, and that rotation is baked into the geometry, so reading item.rotation back returns 0. Verify a rotation by geometry or pixels, not by re-reading the property.pinepaper_keyframe_animate points at cheaper routes before you author hundreds of keyframes: the time_expression relation for motion that is a function of time, staggered_with / wave_through for a whole group, and pinepaper_execute_custom_code for procedural scenes.Long-form export stops crossing the bridge as one string. pinepaper_agent_export handed the bytes back as a base64 data URL. Two things were wrong with that and only one of them was a bug.
{streamed:true} marker rather than a Blob, and FileReader.readAsDataURL throws on it — so any export that streamed broke this path instead of extending it.filePath — the only real limit was the single page.evaluate return value. So the paging belongs inside the tool: the studio holds the encoded file in its export store and returns an id, and the server pages it into the file it was going to write anyway. One tool call, one filePath, at any size, with at most one 4 MB chunk in memory on either side. An agent-facing pager would only have exposed a transport limit the agent cannot act on.duration goes from a 60-second cap to 600. Ten minutes was unreachable regardless of how the bytes came back, so the cap was the other half of the ceiling. GIF keeps its own 15-second cap: it has a different encoder and does not stream.exportToStore / readExport / releaseExport, which are live in the engine. An older studio takes the buffered path exactly as before, and both of its failures are now named instead of fatal: the marker says it streamed and there is nothing to page it from, and an export past a 96 MB inline ceiling gives its size, the ceiling and what to do. That ceiling sits above what the buffered path could already deliver, so nothing that worked before starts refusing.pinepaper_instantiate_ontology builds through the studio's own compiler. The server-side port placed items as boxes — no paint, no text, no keyframes, no masks. The engine's compiler reads every node facet and builds through the template loader's own paths, which is the round trip the studio's pixel tests verify. The box loop survives only as the fallback for studios that predate the facade, and says what it lost.
imagesFailed among them — so a raster that never decoded read back as success: true with a full itemIds array. All the counts are surfaced now, and imagesFailed alone is enough to make the call unsuccessful, whether or not the compiler also logged an error diagnostic.The design graph keeps the recipes for everything code drew. Three kinds of node have no geometry to round-trip, because code drew them and the code's inputs are their geometry. The graph kept a generator's name and not its parameters — enough to name a generator, not enough to re-run one.
generator, generatorParams and generatorRole; a mesh carries meshProvenance (op, the node it was derived from, and the options); the 3D stage rides at document level as world. All of it reaches the JSON-LD.pp:world is a sibling of pp:generator, never nested inside it. A World3D scene has no generator, so a stage read inside if (generator) is dropped from exactly the scenes that cannot rebuild without it.Relation presets are adoptable. pinepaper_add_relation takes a presetId — a community motion someone already tuned, with its symbols exposed as knobs — instead of making a caller name a type and guess at params. The preset decides the relation type, and any symbol you omit takes the midpoint of its declared range, so a preset applies sensibly with no values at all. List them through the new catalogue, which also returns what this build rejected: a preset for a relation type this engine does not have is a named absence, where dropping it silently would read as the catalogue simply being smaller.
pinepaper_query_capabilities gains catalogue — the eight registries the engine's own aggregate does not gather: rig presets, shader effects, stroke decorations, the precomp list, the image library, segment-edit kinds, shatter orders and world meshes. Each was a capability an agent had to guess at, which by this project's rule is the same as its not existing. A missing facade is named rather than answered with an empty list, because an empty list reads as "none exist" and that is a different, worse answer.
Four more reachable things, and a note on the ones that turned out not to be gaps. pinepaper_text_style gains cursive (text written as a stroked path rather than set in a glyph, so draw-on animation and outline_stroke both apply to it), wrap/unwrap (break a text item to a width, reversibly), and to_collage (convert an existing text item in place). pinepaper_equation_path gains solveOde — integrate an ODE and get the trajectory back as data rather than a drawn path, to inspect, feed to a path, or drive keyframes with.
What was not a gap is worth saying too, because it was on the list: createFoldedText, createGradientText, createMagazineText, createPaperCutText and createTileText are one-line wrappers over the letter-collage styles this server already offers, and plotFunction/plotParametric/plotSurface wrap three generators it already exposes. A method-name diff cannot tell "no tool reaches this" from "a tool reaches it under another name."
Animated character parts no longer jump the moment they start moving. pinepaper_character places each part at its path's bounding-box centre — the point PinePaper's position names — while the keyframe tracks were anchored on the depiction's declared anchor. The two are not the same point, so every animated part snapped between them as soon as its t=0 keyframe evaluated: measured at 19.6px for a pigeon's beak, which detaches it from the head. A track is relative motion around a base, so re-basing it on the centre the part was actually placed at preserves every displacement and removes the snap — now 0.000000px, with a test that also checks the performance did not flatten into a constant in the process.
A concept that inherits performs and inks like the one it inherits from. Two defects in the same resolver, both measured. Channels were read off the concept alone, so pp:Bus — which composes pp:Vehicle's parts and inherits its jounce and travel — returned zero tracks with every beat reported ignored, while claiming success. And ink was keyed on the concept requested rather than the depiction actually composed, so pp:Bus drew 0 stroked parts where pp:Vehicle, the same geometry, drew 3 — the "flat art dropped into an inked scene reads as pasted in" failure the ink exists to avoid, hitting exactly the concepts that inherit rather than declare. Both now match their parent measure for measure, pp:House included.
pinepaper_character stops silently ignoring four arguments it documents. position, scale, palette and ink were all handled by the resolver and absent from the schema, so zod stripped them and a call written the way the rest of this vocabulary reads landed dead-centre at the default size — the precise outcome the resolver's own comment says must not happen.
Two production advisories cleared, both found by validating the release rather than by anything failing. fast-uri (HIGH — host confusion via skipped IDN canonicalization, SSRF via malformed IPv6 normalization, SSRF via repeated hostname percent-decoding, host confusion via percent-encoded scheme normalization) and hono (MODERATE — toSSG() writing outside its output directory, unbounded dot-notation nesting in parseBody(), a query parser reading past the URL fragment). Both arrive transitively and are pinned to floors in overrides, so neither resolver can drift back; both lockfiles re-resolved and npm audit --omit=dev reports zero.
pinepaper_design_medium's thread surface is now the whole engine's, and the second tool over it is gone. pinepaper_create_stitchcraft was removed: all six of its presets dispatched into the same ThreadPainting module apply_thread already reaches — embroidery_satin → satinBetween/fillRegion, running_seam → runningStitchContour, cross_stitch → crossStitchFill, stem_outline → stemAlong, seed_texture → seedFill, needlepainting → fillRegion. Two tools over one engine is exactly the drift this surface keeps paying to remove.
apply_thread now offers all six stitches the engine publishes, not four: runningSeam and crossStitch were missing, and they are what a caller reaching for stitchcraft wanted. It also gained the per-stitch parameters STITCH_OPS declares and nothing named — slant, overlap, stagger, gapLen, gridSize. slant is the one that matters most: a satin fill laid at a slant is what separates embroidery from ruling.roughness is hand wobble, applied by the tool after the engine lays the stitches, since the engine has no such option. Every point of every stitch takes its own phase off the seed, so a long contour wobbles along its whole length rather than shearing at one end, and a given seed sews the same irregularities every run.pp:Stitchcraft points at pinepaper_design_medium now.pinepaper_compose unable to ask for two stitches apply_thread could. The engine had the identical split for the identical reason on the same day: MEDIA.thread.stitches was hardcoded to the same stale four while STITCH_OPS published six. Two repos, four copies, one list, drifted the same direction — because neither read the published table. It is THREAD_STITCHES, declared once and spread, with a test asserting every served copy matches.Two corrections to this note, made in place rather than quietly edited.
The removal was first justified as a tool "built on a phantom", because app.applyStitchcraftToItem had zero hits in the engine. The grep was accurate and the inference was not, twice over: the capability is real — it lives in js/core/StitchcraftPresets.js, imported as a module by the code runner and never exposed as an app.* method — and it returned zero hits only because that file was unpushed when the search ran. A missing method and an unpushed commit look identical to a grep and call for opposite responses. The redundancy was always the real reason, and it is the better one.
The replacement enum was then built by scraping quoted strings out of ThreadPainting.js, which is not the same thing as reading what it publishes. STITCH_OPS is the vocabulary, and it holds exactly six names. The scrape added eight that are planner functions and field kinds — satinBetween, fillRegion, radial, spine and the rest, every one of which applyThreadPainting refuses — and it dropped runningSeam and crossStitch, which are real. For one commit this surface advertised eight stitches that error and hid two that work, while removing the tool that had offered them. Enum parity means diffing against the published vocabulary, not against the file.
New tool: pinepaper_design_system — the design vocabulary as data, and eighteen styles that build a scene. Two different things, kept apart because they are not the same claim:
list_easings returns every motion curve across all of them as a named easing with its licence — and with pinepaper:authored on the three we had to fill in, so a vendor's published curve and one of ours are never mistaken for each other.compose a titled scene. draw: false returns the scene and the ops as data and draws nothing — read the composition before committing to it, the way pinepaper_story's distill works.This runs in the server, not the browser: a design decision does not need the canvas to be open. The data is vendored from mcp-cloud by scripts/sync-design-systems.mjs, the same shape as the engine's stick-figure sync — one authored copy upstream, a mechanical mirror here, a sha256 in every header, and a provenance test that fails on a hand edit. A partial composition reports as a failure naming the elements that did not create, rather than as a success with half a poster.
New tools: pinepaper_stick and pinepaper_story.
pinepaper_stick is the vendored stick-figure kit — a rigged figure posed, walking, travelling, holding a prop, with a garment and hair and a timeline of expressions, plus the floor and wall it stands on. Distinct from pinepaper_character, which places a figure from the design graph by concept. Its options were discoverable only by reading the engine's source. The geometry is vendored from mcp-cloud, which makes it a three-repo artifact, and the failure message says so — the fix for a missing kit is in another repository.pinepaper_story turns prose into a scene. distill reduces an article to its beats and draws nothing, which is the half worth having on its own: read what the distiller made of the text, edit the beats, and only then assemble. Discovering that reading by looking at a finished scene is the expensive way round.New tool: pinepaper_interchange — Lottie, dotLottie, GLB, BVH and PNG sequence. pinepaper_agent_export covers the platform formats sized for Instagram or YouTube; these are the ones another tool reads, and they had no tool at all. Import is here too, because a format you can only write is half a bridge.
exportGLB and exportBVH refuse through console.warn and a falsy return, and the production build strips the console — over MCP that is a silent nothing. Both preconditions are checked before the call and named: "no perspective objects to export — create one with createObject3D first", "no rig to export — create a skeleton first".New tool: pinepaper_sound — synthesis, and the drawing that is the same object. The engine carries a whole audio graph and nothing reached it; pinepaper_audio_beats analyses audio that already exists, and this makes audio.
from_text resolves "a soft warm bell on A4" to a spec without playing it, so a caller can inspect or edit before committing.create draws a sound as a waveform path: edit the path and the timbre changes with it. timbre_from_path reads any drawn path back as harmonic content — a curve someone drew by hand becomes the partials of a note.timbreFromPath answers a flat single-partial timbre for anything that is not a path with length, which is a legitimate timbre and the refusal, indistinguishable once returned. So the item is checked before the call and a non-path is refused by name.New tool: pinepaper_motion — the generators' Animation knob, pointed at anything. The motion engine every generator's own animation runs on was reachable from the code console and from custom generators, and from no tool call.
pinepaper_animate applies a loop preset to one item.action: 'list' returns the engine's own catalogue. motion is deliberately a free string rather than an enum: pinning the names here is exactly how add_relation came to list 39 of the engine's 134 relations.random (every element on its own phase — a twinkle, not a wave) and roam (hopping between the corners and the centre); pass a seed to make random reproducible.spike is a narrow bump. It was called pulse until the engine noticed one word was naming both a waveform and an animation; saved scenes still carry the old spelling and it is still accepted, but it is not offered as a choice.New tool: pinepaper_path — the destructive path operations Paper.js has always had and nothing here could reach. Booleans (unite, subtract, intersect, exclude, divide), simplify, outline_stroke, toggle_closed, repeat patterns (concentric, radial, grid, extrude), get_geometry, and a lock.
pinepaper_modify_item on purpose: modify restyles an item and keeps its id, while every action here changes what items exist. A boolean consumes its operands — they stop existing, and their relations and keyframes go with them. The tool description says so, because a caller who reads a boolean as a restyle loses work.outline_stroke is the one worth knowing about: it turns a stroked line into a filled shape of the same width, which is what you do before exporting to a format with no stroke model, or before a boolean against a line.get_geometry, whose facade returns a bare null; that one place supplies a wording.pinepaper_world3d gains the mesh half, the import half and the physics half — 33 actions. The graph learned to record meshProvenance above, and nothing could author a mesh: it could describe a thing the tool surface could not make.
extrude_path sweeps a canvas path along a depth; lathe_path revolves its profile. The path itself is untouched — the mesh is new, and the engine records the op, the source path and the options as the mesh's provenance, so a captured scene can rebuild it rather than merely recognise it. The option names on this action are exactly the names in that record, pinned by a test, so meshProvenance.opts spreads straight back in.add_light / set_light / remove_light / list_lights) and shared materials (add_material / set_material / remove_material / list_materials), plus list_meshes and remove_mesh. At most 8 point lights exist at once: the shader array is fixed-size, so a ninth is refused by name rather than ignored.rotY is radians, and says so. It is stored raw and handed to cos()/sin() in the shader with no conversion anywhere on the path — an exception to the degrees convention the rest of this surface follows, and the sort of thing that silently produces a scene rotated by a factor of 57. rotYDegrees is the convenience spelling and is folded in before the engine sees it. (arc on lathe_path is degrees; the engine converts that one.)import_obj and import_gltf bring real geometry in; list_mesh_clips and set_mesh_clip play what a skinned model arrived with. set_nav_target decides whether the pointer drives the 2D canvas or the 3D world, ground_height and canvas_to_ground and world_to_canvas convert between them, raycast picks, and dolly_camera / pan_camera move the view.pinepaper_physics (which is the 2D physicsWorld) and deliberately narrower: a sphere or capsule under gravity, colliding with terrain, objects, actors and imported meshes. No stacking solver and no joints. step_physics reports the contacts made and the bodies removed — and returns {contacts: [], removed: []} with no world, which is exactly what a quiet step returns, so the guard is what separates them.null — or 0 — when there is no world, which is indistinguishable from a real answer: ground really can be at height 0, and a ray really can hit nothing. Each is guarded ahead of the call, so "there is no world" never arrives disguised as a measurement. Once past the guard, a null from can only mean a miss, and is reported as one.attracts was live in the engine and unnameable here. The relation-parity guard compares the tool enum against a fixture of the engine's relation map — but the fixture is copied by hand, so the guard passed while the engine moved. The fixture is refreshed and attracts is callable. The exclusion list that keeps tool-emitted families (deform_*, effect_*, geo_*, the rigging set) out of add_relation is unchanged, and still written down with its reason.
pinepaper_character was defined and dispatched nowhere. It shipped in 1.6.7 as a schema with no handler behind it — the worst version of the problem this project keeps naming, because the tool listed. It is wired now, and a test fails the build if any defined tool has nothing dispatching it.
Four chart types classified as pp:Group. The graph's type lookup lowercased the item type before consulting its map, so every camelCase key in that map was unreachable — barChart, lineChart, scatterPlot and areaChart answered pp:Group instead of their own types. Nothing errored; a caller got a readable graph of the wrong thing. The lookup tries the key as spelled before folding case, and a test now asserts every key in the map is reachable, so the next camelCase key the engine adds fails a test instead of quietly answering pp:Group.
The error path could throw while reporting an error. Canvas-state capture promises never to throw and returns whatever the studio handed back, but the formatter read its fields unguarded — so a partial state replaced a real failure with a TypeError from the error path itself. Every field is read defensively now.
The instrument catalogue stopped being a ceiling, and the soundtrack can be rendered. Five audio facade methods landed in the engine after pinepaper_sound was written, and the tool could list and play the seventeen built-in sounds and nothing else — so a rhodes, a koto or a taiko was unreachable, while the engine's own comment calls those tables "a curated starting set, not a claim that music contains six instruments."
define_instrument, define_percussion and define_sfx register a sound at runtime. It is first-class from then on: listInstruments() reads the live table, so it appears in the list_* catalogues and plays by name like a built-in. The response returns the canonical name — registration normalises to [a-z][a-z0-9_-], so a model that registers Rhodes and plays Rhodes is naming something that does not exist.pitch: {from, tau} is carried through — the glide is the drum, and without it a defined kick comes out a beep.pinepaper_agent_export takes format: wav — the soundtrack on its own, off the visual path. No platform preset resolves to it, so a request for a picture can never come back as audio; it has to be asked for by name. framing: camera and the audio knobs on a visual format are rejected rather than ignored, because silently dropping them leaves a caller believing something untrue about the output, and estimateOnly reports confidence exact — uncompressed PCM size is arithmetic, and a model would only be less true. The empty case distinguishes two things the engine distinguishes: a scene with uploaded audio renders to nothing here (the offline renderer bakes only what it can synthesize) and is routed to mp4/webm, which mux it — telling that caller "no audio" would be false.render_soundtrack mixes every placed sound to a WAV offline — no Web Audio, no playback, which is what makes it work in a headless studio — and writes it to a file through the same path video and PDF take. An empty scene is refused by name instead of producing a silent file, and any sound that could not be mixed is reported as dropped: a soundtrack missing a track otherwise looks exactly like a complete one.Eleven shapes the engine draws were reachable from nothing. FxTool's ShapeRegistry registers 34 shapes and create() delegates any of them. Flowchart, UML and network route through pinepaper_create_diagram_shape, so 23 were callable — and the other eleven appeared in no tool enum anywhere: disk, circle-outline, arrow-right, and the entire annotation set (speech-bubble, speech-bubble-square, speech-bubble-pointed, thought-bubble, comment-box, callout-box, double-bubble, quote-bubble). The renderer was never the limit: create() already forwards tailDirection and tailSize, parameters that exist only for bubbles. So an agent asked for a comic panel or an annotated diagram had to hand-build the outline or decline, for a shape the engine had the whole time. itemType goes 17 → 28, each one documented with the engine's own description rather than a guess, and the emitted code is executed against a stub in the suite — reading a spread operator and inferring the rest is how a widened enum ships a dead door.
The media list advertised seven of nine. hatch and watercolor — both native fidelity, and hatch has its own apply_hatch action in the same tool — were missing from the served JSON description while the Zod copy beside it and the agent guide both listed them. Three copies, and the stale one was the copy a model reads. pinepaper_compose's two-medium enum was checked against the engine for the same reason and is correct: COMPOSABLE_MEDIA is frozen at ['vector','thread'], and both tables are fixtures now.
A scene read could be 500 items of 800 and say nothing. exportCanvasOntology returns the true itemCount beside an item list capped at maxItems, and childCount beside a capped children array — so a partial answer was indistinguishable from a complete one unless the caller thought to compare two fields that look like they agree. Truncation is now reported when, and only when, something was clipped: both numbers, the knob to raise, and what to do past 500 where the cap truncates rather than paginates. An older studio that omits itemCount is reported as unknown, never as partial.
The server's own identity string banned two tools it ships. SERVER_INFO.description carried a flat prohibition on producing HTML or React — no qualifier, no tool named — while this server ships pinepaper_export_widget_html and pinepaper_export_widget, which produce exactly those. That string is the first text a client shows a model, before any tool, every session. Same defect as the agent_start_job description fixed above, in the widest-read place available. The rule is substitution, not format — and the prohibition is described here rather than quoted, because the guard flags a banned imperative in this README too, and it is right to: a model reads the sentence, not the quotation marks around it. The guard that catches it had been reading two surfaces out of four; it now sweeps all 19 resources, every prompt, all three verbosity tiers, the manifest and this README, and its liveness floor compares against the declared lists name by name rather than counting.
Four more item-stage shader effects — electric_arc, vortex, rain_veil, caustics (ABYSSAL's noise library). The item shader stage went from four built-ins to eight and nothing here named the new half.
on_key_fire now matches a chord exactly. Its modifier tests were one-way — they required a modifier that was asked for but never rejected one that was not — so { key: 'Enter' } fired on Ctrl+Enter, Shift+Enter and Cmd+Enter alike, and { key: 's' } fired on the browser's own Ctrl+S. Those are different intents. The relation's documentation says so now, along with the focus gate that keeps it from taking keys from the rest of the page. (Reported from this repo during the relation audit; fixed engine-side.)
Three appliers that failed in silence now report. The engine's animate, applyAnimatedMask and applyCutoutStyle refused an unknown key only through console.warn, and the production build strips the console — while returning values identical between success and failure (undefined either way, a Paper Group either way, and the very item it was given). Over MCP, where there is no console to read, an unknown key left the canvas unchanged and told every caller it had worked.
The engine now records the refusal on the item it already hands back, and these tools read it: a refused animation type, a mask that applied but will never animate, and a cutout preset that returned the item untouched are all reported as failures, each carrying the requested value and the known list, so a caller can correct itself without a second round trip.
New tool: pinepaper_character — place a figure from the graph and direct it. The character layer was reachable from the cloud's build script and nowhere else, which by this project's own rule means it did not exist: an MCP client, a cloud caller and a small on-device model all see these schemas and nothing behind them.
{ concept: "pp:Pigeon", at: {...}, height: 300, beats: [{ at: 0, channel: "bob", until: 8 }, { at: 1.4, channel: "blink" }] }.create_item gains shader and field item types. The cloud renderer has drawn shaders and parametric fields for months, reachable only by hand-writing a scene document — so the capability existed for whoever writes the build script and for nobody else.
bornAt/ttl are documented on properties. They always passed through (properties is a free record) and nothing mentioned them, so no caller could build a piece that cuts between shots — which is why the only multi-shot pieces that exist had their shots inferred from a naming convention in a build script.pp:CanvasSurface, with pp:ShaderSurface and pp:ParametricField under it. A shape is built as an item and drawn from its geometry; a surface is evaluated as the frame is drawn.New tool: pinepaper_import_motion_capture — BVH import and retarget. The engine has had importBVH/retargetBVH for releases and nothing exposed them; a model cannot use a capability no tool call reaches.
mode: 'import' builds a new skeleton shaped like the capture file. mode: 'retarget' drives an existing rig, so proportions stay the character's and only the motion comes from the clip — that distinction is the reason the tool exists.unmatchedSource/unmatchedTarget instead of silently driving half a rig, so a caller can build boneMap from the failure. fps defaults to 15 — CMU records at 120, and nobody wants 120 poses a second on a canvas timeline.pinepaper_rigging gains the pose-motion half — 18 actions. The engine had 26 pose methods; the tool exposed two. The pose library (list_poses, load_pose, interpolate_poses, list_skeletons), playback (play_pose_sequence, stop_pose_sequence, apply_pose_transition), procedural layers (auto_walk, auto_breath, auto_idle, auto_jump), root locomotion (move_root, stop_root_track), and the export/deform edges (bake_animation, add_secondary_motion, skin_path, list_shape_keys, load_shape_key). Each was checked against the engine source rather than its docs, which name tools that were never registered.
stitch_poses — join clips into one continuous performance, entering a cyclic clip at the phase closest to where the previous one ended so the legs do not teleport mid-stride.New tool: pinepaper_design_medium — the third design axis: what physically makes the marks. Every medium declares a fidelity, and resolve refuses the ones it cannot honestly render rather than producing flat shapes in their colours. apply_thread renders an item as needlepainting; the direction field is what separates that from hatching.
New tool: pinepaper_text_effect — 37 character-level text animations (terminaltexteffects' vocabulary, reimplemented in the engine from source). list returns the effects; apply explodes a text item into one animated item per character.
pinepaper_text_style (which adopts the text's registry id), this removes the original and returns the new per-character ids — so relations and keyframes on the source id do not survive. keepSource: true is the escape hatch. The tool is marked destructiveHint and says so in its description, because it inverts the id-preservation convention every neighbouring tool follows.gradient: false keeps the authored fill. seed defaults to 1, so a given text + effect + seed animates identically every run.Two more enums that named things the engine does not have. Both were the same shape as the relation gap, found by diffing every validated enum against the engine rather than by anything failing.
canvasPreset on the agent-flow tools passed an export platform name (instagram, youtube, web) straight to setCanvasSize, which keys on instagram-post and full-hd-1080p. Seven of ten matched nothing: the engine fell through to its default, resized the artboard to 800×600, and recorded the preset as applied — so asking for an Instagram canvas got neither the size nor an error. Now mapped, and pinned by a test against the engine's preset list. The platform vocabulary is unchanged, because instagram is the right word for an export target and only the canvas-size use was wrong.pinepaper_modify_item now documents pathData — reshaping the geometry itself, not just its styling, so a traced or hand-drawn outline can be corrected without deleting and recreating the item (which would lose its id and every relation and keyframe pointing at it).pinepaper_animate offered an animation that does not exist. slide was in the enum, in three JSON schemas and in two prose lists. The engine has no such type — the real ones are slideLeftRight and slideUpDown. It accepted the value, wrote it to the item, and the driver's switch fell through: the item sat still while its own data claimed a slide, and the call reported success. The same enum hid twelve types that do work (breathe, glow, jelly, path, shake, swing, scrollUp/Down/Left/Right, and the two real slides), so the surface both invented one animation and concealed a dozen.
All 18 driver types are now offered, and a parity test pins the enum, every JSON-Schema copy of it, and the prose against a fixture of the engine's ANIMATION_TYPES. A unit test that asserted slide parses — pinning the bug in place — was corrected. Letter collages keep their own separate four-name vocabulary, which is not drift.
95 live relations were not callable. pinepaper_add_relation offered 39 of the engine's 134, and the enum is a hard gate — a name missing from it is rejected at validation. The missing set was not a random 95: it was essentially the entire interactive vocabulary, every event-channel relation included, so the state-machine-via-relations capability was undiscoverable and unusable. pinepaper_scene_graph was emitting on_click_fire and on_event_set_active relations that an agent could not then create, inspect or recreate by hand.
Nothing was broken at runtime, which is why it survived: the engine could do it and nothing could name it. For a model those are the same condition.
on_click_fire, on_pointer_enter_fire, on_pointer_exit_fire, on_key_fire), the full on_event_* reaction set including template-interpolated property writes and the persistent-store pair, the on_enter_*/on_exit_* proximity families, exclusive_group / menubar_group, and the behavioural relations repels, wiggle, spring_follow, syncs_with, triggers_animation, connects_to, part_of, attached_to_tail, head_points_to, anchored_in_world, tours, synced_to_audio, and expresses — which pinepaper_import_layered_character already promised would make an imported character blink, while the enum made it unnameable.deform_*, effect_*, geo_*, bone_* and friends. The agent authors those through that tool, and a second name here would be a worse way to do the same thing. That exclusion list is written down with its reason, so the next engine diff doesn't re-litigate all 95.RELATION_TYPE_MAP gates "is this a known relation", so a relation callable but unmapped makes the validator report a perfectly valid scene as using an unknown one. 40 entries and 42 pp: edge definitions were backfilled from the engine's own descriptions.The relation catalogue in the tool description now names families rather than all 80 members, and points at pinepaper_query_capabilities { kind: 'relation' } for the live list, which reads the registry instead of a list written down in prose.
Hatching reaches the tool surface — pinepaper_design_medium gains apply_hatch, list_flow_fields and list_hatch_options. PinePaper could fill and it could stitch, and it could not hatch; the gap was already named in the thread-painting code, which notes that a constant stitch field "looks like hatching, which is a different medium."
gradient makes the density fall off across the shape — a shaded ramp rather than a flat tone.flowField is what makes it read as drawn — hand is the small correlated wander of a hand-drawn line, waves for water and hair, spiral for wood grain around a knot. continuous joins the whole set into one serpentine path.console.warn, which production strips — so the tool checks the shape first and answers "text has no outline, convert it with pinepaper_text_style first" or "distance is 400px against bounds 150x150" instead of a bare null. A group is hatched up to 40 paths and says so when there were more.GSAP's vocabulary, PinePaper's engine. An audit of GSAP's concept set against the 49 relations found most of it already present under other names — MotionPath is moves_along_path, MorphSVG is morphs_to, DrawSVG is trim paths, Physics2D is spring_follow, SplitText is the 37 text effects, nested timelines are precomps, and wiggle is richer than CustomWiggle. Five concepts were genuinely missing. They are adopted as vocabulary, not as a dependency: a second animation runtime is one that none of the SMIL, Lottie or MP4 exporters would understand, so the grammar is GSAP's and the implementations are independent.
New tool: pinepaper_sequence (pp:TimelinePosition) — say WHEN relative to something else instead of in absolute seconds. "<", ">", "+=1", "-=25%", labels and "intro+=0.5" resolve to seconds, and place threads a whole run so each clip resolves against the ones before it. A pure planner; it touches nothing.
"-=25%" is a quarter of the clip being inserted; "<25%" is a quarter of the previous one. They agree only when the two clips are the same length, and getting it backwards yields timings that look almost right.New tool: pinepaper_stagger (pp:Stagger) — the shape of a delay across many items: a grid lighting up outward from the centre, a row converging from both edges. each fixes the gap between neighbours; amount fixes the total. Delays are written to the channel the engine and the SMIL exporter already read, so a staggered scene scrubs and exports — nothing here is playback-only. staggered_with gains the same shape parameters (count, from, amount, grid, axis, distributeEase).
New tool: pinepaper_flip (pp:Flip) — animate a layout change without describing the motion. Record where things are, rearrange them however you like, and the transition is derived from the difference: the one animation an author never has to specify, which is what makes it usable for re-sorts, auto-layout passes and filters nobody could enumerate in advance. It writes ordinary keyframes, so the transition scrubs and exports. Rotation is compared on the shortest arc — 359° to 1° is a two-degree move, not a near-full spin the wrong way.
pinepaper_play_timeline gains rate, progress and scroll (pp:TimeScale, pp:InputDrivenPlayback) — set_time_scale / get_time_scale, get_progress / set_progress, bind_scroll / unbind_scroll / list_scrub_anchors.
orbits gains phaseDegrees. phase was the single parameter in the whole relation vocabulary measured in radians, against this engine's own stated convention that angles are degrees. phaseDegrees now takes precedence; phase is kept, and documented as the exception, because changing it outright would silently re-time every scene that already sets it — a 57× error of exactly the kind the convention exists to prevent.
pinepaper_design_medium was served but unlisted. It had been missing from manifest.json's tool list since it shipped — introduced above, but invisible to the marketplace listing. Caught by the prepublish guard while regenerating the manifest for the tools above.
New tool: pinepaper_scene_graph — compiles an interactive story or quiz into native items and relations: cards, answer buttons, click→event routing, exclusive-group mutex visibility, and score tracking.
action: 'validate' runs the same structural check without drawing anything — errors, warnings, reachable nodes, cycles. Check a generated graph before committing a canvas full of cards to it.to, a start naming no node, a non-terminal card with no way out, and duplicate node ids — the engine keys its node index by id, so a repeated id quietly replaces the earlier node rather than erroring.answers waits for a click, next (+ duration) auto-advances for a linear story beat.failed, wired and cycles. A graph can render completely and still leave relations unwired — it looks built and is inert, and that count is the only thing that says so.New tool: pinepaper_query_capabilities — asks the engine what it can do (text styles, character effects, generators, deforms, relations) and recommends one: list, find, coverage, and a mood/subject-weighted choose.
app.getCapabilities(), which warms the lazy registries first. Generators do not exist until the heavy modules land (~1.2s after boot) and the rigging/blending/deform relation rules only register once their subsystem is touched — answered cold, the engine reports zero generators and roughly 77 of ~100 relations. warm: false opts out when a cheap re-read of what is already resident is enough.coverage names its own blind spots: kinds with no source wired, and entries that can be applied but not ranked because they carry no description. A chooser that scores on description can never recommend those, so it says so.seed gives a stable tiebreak among equal-scoring candidates; with no seed the order is stable by key. Either way a repeated call answers the same way.Every tool property now declares a type. Ten inputs across pinepaper_event, pinepaper_component, pinepaper_world3d, pinepaper_rigging, pinepaper_text_style, pinepaper_equation_path and the new capabilities tool were published with a description and no type — valid JSON Schema, but strict function-calling clients reject a typeless property, and this server claims to work with any MCP-capable model. They are anyOf unions now.
pinepaper_connect / connect_ports accept an id. update_connector and remove_connector address a connector by connectorId, and there was previously no value a caller could correctly pass — creation returns code rather than a result, and the engine's fallback is timestamp-based. Assign your own and reuse it.
pinepaper_world3d add_object forwards PBR material fields — metalness, roughness, emissiveIntensity. (Corrected in 1.6.8: metalness and roughness are accepted and ignored on a plain object — the prop shader declares no such uniform. They render on the mesh path. Left here rather than quietly edited, because a drifted claim is worse than a missing one.)
Follows 1.6.6, whose dependency-security work is described below.
Dependency security. puppeteer moves to ^25, clearing GHSA-jmr9-qjv8-65gv (extract-zip symlink path traversal) — @puppeteer/browsers 3.2.1 drops extract-zip entirely. The published package was never exposed (puppeteer is an optional peer), but the browser tools need one, and the path was re-verified against real Chrome rather than a green unit suite that never launches a browser.
qs is pinned to ^6.16.0, clearing GHSA-4mjr-xmp4-gh2g — a denial of service on the production chain, via @modelcontextprotocol/sdk → express. npm audit reported zero against it, as it did through the 1.6.6 work: its registry feed lags GitHub's. Verified instead with an OSV sweep of all 82 production packages, which is clean.
Engine requirement. Several capabilities this server has always emitted correct calls for did nothing until recent FxTool builds: physics (the step callback was never registered, so nothing moved), scene-wide GPU filters on the WebGPU tier, map region colour animation (which reported success while animating nothing), and modify_item's pathData. No change was needed here — the calls were right — but run an FxTool from 2026-08-29 or later to get them.
Dependency security, no new tools and no API changes:
basic-ftp, fast-uri, js-yaml, path-to-regexp, ws, ip-address, qs, flatted, body-parser and ajv. Each is pinned to a floor in overrides so neither resolver can drift back.bun.lock. It pinned the vulnerable versions while package-lock.json had already re-resolved most of them — and bun test/bun run build install from bun.lock, so that was the tree in use. Both lockfiles now agree.npm audit reported zero against all of this; its registry advisory feed lags GitHub's. Verified instead with an OSV.dev sweep of both lockfiles, red-tested against the previous commit.manifest.json version parity is now tested. It had silently sat at 1.6.4 through the 1.6.5 release.Exposure note: puppeteer has been an optional peer since 1.6.5, so its chain (basic-ftp, ws, ip-address, js-yaml) never reached installs of this package. The @modelcontextprotocol/sdk chain (fast-uri, path-to-regexp, qs, body-parser, ajv) is the production surface.
Security hardening, no new tools:
js/incomplete-sanitization, High ×3) — an input like x\'; evil() could land outside the string in emitted code. All string literals now emit via JSON.stringify; regression tests pin the class.npm i puppeteer) when absent. The default dependency tree drops the headless-browser download, its install script, and its large transitive tree (tar-fs/bare-* — the usual "obfuscated code" scanner alerts). Default deps: @modelcontextprotocol/sdk + zod.dist/.Fourteen new tools (121 → 135) and new actions across the surface — the release that catches the agent surface up with the engine.
3D worlds. pinepaper_world3d — a real depth-buffered 3D world under the canvas: terrain presets (forest, snowMountain, field, jungle), sun shadows, an addressable actor stage and a directed camera (follow/fixed/orbit). add_actor with live: true puts a rigged canvas character INTO the world, performing — walk cycle, expressions and all. describe returns the engine's own parameter schema, so the docs cannot drift.
Motion capture & characters. pinepaper_rigging gains import_bvh (CMU/Mixamo mocap → a new rig, stick figure included), retarget_bvh (drive an existing rig by bone name — the result reports matched/unmatched bones) and import_spine (Spine JSON). New pinepaper_import_layered_character lands a layer-decomposed illustration as role-bound parts — blink and smile work with zero wiring (check rolesWired in the result).
Video editing. pinepaper_media gains set_time_remap (speed ramps, freeze frames, reverse), speed_ramp, match_cut (subject-aligned cuts via on-device detection), apply_track_matte (a headline filled with footage; live: true tracks an animating matte) and stop_live_matte.
Design systems. pinepaper_brand_kit (plan with WCAG contrast audit, then apply), pinepaper_component (master/instance with overrides that survive master updates), pinepaper_artboard (retarget a finished design to a new format), pinepaper_comment, pinepaper_provenance, pinepaper_scene_diff — plus pinepaper_transform fit (contain/cover).
Typography & imagery. pinepaper_text_style (stacked-layer display titles + variable-font weight/width/slant as animatable properties), pinepaper_shatter_image (raster → tile grid, inert until animated), pinepaper_compose (the collage patterns), and pinepaper_image_filter now documents the full GPU registry — grain, scanlines, duotone, bloom, halation, lightShafts, paletteMap, and the second-input set (displace, refract, trackMatte, datamosh) — plus analyze_palette/recolor_palette (read an image's palette, recolor another to match, shading preserved).
Games & data. pinepaper_game (deterministic A* pathfinding that feeds moves_along_path, tilemaps with merged collision rects), pinepaper_audio_beats (beat detection → animate_to_beat), pinepaper_template_params, and Figma import via pinepaper_import_asset.
Agent economics. pinepaper_agent_export gains estimateOnly — preflight an export's size without rendering it; GIF exports are capped at 15s with a clear message instead of an OOM.
pinepaper_crop_image (one-shot crop, keeps the item's id and relations) and pinepaper_chroma_key (green-screen background removal with auto-estimated thresholds)pinepaper_media gains set_clip — re-trim an already-uploaded video/audio clippinepaper_apply_effect: heatmap, liquid_metal, gem_smoke (WebGL2, silhouette-clipped)pinepaper_image_filter fixed and expanded — routed to the real GPU filter enginepinepaper://docs/readme (and per-language variants) without leaving the protocol178 tools is a lot of context. The server ships a toolkit system that serves only the tools a given client needs, plus a verbosity system that controls how long each tool description is.
Toolkit profiles (PINEPAPER_TOOLKIT):
| Profile | Contents |
|---|---|
core | The default. The craft tools the server's instructions point to, the agent workflow, canvas and query tools, plus every tool those name, so no listed tool points at a missing one. About half the tokens of agent. Anything else loads on demand (below) |
full | Every tool, no filtering |
agent | Every tool except the diagnostics (pinepaper_get_performance_metrics, pinepaper_diagnostic_report) and pinepaper_register_item |
diagram | Canvas + diagram + query/export |
map | Canvas + map + query/export |
font | Canvas + font + letter collage + export |
minimal | Agent, browser, canvas, and guide only |
lane | The agent lane: exactly seven tools — pinepaper_agent_batch_execute, pinepaper_keyframe_animate, pinepaper_pattern, pinepaper_agent_export, pinepaper_lint_scene, pinepaper_event, pinepaper_clear_canvas — shown with descriptions that carry the rules an agent otherwise learns by failing (the keyframe shape, an item's words and look inside properties, a position being the item's centre, the export's 5 s default). The whole contract, instructions included, is ~4.3k tokens. For scripted builds of shapes, text, keyframes and relations; use core for characters, styled scenes, maps, diagrams and sound |
Verbosity tiers (PINEPAPER_VERBOSITY): verbose, compact (default), minimal.
Load on demand. Every client starts on core. When a task needs more, the model calls pinepaper_set_toolkit with load: a group ({"load": ["map"]}, ["diagram"], ["font"], ["physics"], …) or a tool's own name. The tools are added for the session, along with any tools they mention, and the client is told the list changed. The server logs each client's name at start-up.
PINEPAPER_TOOLKIT always wins. You can also hand-pick tools with PINEPAPER_TOOLS (comma-separated names), or switch profiles at runtime with the pinepaper_set_toolkit tool. Start with pinepaper_tool_guide to have the server explain its own surface.
"Create a red pulsing text that says HELLO" # Browser auto-connects
"Create 5 items in batch, then export for TikTok"
"Analyze the scene and recommend export format"
No manual setup required — just start making tool calls.
The key feature — describe HOW items should behave, and the engine solves the motion every frame. 39 relation types are available via pinepaper_add_relation. Relations are compositional: one item can carry several at once.
Spatial & motion
| Relation | Description |
|---|---|
orbits | Circular motion around a target |
follows | Move toward target (with offset) |
attached_to | Fixed offset from target |
maintains_distance | Hold a set distance |
points_at | Rotate to face target |
mirrors | Mirror target's position |
parallax | Depth-scaled movement |
bounds_to | Stay within an area |
wave_through | Wave propagation across items |
moves_along_path | Travel along a path or equation |
Structural layout
Static composition expressed as edges instead of hardcoded coordinates. Placement is derived from the target's bounds and re-derived each frame, so moving or resizing the target brings the dependent along — and the layout stays editable as graph data.
| Relation | Description |
|---|---|
on_top_of | Source's bottom edge rests on the target's top edge — stacking (gap, align, overhang) |
below | Mirror of on_top_of — source's top edge on the target's bottom edge |
beside | Flank the target left or right (side, gap, align) |
inside | Place within the target's bounds at a 9-way anchor, inset by padding |
centered_on | Source center = target center + (offsetX, offsetY); concentric at zero |
aligned_with | Match the target on one axis only, leaving the other free (axis is required) |
Structure & construction
| Relation | Description |
|---|---|
is_midpoint_of | Sit at the midpoint of two items |
lies_on_line | Constrain onto a line |
is_centroid_of | Sit at the centroid of a set |
is_circumcenter_of | Sit at the circumcenter |
concentric_with | Share a center |
circumscribes | Enclose a target |
indicates | Point out / annotate |
construction_reveal | Staged geometric reveal |
Animation & camera
| Relation | Description |
|---|---|
animates | Drive a property over time |
grows_from | Scale in from an origin |
staggered_with | Offset timing across a set |
morphs_to / group_morphs_to | Shape morphing |
camera_follows / camera_animates | Camera behavior |
Deterministic binding (Expression IR)
| Relation | Description |
|---|---|
driven_by | Bind one property to another: source.p = target.p * multiplier + offset, optionally clamped. For fillColor/strokeColor the driven value interpolates colorFrom→colorTo, so a relation can drive color. |
time_expression | Self-relation: drive a property by a math expression of t (scene time) and v (base value), e.g. sin(t*2)*50 + v. |
With signal: true these compile to a pure f(t) Expression IR, making them scrub-, loop-, and replay-stable. Expressions using random() or unknown symbols fall back to per-frame evaluation.
Event-driven scene chains
| Relation | Description |
|---|---|
on_event_fire_after | When source event fires, pulse the target event after a delay (chaining primitive) |
on_event_add_relation | On fire, add a relation to an item — the scene evolves itself |
on_event_remove_relation | On fire, tear a relation down |
on_event_set_color | On fire, set fill/stroke color |
on_event_set_property | On fire, set any item property |
on_event_set_visibility | On fire, show/hide |
Create channels with pinepaper_event (create → eventId, pulse → fire it). Chain beats with on_event_fire_after on the canvas timeline to author a long scene as a graph of timed beats instead of a keyframe track.
Extras: relations can target the live pointer via the reserved targetId 'cursor', and any relation can carry params.window = { start, end?, repeat? } to gate when it is active (repeat: once | loop | pingpong).
"Create a blue circle at position 200, 300 with radius 50"
"Create text saying 'Welcome' with font size 72"
"Draw the perpendicular bisector of AB"
Beyond basic shapes, pinepaper_geometry provides construction primitives, pinepaper_group handles group/ungroup/break-apart, and pinepaper_arrange controls z-order (bring forward/back/front/back).
For quick looping effects: pulse, rotate, bounce, fade, wobble, slide, typewriter. For timed work use pinepaper_keyframe_animate; query the valid targets with pinepaper_get_animatable_properties and pinepaper_get_available_easings.
31 procedural generators via pinepaper_execute_generator (list them with pinepaper_list_generators):
drawBlobs, drawBokeh, drawCircuit, drawFluidFlow, drawFormulaArt, drawFunctionPlot, drawGeometricAbstract, drawGlobeWireframe, drawGradientMesh, drawGrid, drawHalftone, drawLowPoly, drawNoiseTexture, drawOrganicFlow, drawParametricCollection, drawParametricCurve, drawPattern, drawPeaks, drawRibbons, drawScatter, drawShaderArt, drawSimulation, drawSpectrumAnalyzer, drawStackedCircles, drawStackedWaves, drawSunburst, drawSunsetScene, drawTruchet, drawWaves, drawWindField, drawYeganehMountains
Create flowcharts, UML diagrams, network diagrams, and more:
"Create a flowchart for user login process"
"Make a UML class diagram for the User class"
"Design a network topology with 3 servers connected to a cloud"
pinepaper_import_mermaidChoropleths, region styling, and data-driven map animation via pinepaper_map, pinepaper_map_regions, pinepaper_map_animation, and pinepaper_map_data.
pinepaper_font covers font loading and text-to-path work; pinepaper_create_letter_collage and pinepaper_animate_letter_collage build and animate letterform collages.
Search and import free SVG icons:
Import images, then use pinepaper_image_filter, pinepaper_lasso, and pinepaper_cutout_style to process them. pinepaper_detect_objects runs object detection (with text queries) and can composite results as nodes; pinepaper_extract_object pulls a single object out.
The server keeps a design graph of the canvas, so an AI can inspect and critique its own work: pinepaper_get_canvas_ontology, pinepaper_query_ontology, pinepaper_analyze_design, pinepaper_validate_design, pinepaper_validate, and pinepaper_validate_scene.
Built-in performance tracking helps AI assistants optimize workflows:
pinepaper_get_performance_metricsGenerate instruction/code pairs for LLM fine-tuning:
{
"instruction": "moon orbits earth at radius 100",
"code": "app.addRelation('item_1', 'item_2', 'orbits', {radius: 100})"
}
All 178 tools, grouped by the tag used for toolkit filtering.
canvas)| Tool | Description |
|---|---|
pinepaper_set_background_color | Set background color |
pinepaper_set_canvas_size | Set canvas dimensions |
pinepaper_get_canvas_size | Read canvas dimensions |
pinepaper_clear_canvas | Clear the canvas |
pinepaper_refresh_page | Reload the studio page |
pinepaper_background | Manage background layers |
core)| Tool | Description |
|---|---|
pinepaper_create_item | Create text, shapes, graphics |
pinepaper_modify_item | Change item properties |
pinepaper_delete_item | Remove an item |
pinepaper_create_grid | Create items in a grid layout |
pinepaper_create_glossy_sphere | Create 3D glossy sphere effect |
pinepaper_create_diagonal_stripes | Create diagonal stripe pattern |
pinepaper_geometry | Geometric construction primitives |
pinepaper_group | Group / ungroup / break apart |
pinepaper_arrange | Z-order: bring forward/back/front/back |
pinepaper_path | Booleans, simplify, outline stroke, repeat patterns, lock |
batch)| Tool | Description |
|---|---|
pinepaper_batch_create | Create multiple items at once |
pinepaper_batch_modify | Modify multiple items at once |
import)| Tool | Description |
|---|---|
pinepaper_import_motion_capture | Import or retarget a BVH motion-capture clip |
pinepaper_import_svg | Import SVG markup |
pinepaper_import_image | Import a raster image |
pinepaper_detect_objects | Detect objects in an image (text queries, composite as nodes) |
pinepaper_extract_object | Extract a detected object |
assets)| Tool | Description |
|---|---|
pinepaper_search_assets | Search SVG assets across repositories |
pinepaper_import_asset | Import asset from search results |
relations)| Tool | Description |
|---|---|
pinepaper_add_relation | Create a behavioral relationship |
pinepaper_remove_relation | Remove a relationship |
pinepaper_query_relations | Find existing relations |
pinepaper_register_custom_relation | Register a custom relation type |
animation)| Tool | Description |
|---|---|
pinepaper_sound | Synthesis: tones, chords, SFX, sound-as-waveform-path |
pinepaper_motion | Generator motion engine: group and field motions |
pinepaper_animate | Apply a simple loop animation |
pinepaper_keyframe_animate | Timed keyframe animation |
pinepaper_choreograph | Acting for any item: hop, bounce, roll, fly, peek with squash and stretch |
pinepaper_play_timeline | Control playback, rate, progress, scroll-driven scrubbing |
pinepaper_stagger | Shape a delay across many items |
pinepaper_pattern | Move many items with one stored declaration (wave, stagger) |
pinepaper_flip | Animate a layout change without describing the motion |
pinepaper_get_animatable_properties | List animatable properties |
pinepaper_get_available_easings | List easing functions |
pinepaper_construction_sequence | Staged construction animation |
pinepaper_hand_drawn | Hand-drawn line: a rough, bowed outline, or a stroke that draws itself on |
pinepaper_pen_figure | A pen draws a mathematical figure, the ink appearing behind it |
masks)| Tool | Description |
|---|---|
pinepaper_apply_animated_mask | Apply an animated mask |
pinepaper_apply_custom_mask | Apply a custom mask |
pinepaper_remove_mask | Remove a mask |
pinepaper_get_mask_types | List mask types |
pinepaper_get_mask_animations | List mask animations |
camera)| Tool | Description |
|---|---|
pinepaper_camera | Camera state control |
pinepaper_camera_animate | Animate the camera |
pinepaper_camera_director | Shot-level camera direction |
scene)| Tool | Description |
|---|---|
pinepaper_create_scene | Create a scene |
pinepaper_manage_scenes | Manage scenes |
pinepaper_scene_playback | Scene playback control |
pinepaper_scene_graph | Interactive story / quiz card scene graph |
pinepaper_sequence | Relative timeline positions for a run of clips |
pinepaper_sequence | Relative timeline positions for a run of clips |
pinepaper_event | Create / pulse event channels for scene chains |
| Tool | Description |
|---|---|
pinepaper_execute_generator | Run a background generator |
pinepaper_list_generators | List available generators |
pinepaper_generate | AI image generation in the PinePaper cloud (charged; beta, per organisation) |
pinepaper_generate_estimate | Free price check, and the model list |
pinepaper_generate_status | Collect a generation that outlived its timeout |
pinepaper_styled_scene | One composition drawn in a built-in style |
pinepaper_draw_weave | Woven cloth as vector threads: plain, twill, basket, herringbone |
pinepaper_draw_knots | Embroidered knots with shadow and highlight, where you place them |
pinepaper_draw_sashiko | Sashiko running-stitch patterns on ground cloth |
pinepaper_draw_subdivision | Mondrian, Swiss, Bauhaus and minimal rectangle compositions |
pinepaper_apply_effect | Apply sparkle, blast, and other effects |
pinepaper_look | Restyle an item or the whole scene as a named look, graded for fidelity |
pinepaper_original_character | Original characters from a base rig and a variant; a cast from seeds |
pinepaper_morph_sequence | One element through many states, with the camera riding the morphs |
pinepaper_emitter | Particles as a saved, deterministic item: bursts and trails |
pinepaper_render_hook | Custom per-frame drawing a saved scene carries |
pinepaper_text_effect | 37 character-level text animations; replaces the text with one keyframed item per character |
pinepaper_add_filter | Add an image filter |
pinepaper_relight | Light the 3D world with point and directional lights |
pinepaper_shader_graph | Pixel graphs (relight, depth of field, bloom) over an item or the scene |
selection, transform, history)| Tool | Description |
|---|---|
pinepaper_selection | Selection management |
pinepaper_transform | Transform items — fit to frame, nudge, flip, reorder |
pinepaper_history | Undo / redo |
pinepaper_compose | Arrange items into a named collage pattern and film it |
pinepaper_design_medium | What makes the marks — media with honest fidelity, and needlepainting |
pinepaper_brand_kit | Apply brand colours / fonts by role, with a contrast audit |
pinepaper_ui_kit | Phone lock screen, notifications, prompt box, toggle, ad and calendar cards, on brand |
pinepaper_component | Reusable master + instances, with per-instance overrides |
pinepaper_artboard | Resize the artboard; per-item reflow constraints |
pinepaper_comment | Notes pinned to an item, a point and/or a moment |
pinepaper_provenance | Where an item came from; what depends on it |
pinepaper_scene_diff | What changed between two scene states |
pinepaper_audio_beats | Detect beats; bake an item's animation onto them |
pinepaper_template_params | Templates with typed, coerced inputs |
pinepaper_render_batch | One creative per data row: apply the row, export it |
image_processing)| Tool | Description |
|---|---|
pinepaper_image_filter | Apply image filters |
pinepaper_crop_image | Crop an image to a rect (optional aspect ratio) |
pinepaper_chroma_key | Key out a background color (auto-estimates threshold) |
pinepaper_shatter_image | Split a raster into a tile grid (inert until animated; group adopts the original id) |
pinepaper_lasso | Lasso selection on images |
pinepaper_cutout_style | Cutout styling |
precomp, deform, sprite, interaction)| Tool | Description |
|---|---|
pinepaper_precomp | Pre-composition management |
pinepaper_deform | Deformation tools |
pinepaper_sprite_sheet | Sprite sheet handling |
pinepaper_interaction | Click, hover, and drag interactions |
dataviz)| Tool | Description |
|---|---|
pinepaper_create_chart | Create a chart |
pinepaper_equation_path | Function / parametric / Fourier equation paths |
diagram)| Tool | Description |
|---|---|
pinepaper_create_diagram_shape | Create flowchart/UML/network shapes with ports |
pinepaper_connect | Connect items with smart connectors |
pinepaper_connect_ports | Connect specific ports on items |
pinepaper_add_ports | Add connection ports to items |
pinepaper_auto_layout | Auto-arrange items using layout algorithms |
pinepaper_get_diagram_shapes | List available diagram shapes |
pinepaper_update_connector | Update connector style/label |
pinepaper_remove_connector | Remove a connector |
pinepaper_diagram_mode | Control diagram editing mode |
pinepaper_import_mermaid | Import a Mermaid diagram |
map)| Tool | Description |
|---|---|
pinepaper_map | Create / configure a map |
pinepaper_map_regions | Region styling and selection |
pinepaper_map_animation | Animate a map |
pinepaper_map_data | Bind data to a map |
pinepaper_globe | Globe mode + world tour |
media)| Tool | Description |
|---|---|
pinepaper_media | Video/audio + editing: upload, trim, time remap / speed ramps, match cut, track matte (live) |
rigging)| Tool | Description |
|---|---|
pinepaper_rigging | Skeletons, bones, IK, breakdown poses; BVH mocap import/retarget, Spine import |
pinepaper_import_layered_character | Decomposed character layers → role-bound parts (blink/smile work immediately) |
pinepaper_design_system | DTCG design systems + 18 composable aesthetic styles |
pinepaper_stick | The vendored stick-figure kit: figure and set |
pinepaper_story | Prose becomes a scene: distill, assemble, plan a book |
pinepaper_character | Place a figure from the design graph and direct it with beats — no geometry, no bones, no poses |
font, letter_collage)| Tool | Description |
|---|---|
pinepaper_font | Font loading and text-to-path |
pinepaper_text_style | Display text styles (stacked-layer titles) + variable-font axes |
pinepaper_create_letter_collage | Create a letterform collage |
pinepaper_animate_letter_collage | Animate a letterform collage |
magic, physics, measurement, template)| Tool | Description |
|---|---|
pinepaper_magic | High-level "make it look good" helpers |
pinepaper_physics | Physics simulation |
pinepaper_game | Game logic: A* pathfinding (feeds moves_along_path) + tilemaps with collision rects |
pinepaper_world3d | 3D world under the canvas: terrain presets, live-sprite actors, follow/orbit camera |
pinepaper_measurement | Measurement and annotation |
pinepaper_apply_template | Apply a scene template |
query)| Tool | Description |
|---|---|
pinepaper_get_items | Get canvas items |
pinepaper_get_relation_stats | Relation statistics |
pinepaper_query | General canvas query |
pinepaper_query_capabilities | Query and recommend capabilities |
ontology)| Tool | Description |
|---|---|
pinepaper_get_canvas_ontology | Get the canvas design graph |
pinepaper_query_ontology | Query the design graph |
pinepaper_analyze_design | Analyze design quality |
pinepaper_validate_design | Validate against design rules |
pinepaper_validate | General validation |
pinepaper_beat_cuts | Cut points on the music's beats or bars; split a clip at each |
pinepaper_query_mutations | A property's keyframe series over time |
pinepaper_accessibility_check | Text contrast and flashing (WCAG), as a heuristic screen |
pinepaper_validate_scene | Validate scene integrity |
pinepaper_instantiate_ontology | Compile a pp: design graph into a scene |
pinepaper_lint_scene | Relational-density audit + structural-relation suggestions |
export)| Tool | Description |
|---|---|
pinepaper_export_svg | Export animated SVG |
pinepaper_export_scene | Export the scene: a summary, or a restorable project file (full: true) |
pinepaper_import_scene | Restore a scene from a full export file |
pinepaper_place_on_surface | Put a UI onto a screen in a photo |
pinepaper_export_training_data | Export LLM training pairs |
pinepaper_export_widget | Export an embeddable widget |
pinepaper_export_widget_html | Export widget HTML |
pinepaper_capture_frames | Capture deterministic frames |
agent)| Tool | Description |
|---|---|
pinepaper_agent_start_job | Start a content creation job session |
pinepaper_agent_end_job | End job with summary and recommendations |
pinepaper_agent_reset | Quick canvas reset without page refresh |
pinepaper_agent_batch_execute | Execute multiple operations in batch |
pinepaper_interchange | Lottie, dotLottie, GLB, BVH, PNG sequence |
pinepaper_agent_export | Smart export with platform auto-detection |
pinepaper_export_store | Recover an export the studio is still holding — list, save to a file, or release |
pinepaper_agent_analyze | Analyze content for export recommendations |
browser)| Tool | Description |
|---|---|
pinepaper_browser_connect | Connect to the studio |
pinepaper_browser_disconnect | Disconnect |
pinepaper_browser_screenshot | Take a screenshot |
pinepaper_browser_status | Connection status |
| Tool | Description |
|---|---|
pinepaper_tool_guide | Server-side guide to the tool surface |
pinepaper_set_toolkit | Switch toolkit profile at runtime |
pinepaper_get_performance_metrics | Get execution timing metrics |
pinepaper_diagnostic_report | Diagnostic report |
custom_code, p5, register)| Tool | Description |
|---|---|
pinepaper_execute_custom_code | Run custom code against the app |
pinepaper_p5_draw | p5.js-style drawing |
pinepaper_register_item | Register an externally created item |
1. Create a yellow circle as the sun (radius 60) at center
2. Create a blue circle as Earth (radius 20)
3. Create a gray circle as the Moon (radius 8)
4. Add relation: Earth orbits Sun at radius 151, speed 0.3
5. Add relation: Moon orbits Earth at radius 40, speed 0.8
1. Create text "BRAND" with font size 96
2. Apply pulse animation with speed 0.5
3. Apply sparkle effect with gold color
4. Add sunburst background
1. Create a circle as "player"
2. Create text "Player 1" as the label
3. Add relation: label follows player with offset [0, -50]
1. Create events e0, e1, e2 (one per beat)
2. Chain them: on_event_fire_after e0 → e1 (delay 2000, timeline: canvas)
3. Chain: on_event_fire_after e1 → e2 (delay 2000, timeline: canvas)
4. Give beat 1 a reaction: on_event_add_relation e1 → planet (type: orbits)
5. Give beat 2 a reaction: on_event_set_color e2 → planet (color: #ff3300)
6. Pulse e0 to start — the whole chain is scrub- and replay-stable
1. Create a terminal shape with label "Start"
2. Create a process shape with label "Get Input"
3. Create a decision shape with label "Valid?"
4. Create a terminal shape with label "End"
5. Connect Start → Get Input
6. Connect Get Input → Valid?
7. Connect Valid? → End (label: "Yes")
8. Connect Valid? → Get Input (label: "No", routing: curved)
9. Apply hierarchical auto-layout
1. Create a cloud shape with label "Internet"
2. Create 3 server shapes with labels "Web", "API", "DB"
3. Connect Internet → Web (label: "HTTPS")
4. Connect Web → API (label: "REST")
5. Connect API → DB (label: "SQL")
6. Apply force-directed auto-layout
The server does not draw anything itself. It validates a tool call, generates JavaScript that calls PinePaper Studio's app.* API, and executes it in the browser — so the studio app stays the single source of truth for behavior.
┌─────────────────────────────────────────────────────────────┐
│ AI Client (Claude, etc.) │
│ │ │
│ MCP Protocol │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ PinePaper MCP Server │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Tool Handlers │ │ validate + route │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ┌────────▼────────┐ │ │
│ │ │ Code Generator │ │ emit app.* calls │
│ │ └────────┬────────┘ │ │
│ └───────────┼───────────┘ │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ PinePaper Studio │ execute in browser │
│ │ (Browser/App) │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Installing pulls Chrome down through Puppeteer (~320 MB). If that is more than the machine can spare, cloud.pinepaper.studio runs the same server over HTTP with nothing to install.
git clone https://github.com/pinepaper/mcp-server.git
cd mcp-server
# Using npm
npm install
npm run build
# Using bun (recommended)
bun install
bun run build
Each release ships it ready-made (latest). bun run build:mcpb builds mcpb/pinepaper-mcp-server-<version>.mcpb, a one-file Claude Desktop extension: open it and Desktop installs the server, with no config file to edit. The extension drives your installed Google Chrome (choose another Chromium-based browser in its settings), and its settings also set the export folder and the tool list.
Build the server:
bun run build
Add to your MCP client config (example for Claude Desktop on macOS: ~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"pinepaper": {
"command": "node",
"args": ["/full/path/to/mcp-server/dist/cli.js"]
}
}
}
Restart your MCP client
Test with: "What PinePaper tools do you have available?"
Tests run on the Bun test runner.
bun test
# With coverage
bun test --coverage
# Typecheck
bun run typecheck
manifest.json's tools[] must stay in sync with the served tool surface. This is enforced on publish (prepublishOnly), and you can run it directly:
bun run check:manifest # verify
bun run fix:manifest # rewrite manifest to match source
bun run dev
PinePaper MCP Server supports 51 languages, providing localized tool descriptions and messages for AI agents.
| Category | Languages |
|---|---|
| European | English, Spanish, French, German, Italian, Portuguese (+ Brazilian), Dutch, Polish, Russian, Ukrainian, Swedish, Danish, Norwegian, Finnish, Czech, Greek, Hungarian, Romanian, Turkish, Icelandic |
| East Asian | Chinese (Simplified & Traditional), Japanese, Korean |
| Southeast Asian | Thai, Vietnamese, Indonesian, Malay, Tagalog, Filipino |
| South Asian | Hindi, Bengali, Tamil, Telugu, Marathi, Gujarati, Kannada, Malayalam, Punjabi, Urdu |
| Middle Eastern | Arabic, Hebrew, Persian (RTL support) |
| Indigenous (Canada) | Chipewyan, Cree, Michif, Inuktitut, Mi'kmaq, Mohawk, Ojibwe |
Set the PINEPAPER_LOCALE environment variable:
{
"mcpServers": {
"pinepaper": {
"command": "npx",
"args": ["-y", "@pinepaper.studio/mcp-server"],
"env": {
"PINEPAPER_LOCALE": "ja"
}
}
}
}
Or programmatically:
import { setLocale, t } from '@pinepaper.studio/mcp-server';
setLocale('fr');
const description = t('tools.pinepaper_create_item.description');
src/i18n/locales/ (e.g., xx.ts)en.tssrc/i18n/locales/index.tslocaleMapSee CONTRIBUTING.md for detailed guidelines.
| Variable | Description | Default |
|---|---|---|
PINEPAPER_STUDIO_URL | PinePaper Studio URL to connect to (/editor is appended automatically) | https://pinepaper.studio |
PINEPAPER_HEADLESS | Run the browser headless (set false to watch it work) | true |
PINEPAPER_IDLE_PAUSE_SECONDS | Seconds with no tool call before the studio page is paused, so an idle server uses almost no CPU; the next call resumes it. Exports are unaffected. 0 never pauses | 30 |
PINEPAPER_API_KEY | Service-account key (pp_sa_…) for the cloud-backed tools (pinepaper_generate) | — |
PINEPAPER_CLOUD_URL | The PinePaper cloud those tools call | https://cloud.pinepaper.studio |
PINEPAPER_EXECUTION_MODE | puppeteer (execute in a real browser) or code (return generated code only) | puppeteer |
PINEPAPER_PUPPETEER_FROM | An absolute project folder to load puppeteer / puppeteer-core from, for a strict layout (pnpm hoist=false, Yarn PnP strict) where the server cannot see its project's packages | unset |
PINEPAPER_CHROME_PATH | A Chromium-based browser for the canvas tools when full puppeteer is not installed (the Desktop extension uses your installed Chrome by default) | installed Google Chrome |
PINEPAPER_EXPORT_DIR | Directory for exported files | ~/Downloads/PinePaper (<tmpdir>/pinepaper-exports where there is no Downloads folder) |
PINEPAPER_LOCALE | Language locale code | en |
PINEPAPER_TOOLKIT | Toolkit profile (core/full/agent/diagram/map/font/minimal/lane) |
Behind a proxy, or against a Studio with analytics or polling connections open,
networkidle2 may never settle: set PINEPAPER_WAIT_UNTIL only if you need the
old behaviour back. Readiness does not depend on it — the connection waits for
Studio's app.create either way.
Key Features:
pinepaper_get_performance_metrics toolQuick Example:
AI: "Let me check if batch operations are faster"
→ pinepaper_get_performance_metrics(format: 'summary')
Result:
- pinepaper_create_item: avg 145ms
- pinepaper_batch_create (10 items): avg 298ms (~30ms per item)
AI: "I'll use batch_create for the next 20 items"
Configuration:
# Disable metrics if not needed
export PINEPAPER_METRICS_ENABLED=false
# Increase retention for long sessions
export PINEPAPER_METRICS_RETENTION=5000
Learn More: See docs/PERFORMANCE_METRICS.md for complete documentation.
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
bun testMIT License - see LICENSE for details.
Made with ❤️ by the PinePaper team
itemsrelationOptionsxymodifypropertiesitem_1
(pinepaper_clear_canvas, pinepaper_agent_start_job and
pinepaper_agent_reset), so a batch sent after a clear gets the same ids
every time. Before, the numbering carried on from the cleared items, and
only a server restart reset it.group step in a batch now takes the next $N, as a create does, so
a later step can animate the group: in [create, create, group, …] the
group is $2. This renumbers batches that create items after a group:
in [create, create, group, create] the last item was $2 and is now
$3. Before, a $N meant for the group failed with "it has created 2 so
far".on_event_set_visibility,
and its on_enter_ and on_exit_ forms) refuses value and asks for
visible, the field it reads. Before, {"value": false} showed the item
instead of hiding it.time still picks
another moment.strokeColor and no fill is drawn as an outline.
Before, it got the default fill, or the active brand's colour.ttl or bornAt on a line as a likely typo.from and to reports where it actually is, and says
that position does not move it: from and to are canvas coordinates.
Before, the result reported the position asked for while the line was
drawn at the top left.pinepaper_transform nudge moves an item the way pinepaper_modify_item
does, so an item with a loop animation keeps its new place, and the
result no longer reports the move as no change.create_item documents properties.id: an id of your own, so replays
address the same items. An id with a quote, backslash or line break is
refused, since later calls could not address it.pinepaper_modify_item,
pinepaper_delete_item, pinepaper_animate,
pinepaper_keyframe_animate, pinepaper_apply_effect or
pinepaper_query_relations. Before, the call failed with a syntax error.lane toolkit adds pinepaper_event, for things that happen at a
set time or on a click (a panel that hides at 3 s), and
pinepaper_clear_canvas, to start over. Its batch description shows how
to attach an event's reactions and triggers.raycastadd_object stops advertising knobs that reach no pixel. It offered metalness, roughness and emissiveIntensity — but a plain object is drawn by the prop shader, which declares no such uniform, so those were stored, listed and ignored. They render on the mesh path only, and the description now says which is which instead of letting a caller set one and see nothing.corePINEPAPER_TOOLS | Explicit comma-separated tool allowlist | unset |
PINEPAPER_VERBOSITY | Description verbosity (verbose/compact/minimal) | compact |
PINEPAPER_TOOL_VERBOSITY | Deprecated alias for PINEPAPER_VERBOSITY | unset |
PINEPAPER_METRICS_ENABLED | Enable performance metrics tracking | true |
PINEPAPER_METRICS_RETENTION | Max metrics to retain in memory | 1000 |
PINEPAPER_SCREENSHOT_MODE | Screenshot mode (on_request/always/never) | on_request |
PINEPAPER_WAIT_UNTIL | What a navigation waits for (load/domcontentloaded/networkidle0/networkidle2) | domcontentloaded |
PINEPAPER_TIMEOUT | Navigation and readiness timeout, in ms | 30000 |
PINEPAPER_PROXY | Chrome --proxy-server value; also ignores certificate errors, for an intercepting proxy | unset |
PINEPAPER_EXPORT_TIMEOUT | Governor budget for an export run, in ms (raise PINEPAPER_TIMEOUT alongside it) | 300000 |
PINEPAPER_GOVERNOR | off runs code by raw eval — no governor report, no error codes, no seeded determinism | on |
PINEPAPER_ASSET_DIRS | Extra folders local image and media imports may read from, separated by : (; on Windows). The working directory and the export folder are always allowed | unset |
PINEPAPER_ALLOW_PRIVATE_NETWORK | 1 lets URL imports reach loopback, private and LAN addresses, for a local dev server. Cloud metadata addresses stay refused | unset |
PINEPAPER_FFPROBE | off skips the ffprobe check of a saved video export. Without ffprobe on PATH the check is skipped and the result says so | on |