Skip to content
PuniDocs
Open Puni

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

ArgumentTypeWhat it does
projectIdstringA scene of yours to file the asset under. Leave it out to keep the asset in your library only.
name requiredstringThe file name the asset is kept under, with its extension.
mime requiredfont/ttf | font/otf | font/woff | font/woff2 | image/svg+xml | image/png | image/jpegfont/ttf, font/otf, font/woff, font/woff2, image/svg+xml, image/png or image/jpeg; it must match the bytes.
base64 requiredstringThe original file bytes, base64-encoded.
sha256 requiredstringThe SHA-256 of the original bytes as 64 lowercase hex characters; a mismatch is refused.
provenanceobjectWhere the asset comes from. Recorded with it; it never grants ownership.
provenance.authorstringWho made it.
provenance.sourceUrlstringWhere it was obtained.
provenance.licensestringThe 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.

json
{
  "tool": "upload_asset",
  "arguments": {
    "name": "circle.svg",
    "mime": "image/svg+xml",
    "base64": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCI+PGNpcmNsZSBjeD0iMzIiIGN5PSIzMiIgcj0iMjQiLz48L3N2Zz4=",
    "sha256": "223e5f6e5220ada29cd66523230e586a7fe3342b039e6580245a228377f96c6a"
  }
}

#Use an uploaded font

  1. Read the built-in text module with read_contract.
  2. Add {uri, sha256, name, mime} under a key in the module's assets. Preserve the supplied provenance on the module.
  3. Add or update the module, then set the text instance's fontAsset to the returned URI.
  4. 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

ArgumentTypeWhat it does
cursor requiredstring | nullThe cursor from the previous page, or null for the first.
limit requiredintegerHow many scenes to return, 1 to 50.
archivedbooleantrue lists the archived scenes instead of the active ones. Default false.
fieldscompact | fullcompact 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

json
{
  "tool": "list_projects",
  "arguments": {
    "cursor": null,
    "limit": 20
  }
}

#search_projects

Search your scenes and reusable objects by their authored names and descriptions.

#Arguments for search_projects

ArgumentTypeWhat it does
query requiredstringWhat to look for, 1 to 256 characters.
mode requiredkeyword | semantickeyword stays in Puni; semantic sends the query to Voyage.
limit requiredintegerHow many results, 1 to 25.
reusableOnly requiredbooleanOnly 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

json
{
  "tool": "search_projects",
  "arguments": {
    "query": "amber lamp",
    "mode": "keyword",
    "limit": 10,
    "reusableOnly": false
  }
}

#preview_scene_index

Read the exact text that indexing would send to Voyage for one saved revision.

#Arguments for preview_scene_index

ArgumentTypeWhat it does
id requiredstringThe scene.
revision requiredintegerIts 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

json
{
  "tool": "preview_scene_index",
  "arguments": {
    "id": "amber-lamp",
    "revision": 3
  }
}

#index_scene_description

Send the previewed description to Voyage so semantic search can find this scene.

#Arguments for index_scene_description

ArgumentTypeWhat it does
id requiredstringThe scene.
revision requiredintegerThe saved revision you previewed.
contentHash requiredstringThe 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

json
{
  "tool": "index_scene_description",
  "arguments": {
    "id": "amber-lamp",
    "revision": 3,
    "contentHash": "3f1c7a0e9b2d4c6f8a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d9e1f2a4b6c8d0e2f4"
  }
}

#remove_scene_embedding

Remove a scene's semantic-search embedding and stop pending indexing from publishing it.

#Arguments for remove_scene_embedding

ArgumentTypeWhat it does
id requiredstringThe scene.
revision requiredintegerIts 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

json
{
  "tool": "remove_scene_embedding",
  "arguments": {
    "id": "amber-lamp",
    "revision": 3
  }
}

Rebuild one page of the keyword index for scenes saved before keyword search existed.

ArgumentTypeWhat it does
cursor requiredstring | nullThe cursor from the previous page, or null to start.

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.

json
{
  "tool": "refresh_scene_search",
  "arguments": {
    "cursor": null
  }
}

#create_project

Create an empty scene with a stable ID of your choosing.

#Arguments for create_project

ArgumentTypeWhat it does
id requiredstringA stable ID of your choosing. Letters, digits, `_`, `.`, `:` and `-`, up to 128 characters.
name requiredstringThe scene's name, 1 to 80 characters.
width requiredintegerFrame width in pixels, 128 to 4096.
height requiredintegerFrame height in pixels, 128 to 4096.
duration requirednumberLength 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

