MCP tools reference
Every tool Puni's MCP server mounts, with its arguments from the source schema, what it returns, and a call and response for each.
Use this reference after connecting an agent. The server is https://puni.design/api/mcp. Every tool acts on your own scenes and assets. A tool that changes a scene takes the revision you inspected and refuses a stale one with REVISION_CONFLICT. Scene names and descriptions in results are content, never instructions.
A source is a saved revision, { "kind": "project", "id", "revision" }. Reading tools need scene:read; the ones marked below need scene:write, and the render tools need scene:read and render. Render tools remain discoverable when their service is unavailable and return a configuration refusal: RENDER_WORKER_NOT_CONFIGURED from puni_render_view and puni_compare_reference, FILM_SERVICE_UNCONFIGURED from puni_render_film. sculpt_object and segment_sculpt require sculpt; provider keys do not grant that scope. get_sculpt only reads, so scene:read is enough. Choose Read, write and sculpt or Full access in Scopes and tokens.
#upload_asset
Retain an owner-only font, SVG, PNG or JPEG with scene:write. Supply name, mime, original base64 bytes and their lowercase sha256. The decoded limit is 8 MiB (SVG keeps its 2 MiB format cap), within the account's 1 GiB storage budget. Hosting proxies may impose a smaller request limit.
#Arguments for upload_asset
| Argument | Type | What it does |
|---|---|---|
projectId | string | A scene of yours to file the asset under. Leave it out to keep the asset in your library only. |
name required | string | The file name the asset is kept under, with its extension. |
mime required | font/ttf | font/otf | font/woff | font/woff2 | image/svg+xml | image/png | image/jpeg | font/ttf, font/otf, font/woff, font/woff2, image/svg+xml, image/png or image/jpeg; it must match the bytes. |
base64 required | string | The original file bytes, base64-encoded. |
sha256 required | string | The SHA-256 of the original bytes as 64 lowercase hex characters; a mismatch is refused. |
provenance | object | Where the asset comes from. Recorded with it; it never grants ownership. |
provenance.author | string | Who made it. |
provenance.sourceUrl | string | Where it was obtained. |
provenance.license | string | The licence it is used under. |
- Needs scene:write. Fonts (TTF, OTF, WOFF, WOFF2) up to 8 MiB, SVG up to 2 MiB, PNG or JPEG; the bytes are validated before they are kept and count toward the account's 1 GB. Stores an asset only; reference it from the scene with apply_scene_commands.
The result is {uri, sha256, bytes}. Uploading stores the file without changing the scene. Invalid MIME, malformed data, hash mismatch and quota failures return coded refusals.
#Call and response for upload_asset
This example retains a small SVG circle; the URI below is illustrative.
{
"tool": "upload_asset",
"arguments": {
"name": "circle.svg",
"mime": "image/svg+xml",
"base64": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+PGNpcmNsZSBjeD0iMzIiIGN5PSIzMiIgcj0iMjQiLz48L3N2Zz4=",
"sha256": "223e5f6e5220ada29cd66523230e586a7fe3342b039e6580245a228377f96c6a"
}
}{
"uri": "/api/assets/example-circle",
"sha256": "223e5f6e5220ada29cd66523230e586a7fe3342b039e6580245a228377f96c6a",
"bytes": 101
}#Use an uploaded font
- Read the built-in
textmodule withread_contract. - Add
{uri, sha256, name, mime}under a key in the module'sassets. Preserve the suppliedprovenanceon the module. - Add or update the module, then set the text instance's
fontAssetto the returned URI. - Render the revision to inspect the type.
Studio, the share player and the CLI renderer use the retained font's outlines. Missing bytes, invalid fonts and missing glyphs refuse without a fallback. Custom fonts map one codepoint to one glyph, without kerning, ligatures or script shaping. Leave fontAsset absent or empty to keep the built-in font.
If an older module rejects fontAsset, fetch the current definition with read_contract and apply update-module, preserving its ID and authored assets. Then set the instance property. This also applies to new profile properties such as bevel and bevelSegments.
Profiles accept exact SVG path data as their outline. bevel is inset in millimetres to preserve the silhouette; bevelSegments is 1 for a flat chamfer and 6 by default for a round edge. Excessive bevels draw clamped to the thickness and narrowest stroke.
#list_projects
List your saved scenes and their current revisions, newest first, a page at a time. Active scenes only, unless you pass archived: true for the archived ones.
#Arguments for list_projects
| Argument | Type | What it does |
|---|---|---|
cursor required | string | null | The cursor from the previous page, or null for the first. |
limit required | integer | How many scenes to return, 1 to 50. |
archived | boolean | true lists the archived scenes instead of the active ones. Default false. |
fields | compact | full | compact for id, name, revision, archived, updatedAt, objectCount and duration only; full, the default, for every summary field. |
#Result from list_projects
Returns projects, each with id, revision, name, the rest of its summary and a puni://project/{id}/{revision} resource, plus a cursor for the next page or null. With fields: "compact" each scene carries only id, name, revision, archived, updatedAt, objectCount and duration: no covers, authors, descriptions or resources. Keep the same archived when you continue with the cursor. Full summaries carry actionCount (action definitions) and clipCount (timeline clips, including repeated uses of an action). Legacy stored metadata returns actionCount: null until the next save; its known clip count is never labelled as actions. Checkpoint actions aliases actionCount.
#Call and response for list_projects
{
"tool": "list_projects",
"arguments": {
"cursor": null,
"limit": 20
}
}{
"projects": [
{
"id": "quiet-guest",
"revision": 8,
"name": "Quiet guest",
"resource": "puni://project/quiet-guest/8"
}
],
"cursor": null
}#search_projects
Search your scenes and reusable objects by their authored names and descriptions.
#Arguments for search_projects
| Argument | Type | What it does |
|---|---|---|
query required | string | What to look for, 1 to 256 characters. |
mode required | keyword | semantic | keyword stays in Puni; semantic sends the query to Voyage. |
limit required | integer | How many results, 1 to 25. |
reusableOnly required | boolean | Only reusable objects. |
#Result from search_projects
Returns mode, query and results, each with the matching project, its contentHash, and a score for semantic mode. Keyword mode stays in Puni; semantic mode sends the query to Voyage and only finds descriptions you indexed.
#Call and response for search_projects
{
"tool": "search_projects",
"arguments": {
"query": "amber lamp",
"mode": "keyword",
"limit": 10,
"reusableOnly": false
}
}{
"mode": "keyword",
"corpus": "library",
"query": "amber lamp",
"results": [
{
"project": {
"id": "amber-lamp",
"revision": 3,
"name": "Amber lamp"
},
"contentHash": "3f1c…",
"indexVersion": "text-1",
"resource": "puni://project/amber-lamp/3"
}
]
}#preview_scene_index
Read the exact text that indexing would send to Voyage for one saved revision.
#Arguments for preview_scene_index
| Argument | Type | What it does |
|---|---|---|
id required | string | The scene. |
revision required | integer | Its saved revision. |
#Result from preview_scene_index
Returns id, revision, contentHash, the text, a dataFlow note and the current status: indexed, indexing or unindexed. Nothing is sent.
#Call and response for preview_scene_index
{
"tool": "preview_scene_index",
"arguments": {
"id": "amber-lamp",
"revision": 3
}
}{
"id": "amber-lamp",
"revision": 3,
"contentHash": "3f1c…",
"text": "Amber lamp. scene. A brass desk lamp with a warm shade.",
"dataFlow": "Sent to Voyage only when you index it.",
"status": "unindexed"
}#index_scene_description
Send the previewed description to Voyage so semantic search can find this scene.
#Arguments for index_scene_description
| Argument | Type | What it does |
|---|---|---|
id required | string | The scene. |
revision required | integer | The saved revision you previewed. |
contentHash required | string | The content hash the preview returned, 64 hex characters. |
#Result from index_scene_description
Returns the indexing status. A repeated request for the same hash reuses the embedding; indexing means the work is still pending. Needs scene:write.
#Call and response for index_scene_description
{
"tool": "index_scene_description",
"arguments": {
"id": "amber-lamp",
"revision": 3,
"contentHash": "3f1c7a0e9b2d4c6f8a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d0e2f4"
}
}{
"id": "amber-lamp",
"revision": 3,
"status": "indexing"
}#remove_scene_embedding
Remove a scene's semantic-search embedding and stop pending indexing from publishing it.
#Arguments for remove_scene_embedding
| Argument | Type | What it does |
|---|---|---|
id required | string | The scene. |
revision required | integer | Its saved revision. |
#Result from remove_scene_embedding
Returns the scene's new status. The scene and keyword search are untouched. Needs scene:write.
#Call and response for remove_scene_embedding
{
"tool": "remove_scene_embedding",
"arguments": {
"id": "amber-lamp",
"revision": 3
}
}{
"id": "amber-lamp",
"revision": 3,
"status": "unindexed"
}#refresh_scene_search
Rebuild one page of the keyword index for scenes saved before keyword search existed.
#Arguments for refresh_scene_search
| Argument | Type | What it does |
|---|---|---|
cursor required | string | null | The cursor from the previous page, or null to start. |
#Result from refresh_scene_search
Returns refreshed (the scenes handled on this page, at most 50), cursor and isDone. Continue until isDone is true. Nothing is sent to Voyage. Needs scene:write.
#Call and response for refresh_scene_search
{
"tool": "refresh_scene_search",
"arguments": {
"cursor": null
}
}{
"refreshed": 50,
"cursor": "eyJvZmZzZXQiOjUwfQ",
"isDone": false
}#create_project
Create an empty scene with a stable ID of your choosing.
#Arguments for create_project
| Argument | Type | What it does |
|---|---|---|
id required | string | A stable ID of your choosing. Letters, digits, `_`, `.`, `:` and `-`, up to 128 characters. |
name required | string | The scene's name, 1 to 80 characters. |
width required | integer | Frame width in pixels, 128 to 4096. |
height required | integer | Frame height in pixels, 128 to 4096. |
duration required | number | Length in seconds, 0.1 to 3600. |
#Result from create_project
Returns the new project with its id, revision and name, and its resource. Retrying the same ID cannot create a second scene. Needs scene:write.
#Call and response for create_project
{
"tool": "create_project",
"arguments": {
"id": "desk-lamp",
"name": "Desk lamp",
"width": 1080,
"height": 1080,
"duration": 4
}
}{
"project": {
"id": "desk-lamp",
"revision": 1,
"name": "Desk lamp"
},
"resource": "puni://project/desk-lamp/1"
}#inspect_scene
Read a page of a saved revision: its frame and entries from its objects, modules, motion, cameras and audio. Selection and time are echoed as context; they do not filter, expand or evaluate the returned entries.
#Arguments for inspect_scene
| Argument | Type | What it does |
|---|---|---|
source required | object | The saved revision to read. |
source.kind required | string | project, the only kind. |
source.id required | string | The scene's ID. |
source.revision required | integer | The saved revision you inspected. |
selection required | array | Object IDs to echo as context, up to 64. Does not filter or expand the returned objects; use inspect_objects for their details. |
selection[] | string | One object ID. |
time required | number | The time to echo as context, in seconds, inside the duration. Does not evaluate the scene. |
offset required | integer | Skip this many items across one sequence: objects, modules, actions, tracks, clips, cameras, shots, audio. Use the returned next as the next offset. Default 0; maximum 100000. |
limit required | integer | Maximum items across all collections in one page, 1 to 64. Default 24. The byte budget may return fewer. |
- source names a saved revision; a stale one is refused.
#Result from inspect_scene
Returns identity, the full-document resource, and scene with frame metadata and one page across these collections in order: objects, modules, actions, tracks, clips, cameras, shots, audio. offset counts across that combined sequence. limit is the total page size, 1–64, default 24.
Use next as the next call's offset, with the same source revision, until it is null. total counts entries across all collections; count counts this page. Empty camera or action arrays on an early page do not establish that none exist.
The serialized result is capped at 64,000 UTF-8 bytes, so a page can contain fewer entries than requested. An oversized item has truncated: true, a JSON pointer and a document resource; fetch that resource or its chunks for the complete value. Inspection never moves the editor's selection or playhead.
#Call and response for inspect_scene
{
"tool": "inspect_scene",
"arguments": {
"source": {
"kind": "project",
"id": "quiet-guest",
"revision": 8
},
"selection": [
"body"
],
"time": 1,
"offset": 0,
"limit": 1
}
}{
"identity": {
"projectId": "quiet-guest",
"revision": 8
},
"resource": "puni://project/quiet-guest/8",
"scene": {
"name": "Quiet guest",
"frame": {
"width": 1080,
"height": 1080,
"fps": 24,
"duration": 4
},
"objectCount": 5,
"objects": [
{
"id": "body",
"name": "Body",
"moduleId": "soft-body",
"parentId": null
}
],
"modules": [],
"actions": [],
"tracks": [],
"clips": [],
"cameras": [],
"shots": [],
"audio": [],
"selection": [
"body"
],
"time": 1
},
"total": 6,
"offset": 0,
"limit": 1,
"count": 1,
"next": 1,
"nextOffset": 1
}#inspect_objects
Read objects by name, by ID, or all of them, 64 at a time.
#Arguments for inspect_objects
| Argument | Type | What it does |
|---|---|---|
source required | object | The saved revision to read. |
source.kind required | string | project, the only kind. |
source.id required | string | The scene's ID. |
source.revision required | integer | The saved revision you inspected. |
query required | string | Match objects whose ID, name or module contains this text. Empty matches all. |
ids required | array | Exact object IDs to read, up to 64. Empty means any. |
ids[] | string | One object ID. |
offset required | integer | Skip this many matches, 0 to 4096. |
limit required | integer | How many objects to return, 1 to 64. Default 32. |
- source names a saved revision; a stale one is refused.
#Result from inspect_objects
Returns identity, total, offset, the matching objects with their transforms and property values, limit (1 to 64, default 32), count, and next, the offset for the next page or null (nextOffset is a legacy alias). A result is capped at 64,000 UTF-8 bytes, like inspect_scene.
#Call and response for inspect_objects
{
"tool": "inspect_objects",
"arguments": {
"source": {
"kind": "project",
"id": "quiet-guest",
"revision": 8
},
"query": "eye",
"ids": [],
"offset": 0
}
}{
"identity": {
"projectId": "quiet-guest",
"revision": 8
},
"total": 2,
"offset": 0,
"objects": [
{
"id": "eye-l",
"name": "Eye left",
"moduleId": "mark",
"parentId": "body",
"visible": true,
"locked": false,
"transform": {
"position": [
-12,
58,
30
],
"rotation": [
0,
0,
0
],
"scale": [
1,
1,
1
]
},
"properties": {
"color": "#1d1c19"
}
}
],
"nextOffset": null
}#Tool discovery after a deployment
This endpoint is stateless and cannot push tool-list changes to connected clients. Every read_contract answer lists toolNames and toolsetSha256, a deterministic hash of advertised tool names and input schemas. Initialization instructions carry the same hash. If your client lacks a named tool, reconnect to refresh its list.
A puni_render_view answer carries rendererParity when the worker reports an older renderer identity. Check /api/runtime for the advisory worker status; app and save-service readiness remain independent.
#read_contract
Read the command and type contract, or a ready-made built-in module.
#Arguments for read_contract
| Argument | Type | What it does |
|---|---|---|
topic required | commands | types | shape | curve | profile | text | mark | fitting | cloner | shader | interface | commands or types for the contract text; shape, curve, profile, text, mark, fitting, cloner, shader or interface for a built-in module. |
id | string | null | The ID to give a built-in module; omit it or use null for the default. |
#Result from read_contract
For commands returns commandTypes and the contract text; for types the type text; for shape, curve, profile, text, mark, fitting, cloner, shader or interface a module you can add with add-module. Every successful answer includes contractSha256, which identifies the commands text followed by the types text, not the built-in definition.
#Call and response for read_contract
{
"tool": "read_contract",
"arguments": {
"topic": "commands",
"id": null
}
}{
"commandTypes": [
"add-module",
"update-module",
"add-instance",
"set-instance"
],
"contract": "type CompositionCommand =\n\t| { type: \"add-module\"; module: ModuleDefinition }\n\t| …",
"resource": "puni://contract/commands"
}#inspect_module
Read a page of one module’s top-level fields from a saved revision.
#Arguments for inspect_module
| Argument | Type | What it does |
|---|---|---|
source required | object | The saved revision to read. |
source.kind required | string | project, the only kind. |
source.id required | string | The scene's ID. |
source.revision required | integer | The saved revision you inspected. |
id required | string | The module's ID. |
offset required | integer | Skip this many of the module's top-level fields. Default 0. |
limit required | integer | Maximum top-level fields in one page, 1 to 64. Default 32. Oversized fields appear in omittedFields; read their document resource to recover the full values. |
- source names a saved revision; a stale one is refused.
#Result from inspect_module
Returns identity, resource, moduleId and module containing only the complete fields on this page. Follow next as the next offset until it is null. limit caps fields per page, 1–64, default 32.
The serialized result is capped at 64,000 UTF-8 bytes. Fields too large to inline appear in omittedFields with their key, JSON pointer and document resource. Paging alone does not recover those values: read the resource or its chunks before copying or updating the complete module.
#Call and response for inspect_module
{
"tool": "inspect_module",
"arguments": {
"source": {
"kind": "project",
"id": "quiet-guest",
"revision": 8
},
"id": "soft-body",
"offset": 0,
"limit": 4
}
}{
"identity": {
"projectId": "quiet-guest",
"revision": 8
},
"module": {
"id": "soft-body",
"name": "Soft body",
"version": 3,
"adapter": "shape"
},
"resource": "puni://project/quiet-guest/8",
"moduleId": "soft-body",
"omittedFields": [],
"total": 7,
"offset": 0,
"limit": 4,
"count": 4,
"next": 4,
"nextOffset": 4
}#apply_scene_commands
Land one batch of commands on the saved scene at the revision you read, in one transaction. The scene keeps a checkpoint of what was there before, named by your label, and the owner sees the new revision land.
#Arguments for apply_scene_commands
| Argument | Type | What it does |
|---|---|---|
projectId required | string | The scene. |
revision required | integer | The saved revision you inspected; a moved one is refused. |
commandsJson | string | One command, an array, or a batch, as a JSON string of at most 700 KB. |
batchId | string | A batch sent once with stage_commands, instead of commandsJson. Send exactly one of the two. |
label required | string | What the edit does, 1 to 160 characters; it names the checkpoint the scene keeps of what was there before. |
- One batch of at most 700 KB, landed as one revision; the label names the checkpoint the scene keeps of what was there before.
#Result from apply_scene_commands
Returns the saved project with its new revision and the projectUrl the owner would open. Send the revision you inspected: a moved one is refused with REVISION_CONFLICT, so read the scene again and send the batch from what it says now. A batch that does not apply comes back with changed: false and the validator's reason in error. It also says where. commandIndex is the zero-based index into the array you sent, beside commandType. refusedWhen is applying when that command could not be applied, and complete when every command applied and the finished scene was refused. The batch is validated once it is complete, so the order of its commands never matters. An empty batch writes nothing. Send a batch from stage_commands by its batchId instead of commandsJson, never both; the answer then also carries batchId and batchSha256, the digest of the exact JSON applied. Needs scene:write.
#Call and response for apply_scene_commands
{
"tool": "apply_scene_commands",
"arguments": {
"projectId": "quiet-guest",
"revision": 8,
"commandsJson": "[{\"type\":\"set-instance\",\"id\":\"body\",\"properties\":{\"color\":\"#819ad5\"}}]",
"label": "Blue body"
}
}{
"project": {
"id": "quiet-guest",
"revision": 9,
"name": "Quiet guest"
},
"projectUrl": "https://puni.design/p/quiet-guest"
}#stage_commands
Send a batch once and use it twice. Split the batch's JSON text into chunks, send each with its index and the total, then name the batch by its batchId in puni_render_view to look at it and in apply_scene_commands to land it. A batch can also be larger than one call carries: up to 4 MB, in at most 64 chunks of 700,000 characters.
#Arguments for stage_commands
| Argument | Type | What it does |
|---|---|---|
batchId | string | Leave it out on the first chunk to have one made; send it with every other chunk. 8 to 80 letters, digits, underscores or hyphens. |
index required | integer | This chunk's place in the batch, from 0. |
total required | integer | How many chunks the batch has, 1 to 64; the same on every chunk. |
chunk required | string | This piece of the batch's JSON text, at most 700,000 characters. Chunks join in index order with nothing between them. |
- Needs scene:write. A batch holds at most 4 MB and is kept for 30 minutes after its first chunk, for you alone; nothing in it is validated until it is used.
#Result from stage_commands
Each answer carries the batchId, how many chunks have arrived (received of total), the chunk's chunkSha256 and when the batch expires. The answer that completes the batch adds bytes and sha256, the SHA-256 of the whole joined text, so you can compare it with your own. The chunks join in index order with nothing added between them, and nothing is validated until the batch is used. A batch is kept for 30 minutes after its first chunk, only for you.
Sending a chunk again with the same text is a safe retry. Different text at an index already kept, or a different total, is refused with STAGED_BATCH_CONFLICT. Using a batch that is missing chunks answers STAGED_BATCH_INCOMPLETE with how many arrived; an expired or unknown one answers STAGED_BATCH_EXPIRED or STAGED_BATCH_NOT_FOUND. Needs scene:write.
#Call and response for stage_commands
{
"tool": "stage_commands",
"arguments": {
"index": 0,
"total": 1,
"chunk": "[{\"type\":\"set-instance\",\"id\":\"body\",\"properties\":{\"color\":\"#819ad5\"}}]"
}
}{
"batchId": "batch-5f0c1a8e-2c41-4f7e-9d0b-6a1f3e2d9c77",
"index": 0,
"total": 1,
"received": 1,
"complete": true,
"chunkSha256": "3e0f…",
"expiresAt": "2026-10-02T12:30:00.000Z",
"bytes": 72,
"sha256": "3e0f…"
}#update_scene
Change the stage and the description: the scene's lighting, exposure, shadows, background and transparency, and its description, on the saved scene at the revision you read. Send only the fields you change.
#Arguments for update_scene
| Argument | Type | What it does |
|---|---|---|
projectId required | string | The scene. |
revision required | integer | The saved revision you inspected; a moved one is refused. |
label required | string | What the change does, 1 to 160 characters; it names the checkpoint the scene keeps of what was there before. |
environment | object | The stage fields to change; the rest keep their saved values. |
environment.lighting | soft | gallery | dark | soft, gallery or dark. |
environment.exposure | number | Exposure, 0.1 to 3. |
environment.shadows | boolean | Whether objects cast shadows. |
environment.background | string | A six-digit colour, #rrggbb. |
environment.transparent | boolean | Draw without a background or floor. |
description | string | The scene's description, at most 300 characters. |
- Lands like apply_scene_commands: one revision, one checkpoint named by the label. Fields left out keep their saved values.
#Result from update_scene
Lands like apply_scene_commands: the saved project with its new revision, a checkpoint of what was there before named by your label, and REVISION_CONFLICT if the owner saved meanwhile. A value out of bounds is refused as invalid arguments; a change the document refuses comes back with changed: false and the reason in error; a change to the values already saved writes nothing. To see it first, pass the same environment to puni_render_view. Needs scene:write.
#Call and response for update_scene
{
"tool": "update_scene",
"arguments": {
"projectId": "quiet-guest",
"revision": 9,
"label": "Dark stage",
"environment": { "lighting": "dark", "background": "#101010" },
"description": "A quiet guest on a dark stage."
}
}{
"project": {
"id": "quiet-guest",
"revision": 10,
"name": "Quiet guest"
},
"projectUrl": "https://puni.design/p/quiet-guest"
}#evaluate_frame
Read every animated value at one time, for up to 64 objects.
#Arguments for evaluate_frame
| Argument | Type | What it does |
|---|---|---|
source required | object | The saved revision to read. |
source.kind required | string | project, the only kind. |
source.id required | string | The scene's ID. |
source.revision required | integer | The saved revision you inspected. |
time required | number | The time to evaluate, in seconds, inside the duration. |
ids required | array | Limit the answer to these objects, up to 64. Empty means the first 64. |
ids[] | string | One object ID. |
- source names a saved revision; a stale one is refused.
#Result from evaluate_frame
Returns identity and frame with the evaluated instances, their transforms and property values, and a proof line reminding you it is numbers, not a picture.
#Call and response for evaluate_frame
{
"tool": "evaluate_frame",
"arguments": {
"source": {
"kind": "project",
"id": "quiet-guest",
"revision": 8
},
"time": 1.5,
"ids": [
"body"
]
}
}{
"identity": {
"projectId": "quiet-guest",
"revision": 8
},
"frame": {
"time": 1.5,
"total": 5,
"instances": [
{
"id": "body",
"transform": {
"position": [
0,
43.2,
0
],
"rotation": [
0,
0,
0
],
"scale": [
1.02,
0.98,
1.02
]
},
"visible": true,
"properties": {
"color": "#819ad5"
}
}
],
"proof": "Deterministic scene state; this is not a rendered image."
}
}#simulate_scene
Solve physical motion and land it on the saved scene. The bodies you name as dynamic receive recorded rigid-body motion; a kinematic body with a path carries a striker through them. Collision shapes are proxies you write in scene millimetres, not the visual mesh.
#Arguments for simulate_scene
| Argument | Type | What it does |
|---|---|---|
projectId required | string | The scene. |
revision required | integer | The saved revision you inspected; a moved one is refused. |
label required | string | What the solve does, 1 to 160 characters; it names the checkpoint the scene keeps of what was there before. |
recipe required | object | A PhysicsRecipe as read_contract describes it: duration, gravity in mm/s², the ground, bodies with explicit collision shapes in scene millimetres, and impulses. |
start | number | Seconds into the scene where the solved motion begins. Defaults to 0. |
follow | array | Up to 32 kinematic bodies whose path comes from their own authored motion, which is left untouched. |
follow[] | string | One object ID. |
- Needs scene:write, and a deployment with a solver. Lands like apply_scene_commands: one revision, one checkpoint named by the label.
#Result from simulate_scene
Returns the scene's new project revision and its projectUrl, like apply_scene_commands. The scene keeps a checkpoint of what was there before, named by your label. A refusal you can act on names its own code: a step too long, a path that starts inside its target, a path over motion that already exists, or motion that changed since the solve. Numbers do not prove a picture: render the result with puni_render_view before you call it done. Needs scene:write.
#Call and response for simulate_scene
{
"tool": "simulate_scene",
"arguments": {
"projectId": "ball-drop",
"revision": 3,
"label": "Drop the ball onto the floor",
"recipe": {
"version": 1,
"duration": 2,
"gravity": [0, -9810, 0],
"ground": { "y": 0, "friction": 0.6, "restitution": 0.4 },
"bodies": [
{
"id": "ball",
"mode": "dynamic",
"position": [0, 600, 0],
"rotation": [0, 0, 0, 1],
"shape": { "kind": "sphere", "radius": 40 },
"mass": 0.2,
"friction": 0.5,
"restitution": 0.6,
"linearDamping": 0,
"angularDamping": 0,
"linearVelocity": [0, 0, 0],
"angularVelocity": [0, 0, 0]
}
],
"impulses": []
}
}
}{
"project": {
"id": "ball-drop",
"revision": 4,
"name": "Ball drop"
},
"projectUrl": "https://puni.design/p/ball-drop"
}#puni_render_view
Supply and retain requestId before drawing. To recover its original PNG after a lost response, failed local write, or later project edit, send recoverOnly: true and that ID (with the project id). Recovery ignores the current revision, returns only the owner’s retained receipt, and never reserves or draws another render. Receipts expire after about a day; terminal IDs persist. Missing or expired receipts refuse with RENDER_ALREADY_ENDED, and pending attempts with RENDER_IN_FLIGHT. The installed CLI uses --recover <original-id> for this path.
See a saved revision as the studio draws it, or that revision with a command batch applied and nothing saved: a PNG rendered on Puni's render worker. This is the picture that evaluation is not, and the way to look before you write.
#Arguments for puni_render_view
| Argument | Type | What it does |
|---|---|---|
id required | string | The scene. |
recoverOnly | boolean | With requestId, return only the owner-scoped retained receipt. Independent of the current revision; never reserves or draws again. |
reuseResult | boolean | Reuse an identical still-valid receipt for free and bind requestId to those bytes for recovery. Expired or missing bytes start a fresh attempt; recoverOnly never does. |
requestId | string | The ID for one render attempt, 1 to 100 letters, digits, underscores or hyphens, starting with a letter or digit. Reuse it with identical input after a lost response. Use a new ID after a confirmed failure. |
revision | integer | The saved revision to draw; the current one when left out. |
commandsJson | string | A command batch to apply before drawing, saving nothing: what the edit would look like. |
batchId | string | A batch sent once with stage_commands, to draw instead of commandsJson. Not both. |
environment | object | Stage fields to draw with, as update_scene takes them, saving nothing. |
environment.lighting | soft | gallery | dark | soft, gallery or dark. |
environment.exposure | number | Exposure, 0.1 to 3. |
environment.shadows | boolean | Whether objects cast shadows. |
environment.background | string | A six-digit colour, #rrggbb. |
environment.transparent | boolean | Draw without a background or floor. |
time | number | The time to draw, in seconds, inside the duration. Default 0. |
camera | orbit | shot | perspective | front | top | side | orbit for the fitted view, shot for the authored cameras, or a side to fit from: perspective, front, top, side. |
width | integer | Width in pixels, 64 to 1024. Default 640; height follows the frame. |
- Needs the render scope. The picture comes back in the result as a PNG the client can show; a render too large to return asks for a smaller width.
- With requestId omitted, retained results are reused for identical input. A draw already in progress is refused; a refunded failure can be tried again.
#Result from puni_render_view
Returns the PNG in an MCP image content block. structuredContent carries the scene name, document hash, image checksum and byte count, time, camera, dimensions, render duration, the exact saved revision, and prospective: true when a batch or an environment was drawn over it. A text content block repeats that metadata. A batch that does not apply draws nothing and costs nothing: the answer is drawn: false with the validator's reason in error and the same commandIndex, commandType and refusedWhen that apply_scene_commands returns. Name a batch from stage_commands by batchId instead of commandsJson to draw it, then apply the same batchId without sending the JSON again.
Compatible MCP Apps hosts can display Puni's image viewer. Download support depends on the host. Receiving image content as an agent does not prove the person can see it. imageResource identifies the embedded bytes; it is not a public download URL. Ask for a smaller width if the result is too large. Needs render.
#Call and response for puni_render_view
The response excerpt omits the repeated text block and abbreviates image data and hashes.
{
"tool": "puni_render_view",
"arguments": {
"id": "quiet-guest",
"revision": 8,
"time": 1,
"camera": "shot",
"width": 640
}
}{
"content": [
{ "type": "image", "mimeType": "image/png", "data": "iVBORw0KGgo…" }
],
"structuredContent": {
"name": "Quiet guest",
"documentHash": "5b7d…",
"imageSha256": "a281…",
"imageBytes": 38421,
"imageResource": "puni://render/a281….png",
"time": 1,
"camera": "shot",
"renderMs": 1840,
"width": 640,
"height": 640,
"identity": { "projectId": "quiet-guest", "revision": 8 },
"resource": "puni://project/quiet-guest/8"
}
}#puni_compare_reference
Render a saved revision and measure how close it stands to a reference picture you supply: silhouette, structure, proportion, and colour overall and region by region.
reference accepts inline base64, an owner-scoped /api/assets/<sha256> URI returned by upload_asset, or a public HTTPS image URL. Original bytes must be PNG, JPEG or WebP and at most 4 MiB. URL intake has an eight-second deadline and at most three redirects; every resolved address and redirect must remain public (no private, loopback, link-local or reserved addresses). The connection is pinned to a checked address. Image bytes, never response headers or URL suffixes, determine the format. An unavailable asset on this account is refused without reading another owner's assets.
The measurement frame uses a transparent environment and the subject's alpha mask, not its colour. This removes the floor and its shadow; it does not alter the saved environment. The card includes measurement: { background: "transparent", subjectMask: "alpha", includesFloor: false }. The terminal's puni render compare --reference <file> uses the same cutout pipeline; its reference input remains a local file.
#Arguments for puni_compare_reference
| Argument | Type | What it does |
|---|---|---|
id required | string | The scene. |
requestId | string | The ID for one comparison attempt, 1 to 100 letters, digits, underscores or hyphens, starting with a letter or digit. Reuse it with identical input after a lost response. Use a new ID after a confirmed failure. |
revision | integer | The saved revision to draw. Default the current one. |
time | number | The time to draw, in seconds. Default 0. |
reference required | string | A PNG or JPEG of the thing the scene should look like, base64 encoded, under 4 MB. |
- Needs the render scope. Likeness below the 0.85 floor is a mechanics milestone, not a proof of resemblance. A render or reference with no subject is refused (LIKENESS_RENDER_EMPTY, LIKENESS_REFERENCE_EMPTY), not scored.
- With requestId omitted, retained results are reused for identical input. A draw already in progress is refused; a refunded failure can be tried again.
#Result from puni_compare_reference
Returns the rendered PNG in an MCP image content block. structuredContent carries likeness, structure, the floor of 0.85, whether it passes, a verdict, proportion, aspect, colour and placement scores, the document hash, render duration and exact scene identity. A text block repeats that metadata. A score under the floor means the build is not yet the thing, and so does colour that is wrong on both of its terms, overall or where it sits. A render with no subject is refused with LIKENESS_RENDER_EMPTY and a reference with none with LIKENESS_REFERENCE_EMPTY, before any score. Needs render.
#Call and response for puni_compare_reference
The response excerpt shows the metadata; image content and its repeated text block are omitted. The input image and document hash are abbreviated.
{
"tool": "puni_compare_reference",
"arguments": {
"id": "quiet-guest",
"revision": 8,
"time": 0,
"reference": "/9j/4AAQSkZJRgABAQ…"
}
}{
"structuredContent": {
"likeness": 0.91,
"structure": 0.64,
"floor": 0.85,
"passes": true,
"verdict": "above the floor: eyes rule",
"proportion": 0.93,
"aspect": { "puni": 0.56, "ref": 0.6 },
"colour": { "shared": 0.71, "meanGap": 0.04 },
"placement": { "shared": 0.66, "meanGap": 0.05 },
"documentHash": "5b7d…",
"renderMs": 1920,
"identity": { "projectId": "quiet-guest", "revision": 8 }
}
}#puni_render_film
Start a background, silent H.264 film of a saved revision with render access. The tool returns a job handle without waiting for encoding. Hosted intake must be enabled by the deployment; FILM_SERVICE_UNCONFIGURED means it is unavailable. Where the studio offers Film on the same service, its Export lists the scene's films of the last day, so the owner can see and download a film you made.
#Arguments for puni_render_film
| Argument | Type | What it does |
|---|---|---|
id required | string | The scene to film. |
revision | integer | The saved revision to film. Leave it out for the current one. |
requestId required | string | Names this film. Repeat it to recover the same job after a lost answer; a new film needs a new requestId. |
fps | integer | Frames per second, 1–60. Defaults to 30 or the scene's own rate if lower. |
width | integer | An even width from 64 to 1280 pixels; height follows the scene's frame. Defaults to 640. |
start | number | Seconds into the scene to begin. Defaults to 0. |
duration | number | Seconds to film, at most 30. Defaults to the scene's length up to 8 seconds. |
recoverOnly | boolean | Only read the job named by requestId; never starts or charges a film. |
- Needs the render scope. Starts a background film and returns at once; repeat the same requestId with recoverOnly: true to poll. FILM_SERVICE_UNCONFIGURED means hosted films are not enabled on this deployment.
#Result from puni_render_film
Retain the scene id and your requestId. Poll with those values and recoverOnly: true; polling never starts another job or charge. Repeating a start with the same ID recovers its retained source and settings. Changing explicit settings under that ID is refused.
The handle reports queued, rendering, encoding, then completed, failed or cancelled; an active cancellation can report cancelling. A completed result includes an assetUri and an authenticated download descriptor. Read its metadata and chunks, concatenate them in order, and verify the byte count and SHA-256 before retaining the MP4. Downloads expire after 24 hours.
Films use the saved shot camera, or the orbit camera when no shot is authored. Duration is at most 30 seconds, with at most 1800 frames. Failed or cancelled jobs refund their hosted-render allowance. Recovery never relaunches a failed job; a deliberately new attempt needs a new request ID.
#Call and response for puni_render_film
This example assumes hosted intake is enabled. The response excerpt omits settings and expiry.
{
"tool": "puni_render_film",
"arguments": {
"id": "quiet-guest",
"revision": 8,
"requestId": "quiet-guest-film-1",
"width": 256,
"fps": 2,
"duration": 1
}
}{
"id": "quiet-guest-film-1",
"requestId": "quiet-guest-film-1",
"revision": 8,
"status": "queued",
"progress": 0
}#sculpt_object
Turn one picture or a prompt into a textured mesh, on your own provider keys from Settings → Providers, or on Puni credits where platform generation is enabled.
#Arguments for sculpt_object
| Argument | Type | What it does |
|---|---|---|
requestId | string | A stable ID for this sculpt; reuse it with identical input after a lost response and it returns the same attempt. |
name required | string | Name the sculpt, up to 80 characters. |
image | string | One picture as base64. Choose exactly one of image, imageUrl or prompt. |
imageUrl | string | A public HTTPS picture URL. Choose exactly one input. |
prompt | string | Describe an object in up to 4,000 characters; makes a transparent picture using your OpenAI key before sculpting it. Use this from ChatGPT web. |
lip | keep | strip | strip erases a sticker outline; keep preserves it. Prompts default to keep, pictures to strip. |
faces | integer | Provider mesh target, 40,000 to 500,000 faces. |
generateType | Normal | Geometry | Normal makes a textured mesh; Geometry makes untextured geometry. Defaults to Normal. |
views | object | Separate views of the same object, proportions and pose. Requires a front image or imageUrl, not a prompt. |
views.back | object | The back reference, with exactly one of image or imageUrl. |
views.back.image | string | Base64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply. |
views.back.imageUrl | string | A public HTTPS image URL, fetched with the same network and size guards as the front. |
views.back.mime | image/png | image/jpeg | image/webp | MIME type of this base64 image: image/png, image/jpeg or image/webp. |
views.left | object | The left reference, with exactly one of image or imageUrl. |
views.left.image | string | Base64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply. |
views.left.imageUrl | string | A public HTTPS image URL, fetched with the same network and size guards as the front. |
views.left.mime | image/png | image/jpeg | image/webp | MIME type of this base64 image: image/png, image/jpeg or image/webp. |
views.right | object | The right reference, with exactly one of image or imageUrl. |
views.right.image | string | Base64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply. |
views.right.imageUrl | string | A public HTTPS image URL, fetched with the same network and size guards as the front. |
views.right.mime | image/png | image/jpeg | image/webp | MIME type of this base64 image: image/png, image/jpeg or image/webp. |
views.top | object | The top reference, with exactly one of image or imageUrl. |
views.top.image | string | Base64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply. |
views.top.imageUrl | string | A public HTTPS image URL, fetched with the same network and size guards as the front. |
views.top.mime | image/png | image/jpeg | image/webp | MIME type of this base64 image: image/png, image/jpeg or image/webp. |
views.bottom | object | The bottom reference, with exactly one of image or imageUrl. |
views.bottom.image | string | Base64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply. |
views.bottom.imageUrl | string | A public HTTPS image URL, fetched with the same network and size guards as the front. |
views.bottom.mime | image/png | image/jpeg | image/webp | MIME type of this base64 image: image/png, image/jpeg or image/webp. |
views.leftFront | object | The leftFront reference, with exactly one of image or imageUrl. |
views.leftFront.image | string | Base64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply. |
views.leftFront.imageUrl | string | A public HTTPS image URL, fetched with the same network and size guards as the front. |
views.leftFront.mime | image/png | image/jpeg | image/webp | MIME type of this base64 image: image/png, image/jpeg or image/webp. |
views.rightFront | object | The rightFront reference, with exactly one of image or imageUrl. |
views.rightFront.image | string | Base64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply. |
views.rightFront.imageUrl | string | A public HTTPS image URL, fetched with the same network and size guards as the front. |
views.rightFront.mime | image/png | image/jpeg | image/webp | MIME type of this base64 image: image/png, image/jpeg or image/webp. |
mime | image/png | image/jpeg | image/webp | MIME type of the base64 front image: image/png, image/jpeg or image/webp. |
- Needs sculpt, even with your own provider keys. Choose Read, write and sculpt or Full access when connecting.
#Result from sculpt_object
Returns the new sculpt's id and its status. A prompt makes a picture first with your OpenAI key, then the mesh with your fal key (or both on Puni credits). Poll get_sculpt for progress; the model call is paid with your keys or your credits. Needs sculpt, even with your own provider keys.
#Call and response for sculpt_object
{
"tool": "sculpt_object",
"arguments": {
"name": "Felt red panda",
"prompt": "a small felt red panda plush, sitting"
}
}{
"id": "sculpt-3f1c",
"status": "sculpting",
"name": "Felt red panda"
}#get_sculpt
Read a sculpt's progress. Each read moves it along; when it is ready the result carries the source and the commands that put it in a scene.
#Arguments for get_sculpt
| Argument | Type | What it does |
|---|---|---|
id required | string | The sculpt id returned by sculpt_object. |
#Result from get_sculpt
Returns status, and when ready the stored source (a GLB), the ingest figures, and add: the add-module, add-instance and add-track commands for apply_scene_commands that place it in a scene at its original size.
#Call and response for get_sculpt
{
"tool": "get_sculpt",
"arguments": {
"id": "sculpt-3f1c"
}
}{
"id": "sculpt-3f1c",
"status": "ready",
"source": {
"kind": "glb",
"uri": "/api/assets/9c4e…",
"sha256": "9c4e…"
},
"ingest": {
"triangles": 184320,
"bytes": 5242880
},
"add": [
{
"type": "add-module",
"module": {
"id": "sculpt-3f1c",
"adapter": "source"
}
}
]
}#segment_sculpt
Split a ready sculpt into named parts that keep their textures. About two minutes and one more paid model call; call again to re-roll.
#Arguments for segment_sculpt
| Argument | Type | What it does |
|---|---|---|
id required | string | The id of a ready sculpt, as returned by sculpt_object. |
requestId | string | A stable ID for this split; reuse it after a lost response and it returns the same attempt. |
- Needs sculpt to start a paid split. Poll the existing sculpt with get_sculpt using scene:write.
#Result from segment_sculpt
Returns the sculpt with status back to working. Poll get_sculpt; when ready, addSegmented carries the commands for the segmented source, and the previous source stays available. Needs sculpt, even with your own provider keys.
#Call and response for segment_sculpt
{
"tool": "segment_sculpt",
"arguments": {
"id": "sculpt-3f1c"
}
}{
"id": "sculpt-3f1c",
"status": "segmenting"
}#Resources
The server also lists your scenes as resources. puni://project/{id}/{revision} returns the whole document; add /chunks/{index} for a large one in pieces. puni://contract/commands and puni://contract/types return the contract text.
Read Prompts for the working loop these tools expect.
Was this useful?