Tools Reference¶
Envoy exposes 68 MCP tools for interacting with TouchDesigner, plus 23 bridge meta-tools: 6 TD-lifecycle tools and 17 convoy_* LAN work-relay tools (all listed below). All tools use the standard MCP protocol and can be called by any compatible client.
Two of the 62 (convoy_lifecycle_state, convoy_lifecycle_quit) are internal Convoy host-lifecycle tools: they refuse any session other than the Convoy host app's dedicated loopback session and are not for agent use.
Every mutating TD-authoring tool call is wrapped in a TouchDesigner undo block. Press Ctrl+Z in TD to revert an agent change; a batch_operations call is one undo step for the whole batch.
Responses are compact by default; opt-in flags such as include_defaults and details return full detail when needed.
Operator Management¶
| Tool | Parameters | Description |
|---|---|---|
create_op |
parent_path, op_type, name? |
Create a new operator (e.g., baseCOMP, noiseTOP, textDAT, gridPOP) |
create_extension |
parent_path, class_name, name?, code?, promote?, ext_name?, ext_index?, existing_comp? |
Create a TD extension: baseCOMP + text DAT + extension wiring, initialized and ready to use |
delete_op |
op_path, override? |
Delete an operator. Also purges its externalization tracking (any strategy) and the externalized file — unless the file is clone-owned or still referenced by another operator. Refused while another live session claims the scope or wrote it in the last minute; override=True bypasses that gate. Refuses the Embody COMP, its ancestors, / and Envoy's own extension DAT (envoy.embody.host_destroy_refused); override=True does not bypass that check |
copy_op |
source_path, dest_parent, new_name? |
Copy operator to new location |
rename_op |
op_path, new_name |
Rename an operator |
get_op |
op_path, include_defaults? |
Get operator info. Parameters are NON-DEFAULT only by default; pass include_defaults=True for all parameters. Parameter-heavy COMPs are expensive in full detail, so prefer read_tdxn for structure reads. inputs has one entry per input connector, null for an empty one — entry position IS the real connector index |
query_network |
parent_path?, recursive?, op_type?, include_utility? |
List operators in a container. Child rows are compact: path, type, family, depth (name is derivable from the last path segment). Set include_utility=True to include annotations |
find_children |
op_path, name?, type?, depth?, tags?, text?, comment?, include_utility? |
Advanced search using TD's findChildren — filter by name pattern, type, depth, tags, text content, or comment |
cook_op |
op_path, force?, recurse? |
Force-cook an operator |
Parameter Control¶
| Tool | Parameters | Description |
|---|---|---|
set_parameter |
op_path, par_name, value?, mode?, expr?, bind_expr? |
Set a parameter's value, expression, bind expression, or mode (constant/expression/export/bind). Invalid Menu values are rejected with valid menuNames; sequence-block names auto-grow their sequence (const5name grows numBlocks to 6). A reload or clone pulse (enableexternaltoxpulse, reinitnet, enablecloningpulse) on the Embody COMP, an ancestor, / or Envoy's extension DAT is refused (envoy.embody.host_destroy_refused) |
get_parameter |
op_path, par_name?, search?, search_in?, depth?, max_results?, details? |
Get one parameter compactly, or search parameters by glob/substring across a subtree. Search fields: name, value, expr, or any |
Search mode omits par_name and passes search. It scans the target operator and children to depth (default 2) using fnmatch glob semantics; patterns without *?[ become contains searches. Results are {root, pattern, search_in, count, results, truncated?}, where each hit includes op, par, value, mode, and expr or bindExpr when present.
search_in='value' evaluates every parameter it scans (expressions included), so expression side effects and cooking cost are on the caller; search_in='any' only evaluates constant-mode values and matches expression/bind parameters by text.
Single-parameter mode returns path, parameter, value, mode, label, mode-specific refs (expression, bindExpr, bindMaster, exportOP), and menuNames for Menu parameters. Pass details=True to include defaults, custom/read-only/style metadata, numeric ranges, menuLabels, and menuIndex. Search mode ignores details.
Example -- find expressions under /project1 that reference absolute paths in that subtree:
{"op_path": "/project1", "search": "*/project1/*", "search_in": "expr", "depth": 10, "max_results": 100}
DAT Content¶
| Tool | Parameters | Description |
|---|---|---|
get_dat_content |
op_path, format? |
Get DAT text or table data ("text", "table", "auto"), or "stats" to reduce a table to per-column min/max/mean (numeric) or distinct counts (text) plus head/tail rows -- use it instead of dumping a large table into context |
set_dat_content |
op_path, text?, rows?, clear?, confirm_wipe? |
Full-replace DAT content. Wipe guardrail refuses text="", rows=[], or clear=True with no content unless confirm_wipe=True is passed. For partial edits to text DATs, prefer edit_dat_content -- it sends only the changed substring. |
edit_dat_content |
op_path, old_string, new_string, replace_all?, confirm_wipe? |
Surgical text edit on a DAT (mirrors Claude Code's Edit tool). Replaces old_string with new_string. By default old_string must appear exactly once -- pass replace_all=True to replace every occurrence. Token-efficient: only the changed substring crosses the wire. Text DATs only; use set_dat_content(rows=...) for tables. |
CHOP / POP Data¶
Reduced reads -- never a blind dump. A 4x600 CHOP is 2,400 raw floats; these return the shape and range instead.
| Tool | Parameters | Description |
|---|---|---|
get_chop_data |
op_path, channels?, samples?, compare_to? |
Per-channel min/max/mean/std/first/last, capped at 32 channels. channels is a name glob; samples>0 adds head/tail raw values. compare_to another CHOP adds a diff of per-channel deltas -- the "what did this chain do to my data" read |
get_pop_data |
op_path, attributes?, samples?, max_points? |
Point/prim/vert attribute metadata (name, size, type). Metadata costs ~0.02ms at ANY point count; reading point VALUES is a GPU->CPU readback (~9.5ms at 16k points, ~69ms at 160k -- about 4 frames at 60fps), so samples>0 is opt-in and refused above max_points (default 50,000) |
Why the POP guard is on point count, not sample count: POP.points(attr, startIndex, count) accepts both slicing arguments and ignores them (verified on 2025.33070) -- the readback happens regardless. Asking for 4,096 points from a 160k POP measured slower than reading all of them. Reach for samples only when you actually need values, and prefer a smaller upstream POP for inspection.
The reduce-don't-dump contract for CHOP and DAT reads is adapted from the view tool in Marius Alwan Meyer's code-mode fork of Embody (sporqist/Embody, MIT). The POP reader and its readback ceiling are Embody's own.
Operator Flags¶
| Tool | Parameters | Description |
|---|---|---|
get_op_flags |
op_path |
Get all flags: bypass, lock, display, render, viewer, current, expose, selected, allowCooking |
set_op_flags |
op_path, bypass?, lock?, display?, render?, viewer?, current?, expose?, allowCooking?, selected? |
Set one or more flags on an operator |
Positioning & Layout¶
| Tool | Parameters | Description |
|---|---|---|
get_op_position |
op_path |
Get operator position, size, color, and comment |
get_network_layout |
comp_path, include_annotations? |
Get compact positions of ALL operators (and annotations) in a COMP in one call. Operators include path, type, nodeX, nodeY, nodeWidth, nodeHeight; centers are derivable as nodeX+nodeWidth/2 and nodeY+nodeHeight/2. Annotation text is capped at 160 chars. Returns bounding_box |
set_op_position |
op_path, x?, y?, width?, height?, color?, comment? |
Set operator position, size, color ([r,g,b] floats 0-1), or comment |
layout_children |
op_path |
Auto-layout all children in a COMP |
Annotations¶
| Tool | Parameters | Description |
|---|---|---|
create_annotation |
parent_path, mode?, text?, title?, x?, y?, width?, height?, color?, opacity?, name? |
Create an annotation. Modes: "annotate" (default, has title bar), "comment", "networkbox". Created utility=True (matching TD UI-drawn annotations): visible to get_annotations, hidden from query_network/find_children unless include_utility=True. Every op-path tool still resolves it by path; delete with delete_op (durable), never a raw .destroy() |
get_annotations |
parent_path |
List all annotations in a COMP with their properties and enclosed operators |
set_annotation |
op_path, text?, title?, color?, opacity?, width?, height?, x?, y? |
Modify properties of an existing annotation |
get_enclosed_ops |
op_path |
Get operators enclosed by an annotation, or annotations enclosing an operator |
Connections¶
| Tool | Parameters | Description |
|---|---|---|
connect_ops |
source_path, dest_path, source_index?, dest_index?, comp? |
Wire two operators together. Set comp=True for COMP connectors (top/bottom) |
disconnect_op |
op_path, input_index?, comp? |
Disconnect an operator's input. Set comp=True for COMP connectors (top/bottom) |
get_connections |
op_path |
Get all input/output connections (includes COMP connections for COMPs). Inputs are reported per connector: one entry per input connector carrying its real index, with connected_to: null for an empty connector — nothing is compacted away, so a wire on connector 2 reports as index 2. Dynamic multi-input ops (Switch, Composite) always show one trailing empty connector, the growth slot |
Performance Monitoring¶
| Tool | Parameters | Description |
|---|---|---|
get_op_performance |
op_path, include_children? |
Get CPU/GPU cook times (milliseconds), memory usage (bytes), cook counts |
get_project_performance |
include_hotspots? |
Get project-level FPS, frame time, GPU/CPU memory, dropped frames, active ops, GPU temp. Optional hotspot ranking of top N COMPs by cook time |
Code Execution¶
| Tool | Parameters | Description |
|---|---|---|
execute_python |
code |
Execute Python in TD; set the result variable to return values. Auto-lints newly-created ops and emits a LAYOUT WARNING when they are left at (0,0) or overlapping (unlike create_op, raw comp.create() does not auto-position). Also statically lints the submitted source (as do set_dat_content / edit_dat_content for Python written to DATs) and emits a THREADING WARNING when a thread target calls TD's run() -- worker-side run() silently corrupts TD state and crashes later (Derivative-confirmed 2026-08-17). Refused before anything runs (envoy.embody.host_destroy_refused) when the code would destroy or reload the Embody COMP, an ancestor, / or Envoy's extension DAT in the same call -- me and parent() are the Embody COMP here; see Troubleshooting |
Introspection & Diagnostics¶
| Tool | Parameters | Description |
|---|---|---|
get_td_info |
(none) | Get TD version, build, OS, and Envoy version |
get_op_errors |
op_path, recurse? |
Errors and warnings for an operator and its children. Covers all three surfaces TD reports red: cook errors, Python tracebacks from callbacks/DAT scripts/expressions (OP.scriptErrors, tagged kind: "script" in errors[]), and GLSL compile failures (separate shaderErrors key) |
exec_op_method |
op_path, method, args?, kwargs? |
Call a method on an operator (e.g., appendRow, cook). destroy, reload, changeType and progressiveUnload on the Embody COMP, an ancestor, / or Envoy's extension DAT are refused (envoy.embody.host_destroy_refused) |
get_td_classes |
(none) | List all Python classes/modules in the td module |
get_td_class_details |
class_name |
Get methods, properties, and docs for a TD class |
get_module_help |
module_name |
Get Python help text for a module (supports dotted names like td.tdu) |
get_docs |
query, section?, source?, max_chars? |
Look up official TouchDesigner docs. source is auto (offline then web), offline, or web; normal responses carry title, source, sections_available, content, and optional url/truncated; ambiguous offline lookups return source + matches only |
get_guidance |
topic? |
Serve this project's checked-in TouchDesigner doctrine (.claude/rules/*.md and .claude/skills/*/SKILL.md) over MCP, so agents on a client with no skills folder (VS Code, Copilot, Windsurf) get the same rules Claude Code loads; Codex, Cursor, Gemini and Antigravity read .agents/skills/, OpenCode reads .claude/skills/. Bare call lists topics; topic returns that document. Answered worker-side (no TD round-trip) |
get_focus |
(none) | What the user is looking at: current pane network, selected operator(s), current op, and rollover. When the user says "this operator" they mean the SELECTED/current op -- rollover is incidental mouse position and must not be acted on |
Embody Integration¶
| Tool | Parameters | Description |
|---|---|---|
externalize_op |
op_path, tag_type? |
Tag and externalize operator to disk (auto-detects type if omitted). Never shows the locked-content dialog: a TDXN export with locked TOP/CHOP/SOP/POP operators logs one WARNING (in _logs) naming each operator's source and the COMP to tag tox |
remove_externalization_tag |
op_path, delete_file? |
Remove externalization tracking (tag + row + TDXN breadcrumb); delete_file=True also deletes the file (best-effort). Returns removed_tags, removed_rows, removed_anything, summary -- an operator can have a tracked row but NO tag, so check removed_anything, not removed_tags, to confirm cleanup |
get_externalizations |
(none) | List all externalized operators with status |
save_externalization |
op_path |
Force save an externalized operator to disk. Refuses to overwrite a non-empty file with an operator-empty COMP and returns an error instead — untrack the operator or delete the file if the empty state is intended. Like externalize_op, it logs locked-content findings instead of showing the dialog |
get_externalization_status |
op_path |
Get dirty state, build number, timestamp, file path |
TDXN Format¶
| Tool | Parameters | Description |
|---|---|---|
read_tdxn |
comp_path?, include_dat_content?, max_depth?, embed_all? |
Preferred for reading ≥3 operators. Return the live network as a TDXN dict (in-memory, never written to disk). ~20-90× fewer tokens than a get_op walk thanks to default-omission, type_defaults, and par_templates compaction |
export_network |
root_path?, include_dat_content?, output_file?, max_depth?, embed_all? |
Write a .tdxn file to disk. Same payload as read_tdxn plus file I/O and stale-file cleanup. Set embed_all=True to recurse into TDXN-tagged COMPs instead of skipping their children (self-contained export) |
import_network |
target_path, tdn, clear_first?, override? |
Recreate a network from a .tdxn file. Nested externalized-TDXN children are rebuilt from their own .tdxn files in the same import, recursively, so no nested COMP is left an empty shell; the result's restored_tdn_shells lists what was restored. With clear_first=True, gated against live peer sessions like delete_op. clear_first into the Embody COMP, an ancestor or / is refused (envoy.embody.host_destroy_refused) |
diff_tdxn |
target?, max_changed_ops?, max_bytes? |
What is UNSAVED in TDXN networks -- the live in-memory network vs the on-disk .tdxn, the view git cannot give. Omit target for a whole-project summary (every live TDXN COMP, which changed + counts); pass a COMP path OR a .tdxn file path/bare filename for one COMP in full per-field detail (old=disk, new=live). For committed/history diffs use plain git diff. Read-only, non-interactive |
TOP Capture¶
| Tool | Parameters | Description |
|---|---|---|
capture_op |
op_path, format?, quality?, max_resolution?, inline? |
Capture any operator's current output as an image. A TOP is read natively; every other family (CHOP, SOP, POP, DAT, COMP, MAT) renders through a transient OP Viewer TOP -- what the network editor's viewer shows -- created inside the Embody COMP for the call and destroyed after (the capture waits a few frames for it to render, and the returned text says a viewer was used). Same temp-file return and Quality verdict as capture_top; max_resolution is the viewer's render width (16:9) for a non-TOP. |
capture_top |
op_path, format?, quality?, max_resolution?, inline?, sample_grid? |
Capture a TOP's output as an image (TOP only; any other family -> capture_op). Saves to a temp file and returns the path -- Read that path to view it. Inline base64 previews are token-heavy, so they are off by default (inline=False); pass inline=True to also embed a small preview. Small images (<20 KB) include the inline MCP ImageContent preview when requested. Default: JPEG at 80% quality, max 640px long edge. Pass sample_grid>=2 to return a downsampled NxN RGBA grid instead of an image: row 0 is the top of the image, stats are computed over the full-resolution texture, the requested grid clamps to 2..32, the returned grid is further capped to the TOP's width/height and can drop below 2 on tiny textures, and image params are ignored. Channel padding: RG -> b=0/a=1, mono -> replicated/a=1, monoalpha -> replicated + real alpha; channels reports the raw plane count. Every capture also returns a Quality verdict from the raw float pixels (is_black / is_flat / fully_transparent / pass + fail_reasons), surfaced as a Quality: OK\|FAIL line so you can tell an empty/black/transparent render from a real one without reading the image (black and fully-transparent are failures; a uniform fill is advisory, flat_frame). |
Multi-Session Awareness¶
Concurrent AI sessions (multiple Claude Code windows, other MCP clients) working on the same project are tracked, warned about each other, and gated away from destroying each other's work. See Multi-Session Coordination for the full picture.
| Tool | Parameters | Description |
|---|---|---|
get_sessions |
— | List connected AI sessions: label (repo@branch), idle time, recent_scopes it modified, claims it holds, plus you (the caller's own session id). May include worktrees: in-flight durable worktree tasks, visible even after their session ended |
claim_scope |
scope, note?, ttl? |
Cooperative write lease on an op-path prefix, a file: path, or a project: scope. Peers' overlapping claims are refused while yours is live; their destructive operations on it are gated. Auto-renews on your own writes; expires on TTL or session silence |
release_scope |
scope |
Release a lease you hold. Polite — expiry also handles it |
announce_task |
title, scopes?, note? |
Announce a unit of work to the shared task ledger so parallel sessions see what is in progress and what is finished-but-uncommitted; active entries ride on get_sessions |
update_task |
task_id, status?, note?, commit? |
Transition a ledger task (done_uncommitted / committed with sha / abandoned); any session may update any task, non-owner writes record updated_by |
preflight_landing |
worktree_path |
Landing safety check for a git-worktree diff: intersects the files it would land with main-tree dirt, peer file: claims/touches, and unsaved live TDXN state. Run before porting any worktree diff; a conflicts verdict means reconcile first |
Auto-piggybacked peer advisories
A _peers field rides on any response whose request touches territory another session modified in the last ~10 minutes — one entry per peer: {label, scope, tool, age_s, conflict}. conflict: true means a peer wrote an overlapping scope within the last minute and your operation is also a write — stop and coordinate.
Destructive-operation gate
delete_op, import_network with clear_first=True, run_tests, and batches containing them are refused with a MULTI-SESSION GATE error (naming the holder or recent writer) while a live peer session claims the scope or wrote it within the last minute. Pass override=True only when you are certain.
Logging¶
| Tool | Parameters | Description |
|---|---|---|
get_logs |
level?, count?, since_id?, source? |
Get recent log entries from ring buffer. Filter by level, source, or use since_id for incremental polling |
run_tests |
suite_name?, test_name?, override?, background?, idempotency_key? |
Run test suites. background=True (recommended for full runs) returns a job id immediately and parks results in .embody/jobs/ -- poll get_job_status; the synchronous mode is severed by the watchdog suites' server restart. idempotency_key is background only -- a retry with the same key reconciles to the original run's job handle instead of starting a second run (passing it without background=True is refused). Gated while a peer session holds project:tests |
Auto-piggybacked logs
When a tool call generates WARNING or ERROR entries since the previous call, the response carries a _logs field with up to the last 8 of them. INFO/DEBUG/SUCCESS history does not ride along — fetch it on demand with get_logs. Warning cursors are tracked per session, so concurrent AI sessions each receive their own copy — one session polling first no longer consumes a warning meant for everyone.
Unattended sessions
Nobody answers a dialog in an unattended session, so preset the Embody parameter that decides it: Tdxnpalettehandling (palette Black Box vs Full Export), Filecleanup (deleted-file prompt), Tdxnlockedwarn (quiet) and Tdxndatsafety (the save-time content report). Saves never prompt. Save through save_project and read the save's warnings in get_job_status(job_id)["warnings"]: the save reinitializes extensions, so they may never ride _logs.
Auto-attached recovery hints
When a tool returns an error, Envoy attaches a recovery_hints list — each entry {code, cause, action, next_tools}, matched to the real error string (path-not-found -> query_network/find_children, parameter-not-found -> get_op, wrong family, empty capture -> get_op_performance, thread conflict, timeout -> get_project_performance). Additive, never clobbers, never raises — follow the hint instead of retrying the same failing call.
Stable error codes
Every error envelope also carries error_code, a machine id of the form envoy.<area>.<condition>: envoy.op.not_found, envoy.par.not_found, envoy.parent.not_comp, envoy.op.wrong_family, envoy.top.empty, envoy.capture.failed, envoy.thread.violation, envoy.op.unknown_type, envoy.timeout, envoy.session.gated, envoy.dat.wipe_refused, envoy.project.unsaved, envoy.embody.unavailable, envoy.docs.lookup_failed, envoy.job.error, envoy.embody.host_destroy_refused (a destroy or reload of Envoy's own host, refused -- see Troubleshooting) — and envoy.error when no rule matches. Branch on the code, not the message: messages may be reworded, codes may not.
Write effects and shader lint
Every write tool's response may carry _effects: errors and warnings that appeared after your write, a meaningful fps drop, and — for DAT writes — the compile state of every GLSL operator that consumes that DAT, whether it is the DAT's dock host or references it by parameter from anywhere in the project. shaders_checked lists what was linted (a quiet footer means compiled clean, not unchecked), new_shader_errors the failures your write introduced, and shader_errors_persist a shader still failing after a later write, so silence never reads as fixed.
Background Jobs¶
Long operations that outlive the 30-second operation timeout run as disk-backed jobs: the starting tool returns a job_... handle immediately, results park in .embody/jobs/ (surviving server restarts and extension reinits), and get_job_status polls.
| Tool | Parameters | Description |
|---|---|---|
get_job_status |
job_id? |
One job record (status running/done/error, result when done, stale when a running record stopped updating), or the 16 newest records without job_id. A finished run_tests job carries the summary with failures listed first; a finished save_project job carries version_before/version_after and warnings (the WARNING/ERROR lines logged during the save: errors first, then oldest first, repeats collapsed, at most 8 plus a (+N more) entry; INFO lines stay in get_logs) |
save_project |
idempotency_key? |
Save the project as a tracked job. Refused while a test run is active (a mid-run save bakes test-forced parameters into the export); idempotent -- a second call while a save is in flight returns the existing handle, and the same idempotency_key extends that dedupe to a retry of any age, reconciling it to the original save instead of queuing a second one. The next call after a save may fail once while the bridge reconnects. The finished record lists the save's warnings (warnings) |
update_embody |
idempotency_key? |
Self-update Embody to the latest GitHub release as a tracked job -- bounded and non-interactive (sha256-pinned manifest, downgrade-refusing, TD-build-floor-gated), so it never needs the TD Python grant. Refused in Perform Mode and while a test run is active. A finished record carries version_before/version_after; an up-to-date node finishes done with them equal. The install restarts the MCP server -- expect one reconnect blip |
Bridge Meta-Tools¶
These tools run locally on the STDIO bridge script, not inside TouchDesigner. They work even when TD is not running, or is frozen behind a modal dialog — this is how Claude Code can launch or restart TD without an active Envoy connection.
| Tool | Parameters | Description |
|---|---|---|
get_td_status |
(none) | Check if TD is running, Envoy reachable, crash detection, process liveness, restart attempts remaining. envoy_unresponsive / unresponsive_since (process alive, port accepting, Envoy silent 60s or more) and main_thread_stalled / stalled_since (Envoy answers, but TD's main thread has not run its request loop for 60s or more) flag a frozen TouchDesigner |
launch_td |
timeout?, project_path? |
Launch TD with the project's .toe file. Waits for Envoy to become reachable (default: 120s). Pass project_path (absolute, or relative to the git root) to open a different .toe |
restart_td |
timeout?, project_path? |
Gracefully quit TD and relaunch. Waits for exit before relaunching (default: 120s). Pass project_path to relaunch with a different .toe. Targets only the active instance's verified process — other running TouchDesigner instances are never touched |
list_dialogs |
instance?, screenshot? |
List the modal dialogs a TD instance shows (message boxes, missing-file and save-changes prompts, the license box, file pickers). Runs on the bridge, so it answers while TD's main thread is blocked. TouchDesigner draws its own dialogs, so their text is not readable through the OS: screenshot=true saves a PNG of each to the temp dir to Read. blocked=true when a dialog is up. macOS backend is unverified. |
dismiss_dialog |
instance?, dialog?, action? |
Dismiss a blocking dialog and verify it is gone. action=auto (default) runs close (WM_CLOSE) -> escape -> enter until the window disappears; a single rung tries only that. dialog picks by title substring or window id (default: the first). The main TouchDesigner window is never a target. Failure is envoy.dialog.stuck with what was tried. |
switch_instance |
instance?, all_sessions? |
List all registered TD instances (omit instance) or re-pin this session's bridge to a different running instance; other sessions are untouched unless all_sessions=true. For a single call, pass instance=<name> on any Envoy tool instead: the bridge routes that one call to the named instance and leaves this session's pin alone (an unknown or unreachable name fails only that call, with error_code envoy.instance.unknown / envoy.instance.unreachable). See Multiple Instances |
Convoy Tools (LAN work relay)¶
The remaining 16 meta-tools drive Convoy, relaying work to Convoy-enabled Embody nodes on the trusted LAN through the local per-user host app. Status and inventory calls never wake TouchDesigner.
| Tool | Description |
|---|---|
get_convoy_status |
Is the local Convoy host app available, and what does it know. host_status carries the host's advisories (each {kind, text}: realm_conflict, identity_reminted, handshake_refusals, peer_identity_changed, lan_bind), its lan posture (bound address, adapter, network category, warning or refusal reason), inbound_refusals (refused LAN handshakes in the last 10 minutes and which admitted peers sent them), identity_reminted and peers_mismatched |
convoy_list_nodes |
Local and reachable remote nodes, with embody_version and host_app_version per node, offline_reason on an offline row (no_relay_port: the node heartbeats but its Envoy serves no relay port; heartbeat_stale; no_runtime: its TouchDesigner unregistered on exit), compatibility with compatibility_reason (protocol, shared-operation counts, the peer's host-app version), and a capabilities field (td_python, full_shell) on local nodes -- remote nodes carry none by design (grants are never advertised across the LAN; absent = unknown) |
convoy_list_controllers |
Live client sessions, selected targets, leases, and active work |
convoy_ping |
One node's liveness through its host app, without waking TouchDesigner |
convoy_select_node |
Pin this session to one exact node so ordinary Envoy tools run there (clear=true to unpin) |
convoy_call |
One-off registered operation on an explicit target, without changing the selection |
convoy_batch |
The same ordered batch on one or more explicit targets, reported per target |
convoy_get_job |
Check durable work that outlives the original call or reconnect |
convoy_ack_job |
Acknowledge a finished delivery so the target can release its protected result artifacts |
convoy_cancel_job |
Request cancellation from the exact owning host |
convoy_forget_node |
Delete a stale node row on THIS machine's host app (refuses only while a delivery has not FINISHED, naming the blocking delivery ids; a finished result never holds a row); dead and long-unseen rows are also evicted automatically |
convoy_get_artifact |
Retrieve and verify a large result into a temporary local file by artifact reference |
convoy_save_artifact |
Verify an artifact and save it into the current project (overwrite=true required to replace) |
convoy_start_node |
Reopen a previously registered, currently offline node |
convoy_restart_node |
Safely replace one exact running TouchDesigner process (requires the current runtime id and an idempotency key) |
convoy_update_embody |
Self-update Embody on Convoy nodes to the latest release -- one node (node=<id\|name\|hostname>) or the whole fleet (all=true) -- by dispatching the bounded update_embody operation as a durable per-node job (no TD Python grant involved). Skips offline, disabled, and Perform Mode nodes by name; poll the returned delivery handles with convoy_get_job. See Fleet Updates |
convoy_owlette |
Optional read-mostly bridge to an Owlette site; fails closed without credentials |
Bridge architecture
Claude Code connects to Envoy via a STDIO bridge script (.embody/envoy-bridge.py). The bridge translates between Claude Code's STDIO transport and Envoy's HTTP endpoint. It handles MCP protocol handshake locally when TD is down, so these meta-tools are always available. See Architecture for details.
Batch Operations¶
| Tool | Parameters | Description |
|---|---|---|
batch_operations |
operations |
Execute multiple operations in a single request. Reduces latency and token overhead |
operations is a list of {"tool": str, "params": dict} objects. Each entry maps to an existing tool name and its parameters. Stops on first error.
When to use: 3+ calls to the same tool type (positioning, connecting, parameter setting, flags). Use execute_python instead when you need conditionals, loops, or computed values between operations.
Example — position 4 operators + connect them in one call:
{"operations": [
{"tool": "set_op_position", "params": {"op_path": "/project1/noise1", "x": 400, "y": 0}},
{"tool": "set_op_position", "params": {"op_path": "/project1/comp1", "x": 800, "y": 0}},
{"tool": "set_op_position", "params": {"op_path": "/project1/level1", "x": 1200, "y": 0}},
{"tool": "set_op_position", "params": {"op_path": "/project1/null1", "x": 1600, "y": 0}},
{"tool": "connect_ops", "params": {"source_path": "/project1/noise1", "dest_path": "/project1/comp1"}},
{"tool": "connect_ops", "params": {"source_path": "/project1/comp1", "dest_path": "/project1/level1"}},
{"tool": "connect_ops", "params": {"source_path": "/project1/level1", "dest_path": "/project1/null1"}}
]}
MCP Prompts¶
| Prompt | Parameters | Description |
|---|---|---|
search_op |
op_name, op_type? |
Guide for searching operators by name |
check_op_errors |
op_path |
Guide for inspecting and resolving operator errors |
connect_ops |
(none) | Guide for wiring operators together |
create_extension_guide |
(none) | Guide for creating TD extensions with proper patterns |