json
{
  "tool": "create_project",
  "arguments": {
    "id": "desk-lamp",
    "name": "Desk lamp",
    "width": 1080,
    "height": 1080,
    "duration": 4
  }
}

#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

ArgumentTypeWhat it does
source requiredobjectThe saved revision to read.
source.kind requiredstringproject, the only kind.
source.id requiredstringThe scene's ID.
source.revision requiredintegerThe saved revision you inspected.
selection requiredarrayObject IDs to echo as context, up to 64. Does not filter or expand the returned objects; use inspect_objects for their details.
selection[]stringOne object ID.
time requirednumberThe time to echo as context, in seconds, inside the duration. Does not evaluate the scene.
offset requiredintegerSkip 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 requiredintegerMaximum 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

json
{
  "tool": "inspect_scene",
  "arguments": {
    "source": {
      "kind": "project",
      "id": "quiet-guest",
      "revision": 8
    },
    "selection": [
      "body"
    ],
    "time": 1,
    "offset": 0,
    "limit": 1
  }
}

#inspect_objects

Read objects by name, by ID, or all of them, 64 at a time.

#Arguments for inspect_objects

ArgumentTypeWhat it does
source requiredobjectThe saved revision to read.
source.kind requiredstringproject, the only kind.
source.id requiredstringThe scene's ID.
source.revision requiredintegerThe saved revision you inspected.
query requiredstringMatch objects whose ID, name or module contains this text. Empty matches all.
ids requiredarrayExact object IDs to read, up to 64. Empty means any.
ids[]stringOne object ID.
offset requiredintegerSkip this many matches, 0 to 4096.
limit requiredintegerHow 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

json
{
  "tool": "inspect_objects",
  "arguments": {
    "source": {
      "kind": "project",
      "id": "quiet-guest",
      "revision": 8
    },
    "query": "eye",
    "ids": [],
    "offset": 0
  }
}

#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

ArgumentTypeWhat it does
topic requiredcommands | types | shape | curve | profile | text | mark | fitting | cloner | shader | interfacecommands or types for the contract text; shape, curve, profile, text, mark, fitting, cloner, shader or interface for a built-in module.
idstring | nullThe 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

json
{
  "tool": "read_contract",
  "arguments": {
    "topic": "commands",
    "id": null
  }
}

#inspect_module

Read a page of one module’s top-level fields from a saved revision.

#Arguments for inspect_module

ArgumentTypeWhat it does
source requiredobjectThe saved revision to read.
source.kind requiredstringproject, the only kind.
source.id requiredstringThe scene's ID.
source.revision requiredintegerThe saved revision you inspected.
id requiredstringThe module's ID.
offset requiredintegerSkip this many of the module's top-level fields. Default 0.
limit requiredintegerMaximum 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

