Skip to content

Bridge & REST API

The bridge is the TouchDesigner side of tdmcp: a small Python package that exposes a REST API behind a Web Server DAT, which the server calls over HTTP. It runs inside the TD process — that's what gives it the power to create, connect, inspect and preview real nodes.

A binary .tox can't be generated from source by an AI agent, so the bridge ships as Python modules plus a callbacks template. The one-line installer assembles it for you; you can then export your own reusable .tox.

Install it once

All three options create one tidy tdmcp_bridge COMP (Web Server DAT + callbacks), are idempotent, and can be undone with from mcp import install; install.uninstall().

There are two reusable TouchDesigner objects in this flow:

ObjectPath / artifactPurpose
Runtime bridge/project1/tdmcp_bridgeThe actual Web Server DAT bridge that exposes /api/info and the REST API.
Palette packagetdmcp/tdmcp_bridge_package.toxA draggable Palette component with Install, Reinstall, Uninstall, and Status controls for the runtime bridge.

For repeat use across projects, stage the modules and export the Palette package:

bash
npx --yes --package=@dpantani/tdmcp tdmcp install-bridge --palette
# or, from a clone after build:
node dist/index.js install-bridge --palette

The CLI prints the exact Textport command with your staged module path. Paste the Palette package command in TouchDesigner, then drag tdmcp → tdmcp_bridge_package from the Palette into a project and click Install. /api/info responds only after that package button has created the runtime bridge. Advanced exports can use --palette-dir <path> and --package-name <name>.

A. One paste — no clone, no Preferences. In the Textport (Dialogs → Textport and DATs):

python
import urllib.request; exec(urllib.request.urlopen("https://github.com/Pantani/tdmcp/raw/v0.13.2/td/bootstrap.py").read().decode())

