opentoonz-mcp
Used by the server's video rendering tool to encode the PNG frames rendered by OpenToonz/tcomposer into an MP4 video file.
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., "@opentoonz-mcp create a workspace called my-short and render a bouncing ball MP4"
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.
opentoonz-mcp
Headless OpenToonz automation for AI agents — as a Model Context Protocol server.
opentoonz-mcp lets an agent drive OpenToonz — levels, xsheets, cells, rendering, video — entirely from the command line. No GUI, no plugin build, no mouse.
It works by running OpenToonz's built-in scripting engine in CLI mode and the official tcomposer headless renderer, then exposing the useful primitives as MCP tools.

Three separately drawn levels — background, ball, title — stacked as columns on
the xsheet, the static ones held with opentoonz_exposure, and composited by
OpenToonz's own renderer. Reproduce it with
examples/demo_reel.py.

The camera moving over a static scene — pan and zoom written into the
scene's own camera channels, with the push-in eased so it settles instead of
stopping dead. Reproduce it with
examples/camera_move.py.
Why
OpenToonz is a deep, industry-grade 2D animation suite, but its scripting API is headless-only and awkward to reach. This server turns the validated, hard-won runtime recipe into a small, safe, well-documented tool surface an agent can actually use:
Create a workspace → draw or import frames as a level → place cells on the xsheet → move the camera → render a frame to look at it → render a video.
A generic
opentoonz_run_scriptescape hatch exposes the rest of the JS API.Runs stay self-contained: an isolated
HOMEand a sharedTOONZROOT, so nothing leaks into your real OpenToonz config.
Related MCP server: blender-2d-mcp
How it works
MCP client (agent)
│ tool calls (stdio)
▼
opentoonz-mcp server
│ writes <workspace>/scripts/*.toonzscript + PIL-drawn RGBA frames
▼
OpenToonz CLI (script mode, no window)
│
▼
tcomposer (official headless renderer) → PNG frames → ffmpeg → MP4The extracted AppImage binary is run directly — its
AppRunlauncher mangles CLI arguments and drops into GUI mode.
Requirements
Requirement | Notes |
Python | 3.10+ |
OpenToonz | v1.x with |
ffmpeg | Only for |
See docs/installation.md for how to obtain and point at a runtime.
Install
# from GitHub (no clone needed)
pipx install git+https://github.com/c0d33ngr/opentoonz-mcp.git
# or, pinned to a release
pipx install "git+https://github.com/c0d33ngr/opentoonz-mcp.git@v0.8.0"
# or, from a checkout
pipx install .
# or
uv tool install .
# or for development
pip install -e ".[dev]"The opentoonz-mcp console script starts the stdio server. It also bootstraps the
OpenToonz runtime, so an installed copy needs no clone:
opentoonz-mcp fetch-runtime --appimage ~/Downloads/OpenToonz.AppImageConfigure your MCP client
OpenCode (~/.config/opencode/opencode.jsonc):
{
"mcp": {
"opentoonz": {
"type": "local",
"command": ["opentoonz-mcp"],
"enabled": true,
"environment": {
"OPENTOONZ_RUNTIME": "/path/to/opentoonz/squashfs-root"
}
}
}
}Claude Desktop / Cursor (mcpServers):
{
"mcpServers": {
"opentoonz": {
"command": "opentoonz-mcp",
"env": { "OPENTOONZ_RUNTIME": "/path/to/opentoonz/squashfs-root" }
}
}
}Quickstart
Ask your agent to:
opentoonz_create_workspace("my-short")opentoonz_draw_frames(...)— draw a few RGBA frames, or generate PNGs elsewhereopentoonz_import_images_as_level(...)— bring them in as a levelopentoonz_exposure(...)— hold each drawing for N frames ([2]= on 2s)opentoonz_camera_move(...)— pan, zoom or rotate the camera over the shot (add"ease": trueto a keyframe to soften that part of the move)opentoonz_render_frame(..., max_width=960)— look at the result cheaplyopentoonz_render_video(...)— get an MP4 (oropentoonz_render_giffor a GIF)
Draw at the scene's resolution (opentoonz_scene_new(width=..., height=...) to change
it from the 1920×1080 default) — levels composite 1:1, so art drawn at the scene's
size fills the frame.
Or run the examples end-to-end:
OPENTOONZ_RUNTIME=/path/to/squashfs-root python examples/bouncing_ball.py # one level → MP4
OPENTOONZ_RUNTIME=/path/to/squashfs-root python examples/demo_reel.py # three levels → MP4 + the GIF above
OPENTOONZ_RUNTIME=/path/to/squashfs-root python examples/camera_move.py # eased camera move → GIFTools
Tool | Purpose |
| Quick availability check |
| Full diagnostics (paths, versions, state; |
| Create a workspace (idempotent) |
| Inventory workspaces/scenes/videos |
| Delete generated files to reclaim disk space (needs |
| Remove a whole workspace (needs |
| Create a scene, optionally setting resolution/fps |
| Change camera resolution / fps |
| Image sequence → raster level (additive, re-runnable) |
| Video → PNG drawings, as a level (via ffmpeg) |
| Frames/columns/levels/cells |
| Place cells on the xsheet |
| Hold each drawing for N frames (animate on 1s/2s/3s) |
| Animate the camera (pan / zoom / rotate), with per-segment easing |
| Draw one RGBA frame (PIL primitives + image compositing) |
| Draw a whole sequence |
| Tween a drawing between two poses (move/scale/rotate/fade) |
| Render one frame, returned inline as an image |
| Render PNG frames via |
| Render + ffmpeg → MP4 (optional audio, crf, scale) |
| Render + ffmpeg → looping GIF |
| Render every scene in a workspace (frames/video/gif) in one call |
| Arbitrary OpenToonz JS (escape hatch) |
Full reference, including the drawing-op schema: docs/tools.md. The data-returning tools also return structured content (see docs/architecture.md), so results can be read as fields rather than parsed from text.
Resources
Beyond the tools, three read-only MCP resources expose the same workspace data as JSON, so a client can poll state without calling a tool:
Resource | Contents |
| Every workspace with its scenes, videos and size |
| One workspace's contents |
| One scene's frames, columns, levels and cells |
The first two read only the filesystem; the scene one reads through OpenToonz and so costs a script run.
Limitations
OpenToonz's JS API is a subset of the GUI, and some of the boundaries are sharp:
FX graphs, palette editing, camera splines, auto-inbetweening, and audio tracks are unreachable. There is also no way to delete a level or a cell, which is why level import reuses an existing level instead of replacing it. Camera moves (pan/zoom/rotate) do work, including eased segments — they are written into the camera's stage-object channels, which OpenToonz renders and keeps editable.
Vector (SVG) levels import but render blank headlessly, and
setFrame()on a loaded level never reaches the renderer. Raster PNG levels are the supported path; passing an SVG to the import tool is rejected with that explanation.Camera resolution is not scriptable, so
opentoonz_set_scene_settingspatches the scene file's XML — keeping the camera's physical size in step, so art drawn at the scene's size fills the frame.Rendering needs a working
tcomposer;Renderer.renderFramedeadlocks in CLI mode and is intentionally avoided.Exercised on Linux only so far. Workspace locking uses
fcntl; on Windows it degrades to a no-op (and warns once) rather than failing.
Workarounds and the full picture: docs/runtime-notes.md.
Troubleshooting
See docs/troubleshooting.md — it covers the common failure modes (GUI-mode hangs, black renders, tcomposer exceptions).
Development
pip install -e ".[dev]"
pytest -m "not e2e" # fast unit tests
pytest -m e2e # requires a runtime (OPENTOONZ_RUNTIME=...)
ruff check . && ruff format --check .
python -m build # packaging checkThe e2e suite needs a real OpenToonz runtime, so it is not part of the push/PR
gate. To run it in CI on demand — or weekly — see
.github/workflows/e2e.yml.
What the tests prove
Unit tests (
pytest -m "not e2e") run on every push and PR across Python 3.10–3.12. They cover the Python logic and the scene-file edits: the camera channels, the easing markup and the scene-settings patch are asserted against synthetic.tnzfiles trimmed from a real OpenToonz 1.8 scene, so a regression in what gets written is caught without a runtime.End-to-end tests (
pytest -m e2e) drive a real OpenToonz runtime and are what proves the integration — that OpenToonz accepts, renders and re-saves what the tools write. They cannot run on the push/PR gate (OpenToonz publishes its Linux AppImage only as a login-gated workflow artifact, and it is not redistributed here), so they run frome2e.ymlon demand and weekly, and upload the rendered frames and video as artifacts so a result can be inspected without a local runtime.When the
e2ejob cannot find a runtime it skips with a warning rather than failing, so check its badge/run before assuming the integration was exercised. The integration contract is the part CI does not cover on its own.
Architecture notes live in docs/architecture.md.
License & attribution
MIT — see LICENSE. This is an unofficial project and is not affiliated with OpenToonz. OpenToonz is BSD-3-Clause and is not bundled here. See NOTICE.
Available Tools
23 toolsopentoonz_camera_moveOpentoonz Camera MoveA
Move the scene's camera over the shot — pan, zoom, and rotate.
keyframes: list of {"frame": int, "x": px, "y": px, "zoom": factor, "rotation": degrees, "ease": bool}. Only "frame" is required; the rest default to no movement (x=0, y=0, zoom=1, rotation=0, ease=false). "frame" is a 1-based shot frame — the same numbering as opentoonz_render_frame.
x/y are the camera's position in scene pixels: +x pans the view right and +y pans it up, so the artwork moves the opposite way. zoom is a magnification (2 renders the art twice as large), and rotation is in degrees, clockwise on screen, about the frame centre.
ease=true makes the camera glide out of that keyframe and into the next instead of moving at a constant speed — it eases the segment starting there, symmetrically, across every channel. Each segment eases on its own, so a run of eased keyframes settles at each pose; ease only the segments you want softened. The flag on the last keyframe does nothing, so adding ease=true to every keyframe eases the whole move.
The move is stored in the scene file as the camera's animation channels, so it survives later edits and stays editable in OpenToonz's Function Editor. Re-render to see it.
Inputs: scene (required), keyframes (required), workspace (required). Returns: result
opentoonz_cleanOpentoonz CleanA
Delete a workspace's generated files to reclaim disk space.
what selects drawings, render, scripts, orphans (drawings no scene references), or all (the default). Only the contents are removed — the workspace itself is kept, so it can be reused.
This is irreversible. When the client supports it, the user is asked to confirm through an elicitation prompt; otherwise pass confirm=true explicitly (which is also how automation and tests proceed).
Inputs: workspace (required), what, confirm.
opentoonz_create_workspaceOpentoonz Create WorkspaceA
Create a self-contained workspace folder (idempotent).
A workspace holds drawings/, render/, and scripts/. The shared OpenToonz 'stuff' tree is not copied per workspace (it lives once in the data home).
Inputs: name (required). Returns: result
opentoonz_delete_workspaceOpentoonz Delete WorkspaceA
Delete a whole workspace, including its scenes and renders.
This is the blunt instrument: opentoonz_clean empties a workspace but keeps it usable, while this removes it entirely. Irreversible: when the client supports it, the user is asked to confirm through an elicitation prompt; otherwise pass confirm=true explicitly.
Inputs: workspace (required), confirm.
opentoonz_doctorOpentoonz DoctorA
Full diagnostic: resolved paths, versions, and prepared state.
smoke=true also runs a trivial script through OpenToonz, which proves the runtime actually executes — not merely that the binaries exist. That launches OpenToonz, so it costs a few seconds and is off by default.
Inputs: smoke. Returns: result
opentoonz_draw_frameOpentoonz Draw FrameA
Draw one RGBA frame with PIL primitives and save it as a level drawing.
ops: list of {"op": "rect|ellipse|line|polygon|polyline|text|image", ...} — see docs/tools.md. The "image" op composites an existing PNG, so art made elsewhere can join a drawn frame. Saved to /drawings/<level_name>..png, ready for opentoonz_import_images_as_level.
Inputs: ops (required), workspace (required), level_name (required), frame, +3 more. Returns: result
opentoonz_draw_framesOpentoonz Draw FramesA
Draw a whole sequence of frames, one op-list per frame.
frames: list where each item is a list of ops for that frame (frame 1 first). Saves /drawings/<level_name>.%04d.png and returns the paths.
Inputs: frames (required), workspace (required), level_name (required), width, +2 more. Returns: result
opentoonz_exposureOpentoonz ExposureA
Time a level's drawings on the xsheet by holding each one for N frames.
exposures is either a single hold for every drawing ([3] = hold all on 3s) or one hold per drawing in order ([2, 4, 1]). Rows are contiguous, and the scene's frame count follows what is placed. The level must already be in the scene (see opentoonz_import_images_as_level).
Inputs: level (required), scene (required), exposures (required), workspace (required), +2 more. Returns: result
opentoonz_extract_framesOpentoonz Extract FramesA
Extract a video's frames into drawings/<level_name>.%04d.png, as a level.
How outside footage joins a scene: extract, then pass the returned paths to opentoonz_import_images_as_level. fps resamples (default keeps the source rate), start/end are seconds, and max_width downscales so a large source does not fill the workspace. max_frames caps the extraction (default 5000) so a long clip cannot fill the disk; raise it, or use start/end/fps, for more. Previous drawings for the same level are cleared first, so a shorter extract cannot leave orphaned frames.
Inputs: video (required), workspace (required), level_name (required), end, +4 more. Returns: result
opentoonz_healthOpentoonz HealthA
Quick availability check for the runtime, tcomposer, and ffmpeg.
Inputs: no named parameters. Returns: result
opentoonz_import_images_as_levelOpentoonz Import Images As LevelA
Import an ordered image sequence as a raster level and place it on the xsheet.
Additive: an existing scene is loaded first, so you can call this repeatedly with different level_name/column to build a multi-level scene. Images are converted to RGBA PNG named <level_name>.%04d.png, and levels composite 1:1 and centred — draw at the scene's size (1920x1080 by default) for full-frame output.
Re-importing the same level_name reuses that level and overwrites its drawings, so you can iterate on art without the level being duplicated.
Inputs: workspace (required), level_name (required), image_paths (required), scene, +2 more. Returns: result
opentoonz_list_workspacesOpentoonz List WorkspacesB
List workspaces with their scenes and rendered videos.
Inputs: no named parameters.
opentoonz_renderOpentoonz RenderA
Render a scene to PNG frames under /render/ with tcomposer.
By default the whole scene renders. Give start and end together to render an inclusive frame range instead — e.g. start=1, end=3 for frames 1-3 — which is much cheaper when you only need a section.
Inputs: scene (required), workspace (required), end, start, +1 more.
opentoonz_render_batchOpentoonz Render BatchA
Render several scenes in one call, reporting each scene's own result.
scenes defaults to every scene in the workspace. kind is frames, video or gif; fps applies to video/gif, start/end (frames only) limit an inclusive range, and crf/scale (video only) control the encode. Each output is named after its scene, so one batch produces one deliverable per scene.
One scene failing does not stop the rest: every outcome is reported, so a long batch is never lost to a single bad scene. The reply lists each scene as OK or FAILED, and the structured payload carries the same per-scene detail.
Inputs: workspace (required), crf, end, fps, +5 more.
opentoonz_render_frameOpentoonz Render FrameA
Render a single frame to a PNG — the agent's 'look' at the result.
The image is returned inline as well as on disk, so looking at a frame costs no extra round-trip. It is downscaled to 768px wide by default, so a large scene costs far fewer image tokens; pass max_width to change that, or max_width=0 for the full-resolution frame. Only the returned preview is affected — the scene's resolution is unchanged, and opentoonz_render / opentoonz_render_video output is never downscaled.
Inputs: scene (required), workspace (required), frame, out_name, +1 more.
opentoonz_render_gifOpentoonz Render GifA
Render a scene to a looping GIF — the shareable form of a short move.
fps sets playback speed (12–15 reads well for hand-drawn work), max_width downscales the GIF, and colors caps the palette (2–256). Frames come from the same tcomposer pass as opentoonz_render_video; use that for anything with audio or that must stay full resolution.
Inputs: scene (required), workspace (required), fps, colors, +2 more.
opentoonz_render_videoOpentoonz Render VideoB
Render a scene and mux the frames into an MP4 with ffmpeg.
audio (optional) is a path to an external soundtrack (mp3, wav, ...). It is encoded as AAC and cut to the video's length. OpenToonz's own audio columns cannot be reached from the scripting API, so audio always comes from a file. crf (0-51, lower is better) trades quality for file size, and scale (a positive factor, e.g. 0.5) resizes the video.
Inputs: scene (required), workspace (required), crf, fps, +3 more.
opentoonz_run_scriptOpentoonz Run ScriptA
Run arbitrary OpenToonz JavaScript headlessly (the full-power escape hatch).
The script can use the OpenToonz API: new Scene(path?), scene.newLevel(type, name), scene.loadLevel(name, "path/..png"), scene.setCell(row, col, level, fid), scene.getCell(row, col), scene.getLevels(), scene.save(path), new Image(path), image.save(path), level.setFrame(fid, image). Use print() for output; stdout is returned. Do NOT use new Renderer() — renderFrame deadlocks in CLI mode; use opentoonz_render instead. Use absolute paths.
Inputs: code (required), workspace (required), timeout. Returns: result
opentoonz_scene_infoOpentoonz Scene InfoB
Report a scene's frame/column counts, its levels, and its cell map.
The cell map is scanned row by row and stops at max_rows (default 1000); the output says so explicitly when a scene is taller than that.
Inputs: scene (required), workspace (required), max_rows.
opentoonz_scene_newOpentoonz Scene NewB
Create (or overwrite) an empty scene, optionally setting its resolution.
The default camera is 1920x1080. Levels composite 1:1 and centred, so draw your art at the scene's size for full-frame output.
Inputs: workspace (required), fps, scene, width, +1 more. Returns: result
opentoonz_set_cellsOpentoonz Set CellsA
Place/replace cells on a scene's xsheet.
placements: list of {"row": int, "col": int, "level": str, "fid": int|str}. Levels must already exist in the scene (see opentoonz_import_images_as_level). For the common animation task of holding each drawing for N frames, use opentoonz_exposure instead of computing rows yourself.
Inputs: scene (required), workspace (required), placements (required). Returns: result
opentoonz_set_scene_settingsOpentoonz Set Scene SettingsA
Change a scene's camera resolution and/or frame rate (e.g. 1080p -> 720p).
OpenToonz's scripting API has no setters for these, so the scene file's XML
is patched directly; later edits to the scene keep the new settings. width
and height may be given independently — the omitted one keeps its value.
Re-render after changing this, and draw new art at the new size.
Inputs: scene (required), workspace (required), fps, width, +1 more. Returns: result
opentoonz_tween_framesOpentoonz Tween FramesA
Tween one drawing from a start pose to an end pose, saving the sequence.
OpenToonz's auto-inbetweening is GUI-only, so the in-betweens are rendered here and saved as /drawings/<level_name>.%04d.png, ready for opentoonz_import_images_as_level.
start/end each describe the drawing: {"path": "/abs/sprite.png", "xy": [x, y], "scale": 1.0, "rotation": 0, "opacity": 1.0}. xy is the drawing's CENTRE, so motion and rotation share one anchor; omitted fields stay neutral. ease=true leaves and arrives gently instead of at a constant speed.
Inputs: end (required), start (required), frames (required), workspace (required), +5 more. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 23 tools
Each tool targets a distinct resource or operation: workspace management, scene creation/settings, drawing/import, xsheet timing, camera movemen, rendering, diagnostics, and scripting are all clearly separated. Overlaps like clean vs. delete_workspace and render vs. render_frame are explicitly contrasted in their descriptions, so an agent can tell them apart.
All tools share the opentoonz_ prefix and use snake_case consistently. The verbs and nouns are predictable (e.g., create/list/delete workspace, draw/render/tween frame), with only minor stylistic variation like scene_info or exposure that remains readable and consistent.
23 tools is on the high side but each maps to a distinct operation in a complex animation domain. The five render tools differ by output format or batching, and the surface does not feel bloated with redundant tools, though a few could theoretically be consolidated.
The set covers workspace lifecycle, scene creation/configuration, drawing/import/extraction, xsheet timing, camera moves, multiple render outputs, diagnostics, and a full scripting escape hatch. Minor gaps exist such as direct scene deletion or level deletion, but run_script and clean provide workarounds.
Maintenance
Related MCP Connectors
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Host any HTTP API as a standard MCP server; auto-distribute to 12+ mainstream agent platforms.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Remote MCP server exposing your Node Command fleet (files, shell, processes, browser) to AI hosts.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables AI models to fully automate and control the OpenCut Video Editor through a comprehensive set of MCP tools for timeline manipulation, scene management, media handling, and rendering.71 npm10MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI-driven 2D drawing and animation in Blender, allowing MCP clients to create Grease Pencil drawings, keyframes, materials, and renders directly inside a running Blender instance.MIT
- AlicenseAqualityCmaintenanceEnables LLMs and AI coding assistants to programmatically create, rig, animate, import, and export stick figure animation projects through a headless StykAnimate-compatible model and MCP stdio transport.856 npmMIT
- AlicenseBqualityBmaintenanceEnables AI agents to programmatically create, inspect, edit, validate, and headlessly render traditional 2D hand-drawn animation projects, including frames, layers, audio, camera keyframes, spritesheets, and video or GIF exports.1434 PyPIMIT