json
{
  "tool": "inspect_module",
  "arguments": {
    "source": {
      "kind": "project",
      "id": "quiet-guest",
      "revision": 8
    },
    "id": "soft-body",
    "offset": 0,
    "limit": 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

ArgumentTypeWhat it does
projectId requiredstringThe scene.
revision requiredintegerThe saved revision you inspected; a moved one is refused.
commandsJsonstringOne command, an array, or a batch, as a JSON string of at most 700 KB.
batchIdstringA batch sent once with stage_commands, instead of commandsJson. Send exactly one of the two.
label requiredstringWhat 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

json
{
  "tool": "apply_scene_commands",
  "arguments": {
    "projectId": "quiet-guest",
    "revision": 8,
    "commandsJson": "[{\"type\":\"set-instance\",\"id\":\"body\",\"properties\":{\"color\":\"#819ad5\"}}]",
    "label": "Blue body"
  }
}

#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

ArgumentTypeWhat it does
batchIdstringLeave 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 requiredintegerThis chunk's place in the batch, from 0.
total requiredintegerHow many chunks the batch has, 1 to 64; the same on every chunk.
chunk requiredstringThis 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

json
{
  "tool": "stage_commands",
  "arguments": {
    "index": 0,
    "total": 1,
    "chunk": "[{\"type\":\"set-instance\",\"id\":\"body\",\"properties\":{\"color\":\"#819ad5\"}}]"
  }
}

#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

ArgumentTypeWhat it does
projectId requiredstringThe scene.
revision requiredintegerThe saved revision you inspected; a moved one is refused.
label requiredstringWhat the change does, 1 to 160 characters; it names the checkpoint the scene keeps of what was there before.
environmentobjectThe stage fields to change; the rest keep their saved values.
environment.lightingsoft | gallery | darksoft, gallery or dark.
environment.exposurenumberExposure, 0.1 to 3.
environment.shadowsbooleanWhether objects cast shadows.
environment.backgroundstringA six-digit colour, #rrggbb.
environment.transparentbooleanDraw without a background or floor.
descriptionstringThe 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

json
{
  "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."
  }
}

#evaluate_frame

Read every animated value at one time, for up to 64 objects.

#Arguments for evaluate_frame

ArgumentTypeWhat it does
source requiredobjectThe saved revision to read.
source.kind requiredstringproject, the only kind.
source.id requiredstringThe scene's ID.
source.revision requiredintegerThe saved revision you inspected.
time requirednumberThe time to evaluate, in seconds, inside the duration.
ids requiredarrayLimit the answer to these objects, up to 64. Empty means the first 64.
ids[]stringOne 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

json
{
  "tool": "evaluate_frame",
  "arguments": {
    "source": {
      "kind": "project",
      "id": "quiet-guest",
      "revision": 8
    },
    "time": 1.5,
    "ids": [
      "body"
    ]
  }
}

#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

ArgumentTypeWhat it does
projectId requiredstringThe scene.
revision requiredintegerThe saved revision you inspected; a moved one is refused.
label requiredstringWhat the solve does, 1 to 160 characters; it names the checkpoint the scene keeps of what was there before.
recipe requiredobjectA PhysicsRecipe as read_contract describes it: duration, gravity in mm/s², the ground, bodies with explicit collision shapes in scene millimetres, and impulses.
startnumberSeconds into the scene where the solved motion begins. Defaults to 0.
followarrayUp to 32 kinematic bodies whose path comes from their own authored motion, which is left untouched.
follow[]stringOne 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

json
{
  "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": []
    }
  }
}

#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

ArgumentTypeWhat it does
id requiredstringThe scene.
recoverOnlybooleanWith requestId, return only the owner-scoped retained receipt. Independent of the current revision; never reserves or draws again.
reuseResultbooleanReuse 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.
requestIdstringThe 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.
revisionintegerThe saved revision to draw; the current one when left out.
commandsJsonstringA command batch to apply before drawing, saving nothing: what the edit would look like.
batchIdstringA batch sent once with stage_commands, to draw instead of commandsJson. Not both.
environmentobjectStage fields to draw with, as update_scene takes them, saving nothing.
environment.lightingsoft | gallery | darksoft, gallery or dark.
environment.exposurenumberExposure, 0.1 to 3.
environment.shadowsbooleanWhether objects cast shadows.
environment.backgroundstringA six-digit colour, #rrggbb.
environment.transparentbooleanDraw without a background or floor.
timenumberThe time to draw, in seconds, inside the duration. Default 0.
cameraorbit | shot | perspective | front | top | sideorbit for the fitted view, shot for the authored cameras, or a side to fit from: perspective, front, top, side.
widthintegerWidth 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.

json
{
  "tool": "puni_render_view",
  "arguments": {
    "id": "quiet-guest",
    "revision": 8,
    "time": 1,
    "camera": "shot",
    "width": 640
  }
}

#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

ArgumentTypeWhat it does
id requiredstringThe scene.
requestIdstringThe 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.
revisionintegerThe saved revision to draw. Default the current one.
timenumberThe time to draw, in seconds. Default 0.
reference requiredstringA 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.

json
{
  "tool": "puni_compare_reference",
  "arguments": {
    "id": "quiet-guest",
    "revision": 8,
    "time": 0,
    "reference": "/9j/4AAQSkZJRgABAQ…"
  }
}

#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

ArgumentTypeWhat it does
id requiredstringThe scene to film.
revisionintegerThe saved revision to film. Leave it out for the current one.
requestId requiredstringNames this film. Repeat it to recover the same job after a lost answer; a new film needs a new requestId.
fpsintegerFrames per second, 1–60. Defaults to 30 or the scene's own rate if lower.
widthintegerAn even width from 64 to 1280 pixels; height follows the scene's frame. Defaults to 640.
startnumberSeconds into the scene to begin. Defaults to 0.
durationnumberSeconds to film, at most 30. Defaults to the scene's length up to 8 seconds.
recoverOnlybooleanOnly 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.