Downloads the bridge to ~/tdmcp-bridge/modules and starts it on port 9980. (Needs the repo reachable; if it's private, use B or C.)

B. After adding the module path. Add the absolute path of td/modules to Preferences → "Python 64-bit Module Path", then in the Textport:

python
from mcp import install; install.run()

C. From the terminal. npx --yes --package=@dpantani/tdmcp tdmcp install-bridge (or node dist/index.js install-bridge from a clone) copies the bridge to ~/tdmcp-bridge and prints exactly what to paste.

You should see [tdmcp] bridge running on port 9980 (/project1/tdmcp_bridge). Verify from a terminal:

bash
curl http://127.0.0.1:9980/api/info
# {"ok":true,"data":{"python_version":"3.11.x","td_version":"...","bridge_version":"..."}}

Make a reusable .tox

python
from mcp import install
install.export("/path/to/mcp_webserver_base.tox", modules_dir="/abs/path/to/td/modules")

Pass modules_dir so the import path travels inside the .tox; from then on the install is just dragging the component in.

Keep it on across restarts

Save your project as your Default Project, or use the self-installing td/startup.py in an Execute DAT (toggle Start and Create on). install() is idempotent, so it's safe to leave in place permanently. Full manual (Web-Server-DAT-by-hand) steps are in the TouchDesigner bridge folder.

Endpoints

All responses use the envelope { "ok": true, "data": … } or { "ok": false, "error": { "code": …, "message": … } }.

MethodPathPurpose
GET/api/infoTD/Python/bridge versions
POST/api/nodescreate node {parent_path,type,name?,parameters?,placement?,node_x?,node_y?,viewer?}
GET/api/nodes?parent=…list children
GET/api/nodes/searchbounded compact descendant search with depth, name/path/type/family, result, node-scan and time limits
GET/api/nodes/{path}node detail (path is percent-encoded)
PATCH/api/nodes/{path}update {parameters}
DELETE/api/nodes/{path}apply a broker-authorized delete/bypass, explicit bypass, or explicit YOLO delete; otherwise Keep
PATCH/api/nodes/{path}/metadataatomic rename/move/position/color/comment/writable-flag edit with readback and rollback
PATCH/api/nodes/{path}/annotationedit an Annotate COMP's title/body/RGBA/bounds with per-field readback, redacted text receipts and full snapshot rollback
GET/api/nodes/{path}/params/{param}/menubounded menu names, labels and current value for read-only failure recovery
POST/api/nodes/{path}/params/{param}/pulsevalidate Pulse style and invoke .pulse()
GET/api/nodes/{path}/custom_paramsbounded custom-page/parameter definitions
POST/api/nodes/{path}/custom_paramstransactional add/edit/delete/sort/rename lifecycle with exact rollback
POST/api/params/searchbounded point-in-time parameter search with redaction, unreadable counters and honest completion metadata
POST/api/code/searchbounded BM25-style DAT-text and parameter-expression search with redacted excerpts and honest completion metadata
GET/api/editor/contextcompact project, pane, active Network Editor, selection, rollover and viewport context
POST/api/editor/focusschedule one bounded action-aware pane/selection/framing job
GET/api/editor/focus/{operation_id}poll a follow receipt; the client uses a bounded 750 ms window
POST/api/editor/focus/{operation_id}/cancelcancel/supersede pending follow without touching project topology
POST/api/editor/insertcontext-CAS insertion of one allowlisted same-family operator, with deterministic placement and rollback
POST/api/editor/annotation-layout/contextread a bounded annotation/node/docked/edge geometry snapshot and fingerprint
POST/api/editor/annotation-layout/applycompare-and-swap one bounded annotation-aware layout plan with exact readback and rollback
POST/api/editor/reposition/contextread a bounded scalar exact-placement context and fingerprint; excluded from graph undo
POST/api/editor/repositionatomically place exact operator coordinates with idempotency, readback and full rollback evidence
POST/api/editor/workspacesschedule one temporary right-hand TOP Viewer or Panel workspace on the TD main thread
GET/api/editor/workspaces/{workspace_id}read bounded workspace status without touching TouchDesigner UI objects
POST/api/editor/workspaces/{workspace_id}/restoreschedule compare-and-swap restoration and later-frame close verification
POST/api/editor/workspaces/{workspace_id}/cancelcancel before apply or schedule the same verified cleanup after an apply race
POST/api/project/saveSave or Save As; existing targets require a consumed overwrite interaction
POST/api/interactionsenqueue one allowlisted native delete/overwrite or OAuth-client decision and return immediately
GET/api/interactions/statuscontent-free broker readiness: pending count/limit, active flag and native delivery configuration
GET/api/interactions/{request_id}bounded polling status for an opaque decision ID
POST/api/interactions/{request_id}/cancelcancel to the kind-specific safe choice (Keep for destructive/file prompts, Deny for OAuth); duplicate/late transitions remain terminal
POST/api/oauth/consents/{request_id}/consumerecompute the bounded OAuth target fingerprint and consume exactly one Allow/Deny result; authenticated and available with TDMCP_BRIDGE_ALLOW_EXEC=0
POST/api/operations/previewtoken-required, read-only exact-state preview for one bounded structured operation
POST/api/operations/committoken-required one-callback operation commit with verification, rollback and one callback-journal item
POST/api/operations/receiptrecover a terminal receipt and fresh exact journal observation using the capability in the POST body
POST/api/artifacts/tox/exportsstart a verified as_is or build-gated portable .tox export job
GET/api/artifacts/tox/exports/{operation_id}poll a bounded export receipt
GET/api/artifacts/tox/exports/by-key/{idempotency_key}recover a known export after response loss
POST/api/artifacts/tox/exports/{operation_id}/cancelrequest fail-closed export cancellation and cleanup
POST/api/artifacts/tox/roundtripstart a bounded quarantine-only .tox load/contract/cook inspection job; port 9980 and non-quarantine bridges are refused
GET/api/artifacts/tox/roundtrip/{operation_id}poll the bounded PASS/FAIL/UNVERIFIED round-trip receipt
POST/api/artifacts/tox/roundtrip/{operation_id}/cancelcancel a pending round-trip and verify scratch cleanup
POST/api/packages/reconcile/checkdry-run marker ownership and return an expiring stable plan
POST/api/packages/reconcile/applyrevalidate a plan and apply explicit Bypass or broker-authorized/YOLO Delete; Keep is a client-side no-op
POST/api/execrun Python {script,return_output?}
POST/api/nodes/{path}/methodcall {method,args?,kwargs?}
GET/api/nodes/{path}/errorsnode errors
GET/api/preview/{path}TOP as base64 PNG
POST/api/batch{operations:[…]} (create/update/delete/connect). Batch delete defaults to fail-closed Keep (ok:false); use explicit mode:"bypass", explicit confirmation_policy:"yolo", or the standalone broker-backed delete flow.
GET/api/network/{path}/errorsrecursive errors
GET/api/network/{path}/topologynodes + connections
GET/api/network/{path}/performancecook times
POST/api/connectconnect {source_path,target_path,source_output?,target_input?} (index-aware)
POST/api/disconnectdisconnect {to_path,from_path?,to_input?} (by target, optionally by source/input)
GET/api/nodes/{path}/params?modes=trueparameter values + modes (keys?, non_default_only?)
PATCH/api/nodes/{path}/params/{param}/modeset a parameter's mode {mode,expr?,value?} (constant/expression/bind)
GET/api/nodes/{path}/textread a DAT's text
PUT/api/nodes/{path}/textreplace a DAT's text {text}
GET/api/logsrecent bridge/cook errors from the in-bridge Error DAT (severity?, max_lines?, scope?)

The /api/exec and node-method endpoints are disabled bridge-side by default unless TDMCP_BRIDGE_ALLOW_EXEC=1 is set inside TouchDesigner. If TDMCP_BRIDGE_TOKEN is configured, it authenticates requests but does not enable arbitrary exec by itself — see Security. The structured endpoints added in 0.6.0 — /api/connect, /api/disconnect, /api/logs, the ?modes=true parameter reads, …/params/{param}/mode and the DAT …/text reads/writes — are not behind the exec gate, so they keep working while arbitrary exec is closed.

The editor context/follow/insert/annotation-layout/reposition/workspace, metadata/annotation, custom-parameter lifecycle, parameter-menu, Pulse, project-save, interaction, TOX export/round-trip and package reconciliation endpoints are also structured and independent of TDMCP_BRIDGE_ALLOW_EXEC. Every route still passes through the same bearer-token, loopback, Origin and Host checks. Editor insert and package apply are single REST mutations and therefore receive one named request-level undo block; follow and filesystem export do not create graph undo entries.

TOX round-trip is additionally gated by TDMCP_PROJECT_RAG_QUARANTINE=1, refuses the artist bridge port 9980, accepts only absolute regular non-symlink .tox files, and never falls back to /api/exec, project.load() or project.quit(). Annotation layout context is read-only; apply requires its current fingerprint and fails before mutation on editor drift. Both annotation edit and layout apply snapshot supported state, verify exact readback and report rollback failure explicitly.

Mutation receipts report undo_label only when the bridge observes exactly one new native stack item after the request. That value is TouchDesigner's actual artist-visible item, such as Delete Node or Change Bypass Flag; when it differs, undo_wrapper_label preserves the bridge's requested wrapper name. Those labels are receipts for people and diagnostics, not safe programmatic undo identities: the native stack exposes repeated strings and no stable item ID. Automated undo/redo and whole-tool grouping therefore remain held after live probes reproduced both same-label ABA ambiguity and broken cross-request block ownership.

The guarded /api/operations/* family is narrower than generic whole-tool undo. It always requires TDMCP_BRIDGE_TOKEN, even when other loopback routes use zero-config auth, and remains usable with TDMCP_BRIDGE_ALLOW_EXEC=0. Preview is read-only; commit performs one bounded allowlisted transaction inside one Web Server DAT callback and owns its callback journal, so the entire family is excluded from the ordinary per-request undo wrapper. Receipt recovery uses a separate 256-bit capability plus the same principal and bridge instance; the capability stays in a POST body and the idempotency key is never recovery authority. Wave 15 added no MCP tool, CLI command, generic undo/redo or revert route, and the public controller path remains live-UNVERIFIED pending a safe disposable TD listener.

The node, parameter and authored-code search routes are also structured read-only operations independent of the exec gate. /api/nodes/search returns compact operator hits; /api/params/search keeps value/expression filters in the POST body, redacts likely credentials, refuses to use sensitive content as a filter oracle, skips unreadable parameters without exception text, and bounds depth, returned hits, node/parameter scans, response size and elapsed work. All three report whether the match count is complete. /api/code/search ranks fresh DAT bodies and parameter expressions locally, redacts before matching, returns only bounded excerpts and reports node/document/parameter/byte/time exhaustion without an embedding or raw-Python fallback. See Live project search.

Native interaction delivery

The bridge installer adds an Interactions custom page and a dedicated Parameter Execute DAT at deterministic, non-overlapping coordinates. Broker delivery is scheduled for the next TD frame, so POST /api/interactions never waits for an artist choice. ui.messageBox is not used because its behavior inside a Web Server DAT callback has not been live-validated for this bridge.

The server, not the HTTP caller, derives the target fingerprint from the current operator identity, normalized Save As path or validated display-safe OAuth target. Choices are fixed to Delete / Bypass / Keep, Overwrite / Keep and OAuth Allow / Deny. TTL is 5–120 seconds, the pending queue is capped, records are retained for a bounded time, and terminal results are consumable exactly once. UI loss, Perform Mode/headless operation, timeout, close/cancel, scheduling error and disconnect fail closed to Keep for destructive/file decisions and Deny for OAuth. Codes, verifier, state and tokens are never accepted by the TD route.

See Native interaction & safe editor actions for destructive tool usage, or OAuth, PKCE & TD consent for the remote-connection boundary and current live-validation limits.

Developing the bridge

TouchDesigner imports the bridge modules once at project open, so editing files under td/ does not update a running bridge. Reload without reopening:

python
from mcp import dev
dev.reload_bridge()   # reimports every mcp.* / utils.* module; returns the names reloaded

For the local dev loop, tdmcp-agent watch-build watches src/ and td/. When a saved change touches td/, it waits for typecheck/build to pass, runs python -m py_compile over the changed Python files, then calls the reload_bridge tool automatically. Use --no-reload-bridge for a build-only watcher, or --no-py-compile when you need to skip the Python syntax gate.

Bump BRIDGE_VERSION in td/modules/utils/version.py on every bridge change. get_td_info reports the running bridge's version, so when it lags the repo you know the running bridge is stale and should be reloaded.

Notes / known limitations

  • Operator types are resolved with a regex-guarded eval of the type name; only [A-Za-z][A-Za-z0-9_]* is accepted.
  • preview returns the TOP at its native resolution; the requested width/height are advisory.
  • WebSocket event streaming is stubbed in the callbacks and forwarded as MCP logging notifications by the server.