PinePaper MCP Server
Enables creation and animation of graphics in PinePaper Studio, with the ability to export animated SVG files and generate procedural backgrounds, shapes, and text with behavior-driven relations.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@PinePaper MCP Servercreate a flowchart for our user login process"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
PinePaper MCP Server
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.
Overview
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 170 tools across drawing, animation, diagrams, maps, typography, physics, image editing, data visualization, and export. Using natural language, you can:
Create geometric shapes, text collages, diagrams, and data charts
Apply 37+ character-level text effects and 28+ vertex deformation presets
Wire behavioral relationships between objects (orbit, follow, point-at, mirror, parallax, morph)
Animate with keyframes, timeline sequencing, and physics simulations
Build interactive widgets with click/hover/drag event bindings
Export to SVG, self-contained interactive HTML, MP4, and animated formats
Related MCP server: ASCII & SVG Art Studio
Running it: local or hosted
Local is free and complete. Every one of the 170 tools works when you run this server yourself. There is no reduced tier and nothing held back.
What it needs:
Node | 18 or newer |
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.
Quick start
Puppeteer is an optional install, so a plain npx @pinepaper.studio/mcp-server
has no browser: every tool then 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"]
}
}
}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".
Made with tool calls
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 blinksAll 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.
Try it interactive
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):
Tabs from relations —
on_click_fire+exclusive_group+on_enter_set_visibilityAccordion from relations — disclosure pairs via
on_event_toggleSolar system, 4-in-1 tabs — the state machine driving four scenes
Menubar from relations — WAI-ARIA menubar semantics from the same graph
The same graph drives visuals, keyboard access, and screen-reader roles (WCAG 2.1 AA) — see pinepaper://docs/relations from your MCP client.
Run these yourself
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.
Security: yes, it executes code
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.
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 |
| The tool returns the JavaScript and stops. Nothing runs. You read it and paste it if you want it. |
| The server launches Chrome and runs the snippet in the page via |
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.
What to weigh before running it.
Code an agent writes runs with whatever that browser page has. Give this server the same trust you would give anything else you let write and run code on your machine — which is the trust you already extend to an MCP client with tool access.
Puppeteer mode launches Chrome with
--no-sandboxand--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, usecodemode or put the server in a container.Puppeteer itself is an optional peer dependency, kept out of the default tree precisely because a headless browser plus an install script is what scanners flag hardest. Install it only if you want the executing mode.
What's new in 1.6.21
Fixed
shaderandfielditems frompinepaper_create_itemdraw 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.
What's new in 1.6.20
New: make anything act
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.
New: sounds on events
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).
New: looks
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.
New: original characters
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.
New: one element, many states
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.
New: a brand every beat inherits
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.
New: particles and drawing that survive a save
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.
New: numbers that count
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.
New: overlays, step timing and hold keys
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.
Changed: no silent success
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.
Fixed
pinepaper_import_mermaidreturns the canvas ids of the shapes it drew, sopinepaper_auto_layoutcan move them.A refusal now carries the studio's own reason, such as a misspelt property and the one it meant.
pinepaper_image_filterdocuments the watercolour and painterly filters for photos, and reports a refused filter as a failure.
What's new in 1.6.19
New: save a scene and restore it
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.
New: put your UI on a screen in a photo
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.
New: one scene, six styles
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.
Changed: for services that run the steps later
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.
Changed: tool results report where their time went
Every result carries _meta["pinepaper.studio/timing"]: the whole call, and
the studio's share of it, for tracing slow requests.
New: light the 3D world, brush strokes, shader graphs
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.
New: handwriting that writes itself on
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.
New: ink and charcoal on what you already drew
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.
Changed: asset search uses Iconify and Font Awesome
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.
Fixed
pinepaper_add_filteroffers every filter the studio has, including dither, CMYK and dot halftones, edge detection, HSL and colour tint, each with its parameter ranges.Maps: a highlight takes the colour you give it, and
opacitytints the land. Highlight, colouring andget_highlightedreport the regions that were actually coloured and name any that are not on the map;get_at_pointreturns the region under a point;add_markeradds the marker it reports.pinepaper_agent_start_jobfixes an unbounded canvas at the size it reports, so a PNG export is the full canvas rather than a crop of the artwork.Tool descriptions at every level quote counts and action lists that match the tools.
What's new in 1.6.18
New: spring easings, the standard easing set, and custom curves
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.
New: look at your own animation
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.
New: make one like this
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.
New: start from the real brand
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.
New: watercolour, oil paint and cut paper
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.
New: motion blur
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.
Changed: plan generations before paying for them
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.
Fixed
The scene validator no longer flags valid easings as unknown.
pinepaper_agent_exportalways 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_mediumapply_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 refusedconfigurenow says why, naming the key.ground_heighttakes{x, z}.createlists the configurable parameters by name.A browser session lost while idle recovers by itself:
pinepaper_refresh_pageand 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_world3dextrude_pathtakesupright(stand a drawing up, like a sign) andplacement(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.
What's new in 1.6.17
New: tell an agent "at 4 s, make it orange"
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.
New: cuts on the beat
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.
New: exact frame counts, and larger canvases
pinepaper_agent_exporttakesframesfor an exact frame count (a duration rounds up), for segments that must line up.Canvases up to 16384 px per side, for digital out-of-home and projection; above 8192 the result notes that Safari and iOS may not manage it.
pinepaper_import_imagetakesmaxEdge(or'native') andnativeSize, and says when the studio stored an image smaller than its source.pinepaper_mediaupload_videotakesfit(contain,cover,none), andscaleis documented as relative to it.
Changed: exports say more about what they produced
When an MP4 cannot be made at the requested size, the file is named by what it is (WebM) and a warning says why.
A video whose animation did not play, or whose encoder froze, is flagged.
Asking for ProRes or HAP explains the route that works (a PNG sequence).
Frame captures, region stills and flash checks wait for video on screen, and report whether they could.
The engine's structured warnings reach the caller whole.
Fixed
pinepaper_accessibility_checkjudged 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_exportacceptsfpsandscalefor APNG.
What's new in 1.6.16
New: video editing on the timeline
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.
New: AI image generation (beta)
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.
New: add to an animation instead of replacing it
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.
New: byte-identical exports
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.
Fixed
pinepaper_import_assetwith aurlfailed on every call after fetching the file successfully.pinepaper_soundrender_soundtracktimed out on longer soundtracks; it now has the same time budget as other exports.pinepaper_audio_beatsanswered success with no result for larger audio, and could not read a local file path. Both work now.pinepaper_camera_animatewithout keyframes failed with a raw JSON error; it now says whatkeyframesneeds.pinepaper_precomp:createreturned an internal id that no tool accepted, andadd/removecould not work at all. All three now round-trip the precomp's item id, and a failedremoveis reported.pinepaper_mediaset_time_remap,speed_ramp,match_cutand the track matte actions accept the media id that upload returns, as documented.modify_itemcontenton text whose content is keyframed now warns that the keyframes override it.Exporting a landscape canvas for a portrait platform (TikTok, Stories) — or the reverse — is a fidelity warning with the fix, not only a note.
What's new in 1.6.15
New: the PinePaper mark
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.
New: transparent WebM
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.
New: fonts in multi-page PDFs
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.
Fixed: npx setups could never execute in a browser
Puppeteer 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.
Fixed: text-only properties on shapes
fontWeight, strokePosition and other text-only properties passed to a
non-text item are reported as not applied again.
What's new in 1.6.14
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.
New: ads, variants and broadcast deliverables
html5-adexport: a display-network zip (index.htmlwith thead.sizemeta and aclickTagclick 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.playableexport: a single-file MRAID playable whose CTA (ad.ctaItemId) opens the store link throughmraid.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.scccaptions: 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.
New: text that fits its box
fiton 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: nullstops fitting. The result'stextFitgives the size, line count and whether it fits.Overflow in every export.
fidelityliststext_overflowfor 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;autois reported with the way it resolved.tabularFiguresfor counters and prices that should not jitter, andstrokePosition: 'outside'for thick caption outlines that keep the letterform.Weight, italic, line spacing, and fonts from the catalogue or Google Fonts (
pinepaper_fontgained aloadaction) now reach the text, loaded before it is drawn.
New: export formats and controls
apng— animation with full transparency, for overlays and stickers.jpg/webpstills for byte-budgeted ad specs;srt/vttcaptions 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).regionexports 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.timerenders a still at a chosen moment rather than wherever the playhead is.GIF
loopplay count and amaxBytesbudget (re-encoded smaller, with each attempt reported).Broadcast MP4 —
broadcast: true(BT.709, limited range, constant bitrate),bitrate/minBitrate, andbroadcastHeadroom. The result reports the achieved bitrate and the measured luma; when the file would fail broadcast QC,fidelitysays so and gives the command that fixes it.NTSC rates (29.97, 59.94, …) with a note when a duration overruns at them, and a warning when H.264 rounds an odd side up by a pixel.
New platform presets
print-a4-landscapeandprint-letter-landscape.
New: tools and parameters
pinepaper_lassogainedcut: cut a subject out along a polygon, no custom code.Generator regions can be shaped (rounded rect, ellipse, circle, polygon, star, or an existing path) and named with
region.id; the same generator in several boxes gives several instances.modify_itemtakesskewX/skewYand an affinematrix(mockups).Pixel-art rasters (
smoothing: 'off'), a globedisableaction, keyed audio gain, and widget HTMLlang,dirandalt.bornAt/ttlnow work as a visibility window in local renders, so shots hand over cleanly on the boundary.serverInfo.versionnames the exact build (1.6.14+<sha>).
Fixed: properties that were silently ignored
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).
Fixed: tools that reported success for the wrong thing
Batch create / modify / execute now behave exactly like their single-item tools, with one undo step per batch.
moves_along_pathfollows the path you name (or refuses); relations to an id not on the canvas are refused.Uploaded media: the id you are given is the one other tools accept, audio levels reach the mix,
data:and server-side URLs upload in production, and clearing the canvas removes uploaded media too.A platform preset no longer stretches a canvas of another aspect ratio.
camera_animateno longer claims pitch / yaw as 3D perspective, and itsdurationis optional.Nine tools' published schemas disagreed with what they accepted; a guard now keeps them in step.
New: a whole music bed in one call
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.
Fixed: create_scene drew the wrong shape for six of its ten types
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.
Fixed: layered-character import described the wrong manifest
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.
Fixed: PDF export failed on every scene
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.
Fixed: the validator's own findings were being swallowed
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.
Changed: export fidelity is always reported
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.
Fixed: documentation that described the wrong contract
solveOdestate variables are positional —t,y0,y1,y2… matchinginitialState. There is noxory, so the natural way to write a Lorenz system failed with "Undefined symbol y". The working form is now in the description.moves_along_pathwithparams.equationonly works on the relation's signal path. Easingbounceis piecewise and leaves that path, which silently discards the equation and leaves the item where it started.grid.blockedinpathfindis 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.
What's new in 1.6.13
Fixed: WebGL was switched off, so every shader feature silently did nothing
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.
Fixed: sixteen parameters that were accepted and then ignored
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:
Choropleth maps rendered near-black:
colorScale: "greens"was sent where the engine indexes a two-stop['#from', '#to']ramp, so it indexed a string.minValue/maxValue/showLegendweren't mapped either.Region highlights came out default blue — the colour was stripped, and the engine reads
style.fill.pinepaper_map loaddocumented anoptionsparameter the schema threw away, so{style: "dark"}never arrived.Mask keyframe
timewas capped at 0–1 while the engine reads seconds — a keyframe at 2.0s was a validation error.Animated masks (mode 3) passed the animation preset where the mask shape goes, so the keyframes drove the wrong geometry.
Sound cues needed two calls each because
visual.startTimewas stripped; one call places a cue now.text_effectgainedstartAtandhideAfter— every effect played from t=0, so in a multi-shot video they all fired on the opening frame.import_assetby URL is implemented rather than refused.drawShaderArtdocumentedfragmentSourcewithout mentioning that it is ignored unless you passpreset: "custom", and that the shader is GLSL ES 3.00 (in v_uv/out outColor), notgl_FragColor.Asset search showed
[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.
Fixed: one 3D colour broke every export in the session
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.
Fixed: an unknown item id reported success
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.
Fixed: photos could not be imported from a URL at all
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.
Fixed: the caption system was invisible to the agents told to use it
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.
Fixed: exports sized by the renderer instead of by your request
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"ignoredplatformcompletely: bothyoutube-thumbnail(1280×720) andautoon a 1920×1080 canvas returned 3000×1688, which is the board scaled by 150/96 DPI.pinepaper_agent_end_jobreported 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.
Fixed: size on an effect is a multiplier, not pixels
Every 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.
Fixed: cutout styles and the lasso called into nothing
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.
Fixed: results that were too big to read
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.
Fixed: map tools that called into nothing
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.
What's new in 1.6.12
Fixed: images could not be imported at all
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.
Fixed: parameters that were accepted and then ignored
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:
Ellipses took
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_stickaccepted eight:propSide: 'right'put the prop in the left hand (the engine compares against'L'/'R'),walk: truewalked nowhere, expression beats keyednamewere dropped entirely, andfacingdid nothing on a figure — it is now refused by name rather than ignored.pinepaper_load_mapaccepted seven the engine never reads. Style keys nest understylesand are spelled differently there;enableHover/enableClickareinteractive/selectable.hoverStrokewas 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.
New: anchor — say which corner your x/y meant
Position 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.
New: figures that act, and exports that tell you what they cost
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.
Fixed: discovery
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.
Documentation: shapes described a fraction of what they accept
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.
Fixed: exporting without naming a platform ignored your canvas
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.
Fixed: fps and scale were accepted but undiscoverable
Both 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.
Three design generators are now included
device-mockup, recursive-subdivision and textile-weave existed upstream but had never been copied into the package.
What's new in 1.6.11
Fixed: modifying an item could edit a different one
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.
Fixed: display text styles drew in the wrong font
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.
Fixed: importing an SVG by URL
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.
Fixed: three operations that reported success without doing anything
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.
Two styles were missing from the catalogue
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 item
Pass 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.
Custom generators, and what registering an item buys you
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.
Corrections to earlier release notes
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.
What's new in 1.6.10
Fixed: the nine diagram tools called an object that does not exist
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.
Fixed: six of sixteen font actions
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.
Fixed: local image files, and errors that say why
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".
Fixed: tools could report success when the work failed
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.
Fixed: tools failed when called immediately after connecting
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.
Fixed: pinepaper_batch_modify
It 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.
Fixed: rigging bone angles were read as radians
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.
Fixed: pinepaper_precomp and pinepaper_import_mermaid
precomp 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.
Letter-collage animation works again
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.
Responses are ~90% smaller on batch work
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 traps
Writing 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.
Three routes you had to find by reading source
Text that changes over time.
contentis keyframeable, and it holds rather than tweening —pinepaper_keyframe_animatenow says so, and points at the cheaper route for a cascade. For a word cascade it points atapp.textSequence(item, words, { interval }): one item cycling its words in one call, instead of one item per word with opacity tracks faking the cut.Does this survive export? Answered once, as a rule rather than a per-tool label: if it ticks inside the engine's update loop, it exports — loop animations, relations, keyframes, generators and camera moves all do. Anything driven by the wall clock outside that loop does not. And if exported frames look frozen, check the sampling first: a loop at speed 1 has a one-second period, so frames a whole second apart are identical by design.
Bone angles are sent as degrees and radians, so the studio reads the units you meant rather than inferring them.
Fixed: tools called just after connecting could find a subsystem missing
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.
Fixed: unknown font axes were dropped before the studio saw them
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 changes
quality 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.
Fixed: a batch could report success when an operation failed
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.
New: find out which fonts you can use
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.
Fixed: pinepaper_execute_custom_code could not do anything asynchronous
Your 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.
Fixed: rotation read back as 0 on a rotated item
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.
Under the hood
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.
What's new in 1.6.9
New: pinepaper_export_store
Recover 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 afilePath, 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.
New: scale on pinepaper_agent_export
Render video at a fraction of the platform preset (0.1–1).
scale: 0.5withquality: "draft"is a fast look-check before committing to a full render.Smaller frames also mean smaller files: the encoder derives its target from resolution, so this is the size control. There is no bitrate option.
Video only (
mp4/webm/gif) — passing it topng/svg/pdf/wavis rejected rather than ignored. Omitting it leaves exports exactly as before.
New: list_motion on pinepaper_design_system
Returns 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 tokens
pinepaper_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.
Fixed: compose placed everything off-centre
compose 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.
Fixed: exports could report success while failing
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.
Fixed: long exports timed out at ten seconds
Video exports running longer than about ten seconds failed with a PP_TIMEOUT that looked like a problem with the scene.
PINEPAPER_EXPORT_TIMEOUTsets the budget (default300000ms). For exports over five minutes, raisePINEPAPER_TIMEOUTto match.PINEPAPER_GOVERNOR=offis an escape hatch for older studio builds.
Fixed: exports failed on a fresh session
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.
Fixed: large exports could be left undeliverable
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.
Fixed: connection failures behind proxies, and a false "ready"
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 (defaultdomcontentloaded; setnetworkidle2for the previous behaviour).PINEPAPER_TIMEOUT— navigation and readiness timeout in ms (default30000).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.
Fixed: pinepaper_animate_letter_collage ignored its animation type
The 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.
Documentation
Rotation —
pinepaper_create_itemdocumentsproperties: { rotation: 45 }, and that rotation is baked into the geometry, so readingitem.rotationback returns0. Verify a rotation by geometry or pixels, not by re-reading the property.Dense motion —
pinepaper_keyframe_animatepoints at cheaper routes before you author hundreds of keyframes: thetime_expressionrelation for motion that is a function of time,staggered_with/wave_throughfor a whole group, andpinepaper_execute_custom_codefor procedural scenes.
What's new in 1.6.8
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.
The bug: a streamed export returns a
{streamed:true}marker rather than a Blob, andFileReader.readAsDataURLthrows on it — so any export that streamed broke this path instead of extending it.The ceiling: base64 of a ten-minute 1080p video is a ~1.6 GB string before JSON transport. No memory fix reaches that.
But the ceiling was never the MCP response. This server already wrote video and PDF to a file above 500 KB and handed back a
filePath— the only real limit was the singlepage.evaluatereturn 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, onefilePath, 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.durationgoes 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.Failure leaves the export held. The store is the only copy, so the error names the id and the bytes reached, deletes the partial file, and does not release. An eviction mid-read is reported as an eviction, never as a missing file, and re-exporting stays the caller's decision.
This needs a studio with the export store —
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.
A failed image reported success. The door returned three of the counts the compiler gives it and dropped nine,
imagesFailedamong them — so a raster that never decoded read back assuccess: truewith a fullitemIdsarray. All the counts are surfaced now, andimagesFailedalone 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.
Nodes now carry
generator,generatorParamsandgeneratorRole; a mesh carriesmeshProvenance(op, the node it was derived from, and the options); the 3D stage rides at document level asworld. All of it reaches the JSON-LD.The parameters have to be the merged set. What a generator hands its registry is empty, or two keys of thirty for the GPU generators, and a recipe built from that names a generator it cannot reproduce — which is indistinguishable from a working one until someone re-runs it.
pp:worldis a sibling ofpp:generator, never nested inside it. A World3D scene has no generator, so a stage read insideif (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_threadnow offers all six stitches the engine publishes, not four:runningSeamandcrossStitchwere missing, and they are what a caller reaching for stitchcraft wanted. It also gained the per-stitch parametersSTITCH_OPSdeclares and nothing named —slant,overlap,stagger,gapLen,gridSize.slantis the one that matters most: a satin fill laid at a slant is what separates embroidery from ruling.roughnessis 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:Stitchcraftpoints atpinepaper_design_mediumnow.And the list has one definition. It had four copies here — two Zod enums and their two JSON mirrors — so correcting one left
pinepaper_composeunable to ask for two stitchesapply_threadcould. The engine had the identical split for the identical reason on the same day:MEDIA.thread.stitcheswas hardcoded to the same stale four whileSTITCH_OPSpublished six. Two repos, four copies, one list, drifted the same direction — because neither read the published table. It isTHREAD_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:
A design system is a real, licensed, versioned product — Material Design 3, IBM Carbon, Polaris, Fluent 2, Spectrum, Primer, USWDS, GOV.UK and more. Twenty-one of them, as W3C DTCG tokens.
list_easingsreturns every motion curve across all of them as a named easing with its licence — and withpinepaper:authoredon the three we had to fill in, so a vendor's published curve and one of ours are never mistaken for each other.An aesthetic style is a look: Bauhaus, Art Deco, Swiss, De Stijl, vaporwave, risograph, brutalist, Memphis, claymorphism, op art, pixel art. Thirty-three named, eighteen of which
composea titled scene.draw: falsereturns the scene and the ops as data and draws nothing — read the composition before committing to it, the waypinepaper_story'sdistillworks.
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_stickis 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 frompinepaper_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_storyturns prose into a scene.distillreduces 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.
exportGLBandexportBVHrefuse throughconsole.warnand 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 withcreateObject3Dfirst", "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.
Tones, chords (by name, with the Hz readable without playing), named percussion and SFX, and plain-language descriptions —
from_textresolves "a soft warm bell on A4" to a spec without playing it, so a caller can inspect or edit before committing.Sound and drawing are one object, both ways.
createdraws a sound as a waveform path: edit the path and the timbre changes with it.timbre_from_pathreads any drawn path back as harmonic content — a curve someone drew by hand becomes the partials of a note.timbreFromPathanswers 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.
A group motion (drift, sway, rotate, pulse, wave, bounce) moves the target as one. A field motion (ripple, breathe, undulate) sweeps a crest through the children from an origin, with a chosen waveform — a wave passing through a crowd rather than a crowd moving together. Nothing else on this surface could do the second kind:
pinepaper_animateapplies a loop preset to one item.action: 'list'returns the engine's own catalogue.motionis deliberately a free string rather than an enum: pinning the names here is exactly howadd_relationcame to list 39 of the engine's 134 relations.Origins include
random(every element on its own phase — a twinkle, not a wave) androam(hopping between the corners and the centre); pass aseedto makerandomreproducible.The waveform
spikeis a narrow bump. It was calledpulseuntil 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.
They live apart from
pinepaper_modify_itemon 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_strokeis 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.Each refusal is the engine's own — "a boolean needs at least two paths", "that path has no stroke to outline", "simplify needs a path" — passed through rather than reworded, because two wordings for one condition is the defect this project keeps fixing. The single exception is
get_geometry, whose facade returns a barenull; 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_pathsweeps a canvas path along a depth;lathe_pathrevolves 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, someshProvenance.optsspreads straight back in.Lights (
add_light/set_light/remove_light/list_lights) and shared materials (add_material/set_material/remove_material/list_materials), pluslist_meshesandremove_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.rotYis radians, and says so. It is stored raw and handed tocos()/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.rotYDegreesis the convenience spelling and is folded in before the engine sees it. (arconlathe_pathis degrees; the engine converts that one.)Import and navigation.
import_objandimport_gltfbring real geometry in;list_mesh_clipsandset_mesh_clipplay what a skinned model arrived with.set_nav_targetdecides whether the pointer drives the 2D canvas or the 3D world,ground_heightandcanvas_to_groundandworld_to_canvasconvert between them,raycastpicks, anddolly_camera/pan_cameramove the view.Physics in the world, distinct from
pinepaper_physics(which is the 2DphysicsWorld) and deliberately narrower: a sphere or capsule under gravity, colliding with terrain, objects, actors and imported meshes. No stacking solver and no joints.step_physicsreports 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.Several of those facades answer
null— or0— 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 fromraycastcan only mean a miss, and is reported as one.add_objectstops advertising knobs that reach no pixel. It offeredmetalness,roughnessandemissiveIntensity— 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.
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_percussionanddefine_sfxregister a sound at runtime. It is first-class from then on:listInstruments()reads the live table, so it appears in thelist_*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 registersRhodesand playsRhodesis naming something that does not exist.The engine's rules are mirrored rather than tightened. A melodic instrument needs a partial table because one without renders silence; percussion and SFX may be pure noise, because a hat and a whoosh are, and tightening that would refuse exactly the sounds the relaxation exists for.
pitch: {from, tau}is carried through — the glide is the drum, and without it a defined kick comes out a beep.pinepaper_agent_exporttakes 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: cameraand 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, andestimateOnlyreports confidenceexact— 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_soundtrackmixes 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 asdropped: 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.
What's new in 1.6.7
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.
It replaces roughly thirty exactly-right calls — create the skeleton, add each bone with the right parent and angle, create each shape, attach each to the right bone, then author poses — where one mistake anywhere leaves a broken figure. That volume of exactly-right output is the reported reason characters do not work for smaller models.
Direction is declarative beats:
{ 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.
The description names the parameters and the expression variables a field is written in: a parameter nobody can discover is the same as a parameter that is not there.
bornAt/ttlare documented onproperties. They always passed through (propertiesis 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.A surface is not a shape, and the ontology now says so instead of filing these under Path because they also end up as pixels:
pp:CanvasSurface, withpp:ShaderSurfaceandpp:ParametricFieldunder 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.Angles transfer as bind-pose deltas, so a T-posed CMU rest is not slammed onto a rig with a relaxed stance. Bones the alias table cannot place come back as
unmatchedSource/unmatchedTargetinstead of silently driving half a rig, so a caller can buildboneMapfrom the failure.fpsdefaults 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.
New capability:
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.
The planner is pure and emits keyframes, so the result is ordinary animated items: it scrubs on the timeline, survives undo and session restore, and exports through the existing MP4 / SMIL / Lottie paths. Every effect ends at rest.
It replaces the text item. Unlike
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: trueis the escape hatch. The tool is markeddestructiveHintand says so in its description, because it inverts the id-preservation convention every neighbouring tool follows.Resting characters are painted with a gradient across the text block by default (what the upstream effects actually do);
gradient: falsekeeps the authored fill.seeddefaults 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.
canvasPreseton the agent-flow tools passed an export platform name (instagram,youtube,web) straight tosetCanvasSize, which keys oninstagram-postandfull-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, becauseinstagramis the right word for an export target and only the canvas-size use was wrong.pinepaper_modify_itemnow documentspathData— 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.
42 relations are now callable — the input triggers (
on_click_fire,on_pointer_enter_fire,on_pointer_exit_fire,on_key_fire), the fullon_event_*reaction set including template-interpolated property writes and the persistent-store pair, theon_enter_*/on_exit_*proximity families,exclusive_group/menubar_group, and the behavioural relationsrepels,wiggle,spring_follow,syncs_with,triggers_animation,connects_to,part_of,attached_to_tail,head_points_to,anchored_in_world,tours,synced_to_audio, andexpresses— whichpinepaper_import_layered_characteralready promised would make an imported character blink, while the enum made it unnameable.Relations a dedicated tool emits stay out —
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.The map behind the validator was the same bug one layer down.
RELATION_TYPE_MAPgates "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 42pp:edge definitions were backfilled from the engine's own descriptions.The real fix is the parity test. The enum is duplicated across five tool schemas plus the zod schema, and nothing checked any copy against the engine or against each other. It's now pinned to a fixture of the engine's registry map, asserting in both directions — nothing offered that the engine cannot run, nothing runnable that the surface hides — and that all six copies agree. The additions are just this week's payload.
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."
Hatching states value through line density, not colour. The same shape at 6px spacing and at 3px reads as light and dark with nothing else changed.
gradientmakes the density fall off across the shape — a shaded ramp rather than a flat tone.The straight ruling is what a printer makes;
flowFieldis what makes it read as drawn —handis the small correlated wander of a hand-drawn line,wavesfor water and hair,spiralfor wood grain around a knot.continuousjoins the whole set into one serpentine path.Reimplemented from p5.brush's source (MIT, Alejandro Campos Uribe), not vendored: p5.brush is WebGL2-only and its output is raster, so vendoring it would put a second renderer in front of Paper's vector geometry and forfeit infinite-resolution scaling, SVG export and the relation graph. The maths is renderer-agnostic and is the part worth having.
Every refusal names its fix. The engine reports all five of its distinct failures as
console.warn, which production strips — so the tool checks the shape first and answers "text has no outline, convert it withpinepaper_text_stylefirst" 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.
The one thing to get right: a percentage means different things in different forms.
"-=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.
Rate is deliberately unclamped: 0 freezes the clock without stopping playback, and a negative rate runs the scene backwards. Changing it rebases the clock, so the playhead does not jump. Export is unaffected — a scene watched at 0.5x still exports its real duration rather than a file twice as long.
Scroll binding always releases a previous binding first: the listener holds the scene alive, so a rebind without an unbind is a leak and leaves two bindings scrubbing one timeline.
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.The schema refuses a graph the engine would refuse, and one it would silently mangle: a dangling
to, astartnaming 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.Two ways out of a card:
answerswaits for a click,next(+duration) auto-advances for a linear story beat.The result forwards
failed,wiredandcycles. 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.
It reads
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: falseopts out when a cheap re-read of what is already resident is enough.coveragenames 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.seedgives 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.
What's new in 1.6.6
Dependency security, no new tools and no API changes:
10 vulnerable transitive pins cleared (21 advisories: 1 critical, 13 high, 6 moderate, 1 low) across
basic-ftp,fast-uri,js-yaml,path-to-regexp,ws,ip-address,qs,flatted,body-parserandajv. Each is pinned to a floor inoverridesso neither resolver can drift back.Root cause was a stale committed
bun.lock. It pinned the vulnerable versions whilepackage-lock.jsonhad already re-resolved most of them — andbun test/bun run buildinstall frombun.lock, so that was the tree in use. Both lockfiles now agree.npm auditreported 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.jsonversion 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.
What's new in 1.6.5
Security hardening, no new tools:
Generated code is breakout-proof. Three emitters wrapped user text in hand-escaped quotes without escaping backslashes (CodeQL
js/incomplete-sanitization, High ×3) — an input likex\'; evil()could land outside the string in emitted code. All string literals now emit viaJSON.stringify; regression tests pin the class.Puppeteer is now an optional peer. The 4 browser tools lazy-load it and explain the one-line install (
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.Slimmer tarball. Compiled test fixtures no longer ship in
dist/.
What's new in 1.6.4
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.
Image editing tools:
pinepaper_crop_image(one-shot crop, keeps the item's id and relations) andpinepaper_chroma_key(green-screen background removal with auto-estimated thresholds)pinepaper_mediagainsset_clip— re-trim an already-uploaded video/audio clipShader auras in
pinepaper_apply_effect:heatmap,liquid_metal,gem_smoke(WebGL2, silhouette-clipped)pinepaper_image_filterfixed and expanded — routed to the real GPU filter engineREADME as an MCP resource — clients can read
pinepaper://docs/readme(and per-language variants) without leaving the protocolThis README, in 9 languages, with live animated examples
Toolkits & Token Budget
170 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 |
| Every tool, no filtering |
| Every tool except the diagnostics ( |
| Canvas + diagram + query/export |
| Canvas + map + query/export |
| Canvas + font + letter collage + export |
| Agent, browser, canvas, and guide only |
Verbosity tiers (PINEPAPER_VERBOSITY): verbose, compact (default), minimal.
Client auto-detection. When neither env var is set explicitly, the server picks a profile from the MCP initialize handshake:
Client | Toolkit | Verbosity |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Explicit env vars always win. 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.
Features
🤖 Agent Flow Mode (enforced by default)
Auto-Connection: Browser connects automatically on first tool call (headless mode)
Auto-Session: Agent sessions start automatically — just start creating
Batch Operations: Execute multiple operations in one call (~10x faster)
Smart Exports: Auto-detect optimal format for Instagram, TikTok, YouTube, etc.
"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.
🔄 Relations (Behavior-Driven Animation)
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 |
| Circular motion around a target |
| Move toward target (with offset) |
| Fixed offset from target |
| Hold a set distance |
| Rotate to face target |
| Mirror target's position |
| Depth-scaled movement |
| Stay within an area |
| Wave propagation across items |
| 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 |
| Source's bottom edge rests on the target's top edge — stacking ( |
| Mirror of |
| Flank the target left or right ( |
| Place within the target's bounds at a 9-way |
| Source center = target center + ( |
| Match the target on one |
Structure & construction
Relation | Description |
| Sit at the midpoint of two items |
| Constrain onto a line |
| Sit at the centroid of a set |
| Sit at the circumcenter |
| Share a center |
| Enclose a target |
| Point out / annotate |
| Staged geometric reveal |
Animation & camera
Relation | Description |
| Drive a property over time |
| Scale in from an origin |
| Offset timing across a set |
| Shape morphing |
| Camera behavior |
Deterministic binding (Expression IR)
Relation | Description |
| Bind one property to another: |
| Self-relation: drive a property by a math expression of |
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 |
| When source event fires, pulse the target event after a delay (chaining primitive) |
| On fire, add a relation to an item — the scene evolves itself |
| On fire, tear a relation down |
| On fire, set fill/stroke color |
| On fire, set any item property |
| 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).
🎨 Item Creation & Geometry
"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).
🎬 Simple Animations
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.
🖼️ Background Generators
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
📐 Diagram Tools
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"Shape types — Flowchart: process, decision, terminal, data, document, database, preparation · UML: uml-class, uml-usecase, uml-actor · Network: cloud, server · Basic: rectangle, circle, triangle, star
Connectors — smart routing (orthogonal, direct, curved), arrow styles (classic, stealth, diamond, circle, none), animated bolt effect, labels
Auto-layout — hierarchical, force-directed, tree, radial, grid
Mermaid — import existing diagrams with
pinepaper_import_mermaid
🗺️ Maps
Choropleths, region styling, and data-driven map animation via pinepaper_map, pinepaper_map_regions, pinepaper_map_animation, and pinepaper_map_data.
🔤 Typography
pinepaper_font covers font loading and text-to-path work; pinepaper_create_letter_collage and pinepaper_animate_letter_collage build and animate letterform collages.
🔍 Asset Search & Import
Search and import free SVG icons:
Iconify: 200,000+ icons from multiple icon sets
Font Awesome: 2,000+ free icons (CC BY 4.0)
🖼️ Image Processing & Object Detection
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.
🧠 Ontology & Validation
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.
📊 Performance Metrics
Built-in performance tracking helps AI assistants optimize workflows:
Automatic timing for all tool operations
Phase breakdown (validation, code generation, execution, screenshots)
Export formats: summary, detailed JSON, CSV
Self-optimization through
pinepaper_get_performance_metrics
📊 Training Data Export
Generate instruction/code pairs for LLM fine-tuning:
{
"instruction": "moon orbits earth at radius 100",
"code": "app.addRelation('item_1', 'item_2', 'orbits', {radius: 100})"
}Tools Reference
All 170 tools, grouped by the tag used for toolkit filtering.
Canvas (canvas)
Tool | Description |
| Set background color |
| Set canvas dimensions |
| Read canvas dimensions |
| Clear the canvas |
| Reload the studio page |
| Manage background layers |
Item Creation (core)
Tool | Description |
| Create text, shapes, graphics |
| Change item properties |
| Remove an item |
| Create items in a grid layout |
| Create 3D glossy sphere effect |
| Create diagonal stripe pattern |
| Geometric construction primitives |
| Group / ungroup / break apart |
| Z-order: bring forward/back/front/back |
| Booleans, simplify, outline stroke, repeat patterns, lock |
Batch (batch)
Tool | Description |
| Create multiple items at once |
| Modify multiple items at once |
Import (import)
Tool | Description |
| Import or retarget a BVH motion-capture clip |
| Import SVG markup |
| Import a raster image |
| Detect objects in an image (text queries, composite as nodes) |
| Extract a detected object |
Assets (assets)
Tool | Description |
| Search SVG assets across repositories |
| Import asset from search results |
Relations (relations)
Tool | Description |
| Create a behavioral relationship |
| Remove a relationship |
| Find existing relations |
| Register a custom relation type |
Animation (animation)
Tool | Description |
| Synthesis: tones, chords, SFX, sound-as-waveform-path |
| Generator motion engine: group and field motions |
| Apply a simple loop animation |
| Timed keyframe animation |
| Acting for any item: hop, bounce, roll, fly, peek with squash and stretch |
| Control playback, rate, progress, scroll-driven scrubbing |
| Shape a delay across many items |
| Animate a layout change without describing the motion |
| List animatable properties |
| List easing functions |
| Staged construction animation |
Masks (masks)
Tool | Description |
| Apply an animated mask |
| Apply a custom mask |
| Remove a mask |
| List mask types |
| List mask animations |
Camera (camera)
Tool | Description |
| Camera state control |
| Animate the camera |
| Shot-level camera direction |
Scene & Events (scene)
Tool | Description |
| Create a scene |
| Manage scenes |
| Scene playback control |
| Interactive story / quiz card scene graph |
| Relative timeline positions for a run of clips |
| Relative timeline positions for a run of clips |
| Create / pulse event channels for scene chains |
Generators, Effects & Filters
Tool | Description |
| Run a background generator |
| List available generators |
| AI image generation in the PinePaper cloud (charged; beta, per organisation) |
| Free price check, and the model list |
| Collect a generation that outlived its timeout |
| One composition drawn in a built-in style |
| Apply sparkle, blast, and other effects |
| Restyle an item or the whole scene as a named look, graded for fidelity |
| Original characters from a base rig and a variant; a cast from seeds |
| One element through many states, with the camera riding the morphs |
| Particles as a saved, deterministic item: bursts and trails |
| Custom per-frame drawing a saved scene carries |
| 37 character-level text animations; replaces the text with one keyframed item per character |
| Add an image filter |
| Light the 3D world with point and directional lights |
| Pixel graphs (relight, depth of field, bloom) over an item or the scene |
Editing (selection, transform, history)
Tool | Description |
| Selection management |
| Transform items — fit to frame, nudge, flip, reorder |
| Undo / redo |
| Arrange items into a named collage pattern and film it |
| What makes the marks — media with honest fidelity, and needlepainting |
| Apply brand colours / fonts by role, with a contrast audit |
| Reusable master + instances, with per-instance overrides |
| Resize the artboard; per-item reflow constraints |
| Notes pinned to an item, a point and/or a moment |
| Where an item came from; what depends on it |
| What changed between two scene states |
| Detect beats; bake an item's animation onto them |
| Templates with typed, coerced inputs |
| One creative per data row: apply the row, export it |
Image Processing (image_processing)
Tool | Description |
| Apply image filters |
| Crop an image to a rect (optional aspect ratio) |
| Key out a background color (auto-estimates threshold) |
| Split a raster into a tile grid (inert until animated; group adopts the original id) |
| Lasso selection on images |
| Cutout styling |
Composition (precomp, deform, sprite, interaction)
Tool | Description |
| Pre-composition management |
| Deformation tools |
| Sprite sheet handling |
| Click, hover, and drag interactions |
Data Visualization (dataviz)
Tool | Description |
| Create a chart |
| Function / parametric / Fourier equation paths |
Diagram (diagram)
Tool | Description |
| Create flowchart/UML/network shapes with ports |
| Connect items with smart connectors |
| Connect specific ports on items |
| Add connection ports to items |
| Auto-arrange items using layout algorithms |
| List available diagram shapes |
| Update connector style/label |
| Remove a connector |
| Control diagram editing mode |
| Import a Mermaid diagram |
Map (map)
Tool | Description |
| Create / configure a map |
| Region styling and selection |
| Animate a map |
| Bind data to a map |
| Globe mode + world tour |
Media (media)
Tool | Description |
| Video/audio + editing: upload, trim, time remap / speed ramps, match cut, track matte (live) |
Rigging (rigging)
Tool | Description |
| Skeletons, bones, IK, breakdown poses; BVH mocap import/retarget, Spine import |
| Decomposed character layers → role-bound parts (blink/smile work immediately) |
| DTCG design systems + 18 composable aesthetic styles |
| The vendored stick-figure kit: figure and set |
| Prose becomes a scene: distill, assemble, plan a book |
| Place a figure from the design graph and direct it with beats — no geometry, no bones, no poses |
Typography (font, letter_collage)
Tool | Description |
| Font loading and text-to-path |
| Display text styles (stacked-layer titles) + variable-font axes |
| Create a letterform collage |
| Animate a letterform collage |
Simulation & Utilities (magic, physics, measurement, template)
Tool | Description |
| High-level "make it look good" helpers |
| Physics simulation |
| Game logic: A* pathfinding (feeds moves_along_path) + tilemaps with collision rects |
| 3D world under the canvas: terrain presets, live-sprite actors, follow/orbit camera |
| Measurement and annotation |
| Apply a scene template |
Query (query)
Tool | Description |
| Get canvas items |
| Relation statistics |
| General canvas query |
| Query and recommend capabilities |
Ontology (ontology)
Tool | Description |
| Get the canvas design graph |
| Query the design graph |
| Analyze design quality |
| Validate against design rules |
| General validation |
| Cut points on the music's beats or bars; split a clip at each |
| A property's keyframe series over time |
| Text contrast and flashing (WCAG), as a heuristic screen |
| Validate scene integrity |
| Compile a pp: design graph into a scene |
| Relational-density audit + structural-relation suggestions |
Export (export)
Tool | Description |
| Export animated SVG |
| Export the scene: a summary, or a restorable project file ( |
| Restore a scene from a full export file |
| Put a UI onto a screen in a photo |
| Export LLM training pairs |
| Export an embeddable widget |
| Export widget HTML |
| Capture deterministic frames |
Agent Flow (agent)
Tool | Description |
| Start a content creation job session |
| End job with summary and recommendations |
| Quick canvas reset without page refresh |
| Execute multiple operations in batch |
| Lottie, dotLottie, GLB, BVH, PNG sequence |
| Smart export with platform auto-detection |
| Recover an export the studio is still holding — list, save to a file, or release |
| Analyze content for export recommendations |
Browser (browser)
Tool | Description |
| Connect to the studio |
| Disconnect |
| Take a screenshot |
| Connection status |
Guide & Diagnostics
Tool | Description |
| Server-side guide to the tool surface |
| Switch toolkit profile at runtime |
| Get execution timing metrics |
| Diagnostic report |
Escape Hatches (custom_code, p5, register)
Tool | Description |
| Run custom code against the app |
| p5.js-style drawing |
| Register an externally created item |
Examples
Solar System
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.8Animated Logo
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 backgroundFollowing Labels
1. Create a circle as "player"
2. Create text "Player 1" as the label
3. Add relation: label follows player with offset [0, -50]Event-Driven Scene Chain
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-stableFlowchart Diagram
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-layoutNetwork Diagram
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-layoutArchitecture
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) │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────┘Development
Setup
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 buildTest with MCP Client (Local)
Build the server:
bun run buildAdd 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?"
Run Tests
Tests run on the Bun test runner.
bun test
# With coverage
bun test --coverage
# Typecheck
bun run typecheckManifest Check
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 sourceDevelopment Watch Mode
bun run devInternationalization (i18n)
PinePaper MCP Server supports 51 languages, providing localized tool descriptions and messages for AI agents.
Supported Languages
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 |
Setting Language
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');Adding New Languages
Create a new locale file in
src/i18n/locales/(e.g.,xx.ts)Copy the structure from
en.tsTranslate all strings
Export from
src/i18n/locales/index.tsAdd to the
localeMap
See CONTRIBUTING.md for detailed guidelines.
Configuration
Environment Variables
Variable | Description | Default |
| PinePaper Studio URL to connect to ( |
|
| Run the browser headless (set |
|
| Service-account key ( | — |
| The PinePaper cloud those tools call |
|
|
|
|
| Directory for exported files |
|
| Language locale code |
|
| Toolkit profile ( | auto-detected |
| Explicit comma-separated tool allowlist | unset |
| Description verbosity ( |
|
| Deprecated alias for | unset |
| Enable performance metrics tracking |
|
| Max metrics to retain in memory |
|
| Screenshot mode ( |
|
| What a navigation waits for ( |
|
| Navigation and readiness timeout, in ms |
|
| Chrome | unset |
| Governor budget for an export run, in ms (raise |
|
|
| on |
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.
Performance Metrics
Key Features:
⚡ Automatic timing for all tool operations
📊 Phase breakdown (validation, code generation, browser execution, screenshots)
🎯 Real-time query via
pinepaper_get_performance_metricstool📈 Export formats: summary, JSON, CSV
💾 In-memory storage (resets on restart)
🚀 Minimal overhead (~1ms per operation)
Quick 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=5000Learn More: See docs/PERFORMANCE_METRICS.md for complete documentation.
Documentation
Guides
Workflow Guide — Decision trees, multi-step patterns, performance optimization, and troubleshooting
Performance Metrics — In-memory metrics system for AI self-optimization
Testing Guide — Test layout and conventions
PinePaper Reference — Complete PinePaper Studio API reference
External Documentation
Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Development Workflow
Fork the repository
Create a feature branch
Make your changes
Run tests:
bun testSubmit a pull request
License
MIT License - see LICENSE for details.
Links
Support
📧 Email: support@pinepaper.studio
🐛 Issues: GitHub Issues
Made with ❤️ by the PinePaper team
This server cannot be deployed
Maintenance
Related MCP Connectors
- tonpitOAuthcom.tonpit
Video production studio for AI agents: AI media, motion graphics as code, timeline and export.
Build and run visual creative-production workflows from your AI agent.
Make AI-drawn video from chat: your LLM draws every frame in code; add narration, music and SFX.
Agent-Native design tool - create and edit visual designs with agent assistance
Related MCP Servers
- AlicenseAqualityDmaintenanceTurns plain-language descriptions into animated SVGs with a live preview and conversational editing.8MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI to create ASCII and SVG art through a character canvas, with tools for drawing, previewing, and exporting to multiple formats, as well as composing stop-motion animations with optional voice-over.MIT

Rayzia MCPofficial
AlicenseNot gradedqualityCmaintenanceEnables AI agents to drive a live SVG/vector editor, allowing a full observe-and-act loop on a canvas with real tools, state reading, and PNG rendering.1MIT- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to create and edit Rive animations through 139 MCP tools, supporting shapes, animations, state machines, physics, and export to .riv or .rev files.453 npm1-