json
{
  "tool": "puni_render_film",
  "arguments": {
    "id": "quiet-guest",
    "revision": 8,
    "requestId": "quiet-guest-film-1",
    "width": 256,
    "fps": 2,
    "duration": 1
  }
}

#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

ArgumentTypeWhat it does
requestIdstringA stable ID for this sculpt; reuse it with identical input after a lost response and it returns the same attempt.
name requiredstringName the sculpt, up to 80 characters.
imagestringOne picture as base64. Choose exactly one of image, imageUrl or prompt.
imageUrlstringA public HTTPS picture URL. Choose exactly one input.
promptstringDescribe an object in up to 4,000 characters; makes a transparent picture using your OpenAI key before sculpting it. Use this from ChatGPT web.
lipkeep | stripstrip erases a sticker outline; keep preserves it. Prompts default to keep, pictures to strip.
facesintegerProvider mesh target, 40,000 to 500,000 faces.
generateTypeNormal | GeometryNormal makes a textured mesh; Geometry makes untextured geometry. Defaults to Normal.
viewsobjectSeparate views of the same object, proportions and pose. Requires a front image or imageUrl, not a prompt.
views.backobjectThe back reference, with exactly one of image or imageUrl.
views.back.imagestringBase64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply.
views.back.imageUrlstringA public HTTPS image URL, fetched with the same network and size guards as the front.
views.back.mimeimage/png | image/jpeg | image/webpMIME type of this base64 image: image/png, image/jpeg or image/webp.
views.leftobjectThe left reference, with exactly one of image or imageUrl.
views.left.imagestringBase64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply.
views.left.imageUrlstringA public HTTPS image URL, fetched with the same network and size guards as the front.
views.left.mimeimage/png | image/jpeg | image/webpMIME type of this base64 image: image/png, image/jpeg or image/webp.
views.rightobjectThe right reference, with exactly one of image or imageUrl.
views.right.imagestringBase64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply.
views.right.imageUrlstringA public HTTPS image URL, fetched with the same network and size guards as the front.
views.right.mimeimage/png | image/jpeg | image/webpMIME type of this base64 image: image/png, image/jpeg or image/webp.
views.topobjectThe top reference, with exactly one of image or imageUrl.
views.top.imagestringBase64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply.
views.top.imageUrlstringA public HTTPS image URL, fetched with the same network and size guards as the front.
views.top.mimeimage/png | image/jpeg | image/webpMIME type of this base64 image: image/png, image/jpeg or image/webp.
views.bottomobjectThe bottom reference, with exactly one of image or imageUrl.
views.bottom.imagestringBase64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply.
views.bottom.imageUrlstringA public HTTPS image URL, fetched with the same network and size guards as the front.
views.bottom.mimeimage/png | image/jpeg | image/webpMIME type of this base64 image: image/png, image/jpeg or image/webp.
views.leftFrontobjectThe leftFront reference, with exactly one of image or imageUrl.
views.leftFront.imagestringBase64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply.
views.leftFront.imageUrlstringA public HTTPS image URL, fetched with the same network and size guards as the front.
views.leftFront.mimeimage/png | image/jpeg | image/webpMIME type of this base64 image: image/png, image/jpeg or image/webp.
views.rightFrontobjectThe rightFront reference, with exactly one of image or imageUrl.
views.rightFront.imagestringBase64 PNG, JPEG or WebP under 12 MB. Request-wide transport limits also apply.
views.rightFront.imageUrlstringA public HTTPS image URL, fetched with the same network and size guards as the front.
views.rightFront.mimeimage/png | image/jpeg | image/webpMIME type of this base64 image: image/png, image/jpeg or image/webp.
mimeimage/png | image/jpeg | image/webpMIME 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

json
{
  "tool": "sculpt_object",
  "arguments": {
    "name": "Felt red panda",
    "prompt": "a small felt red panda plush, sitting"
  }
}

#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

ArgumentTypeWhat it does
id requiredstringThe 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

json
{
  "tool": "get_sculpt",
  "arguments": {
    "id": "sculpt-3f1c"
  }
}

#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

ArgumentTypeWhat it does
id requiredstringThe id of a ready sculpt, as returned by sculpt_object.
requestIdstringA 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

json
{
  "tool": "segment_sculpt",
  "arguments": {
    "id": "sculpt-3f1c"
  }
}

#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?