touchbridge
# touchbridge — a live-performance-grade MCP control plane for TouchDesigner
> Drive TouchDesigner from any MCP client (Claude, Cursor, …) — **while it's
> rendering a live show**, without ever destroying your project file.
`touchbridge` is an extraction of the TouchDesigner control layer built inside
ClipSense over hundreds of live shows. It is **not** another "WebServer-DAT +
HTTP" wrapper. It is built for the one situation every other tool falls over
in: **TouchDesigner under full render load, on stage.**
## The wedge: reliable-under-load, safe-by-construction, measurement-verified
Existing community TD-MCP servers (`8beeeaaat` ~13 tools, the WebSocket build
~29) share one architecture: an HTTP/WebSocket **server inside TouchDesigner's
own process** (a WebServer DAT). That's fine for design-time tinkering and
fails exactly when it matters:
| | Community tools (in-process HTTP/WS) | **touchbridge** |
|---|---|---|
| **Transport** | Sync HTTP/WS **in TD's process** — competes with the render thread; a busy TD `ETIMEDOUT`s | **Async file bridge** (`commands/`, `results/`, `status.json`) — poll-based; never blocks TD's main thread; rides through a busy/slow TD |
| **Liveness** | Fails hard if TD is closed; caches failure 60 s | **Heartbeat** `status.json` (age-based staleness) + round-trip canary (`measure_verify` → rtt_ms) |
| **File safety** | No `.toe` protection — `project.save` can overwrite your work | **`bridge_save_increment`**: writes `Show.<N+1>.toe`, **structurally refuses to overwrite**; `bridge_snapshot`/`bridge_restore` for backups |
| **Exec safety** | Runs arbitrary Python, no sandbox notes | **`script.exec` sandbox** (never into `globals()` — a payload once bricked ClipSense's bridge for 5 h; that scar is a guardrail now), and **capability tiers** (destructive tools hidden unless `--allow-destructive`) |
| **Health / cost** | `get_top_image` (a screenshot) | `measure_gpu` (util/VRAM + is-TD-actually-rendering), `measure_chain` (per-device topology + orphan detection), `measure_fps`, `measure_verify` (RTT canary) |
| **Tool surface** | ~13 / ~29 | **67 tools**: node CRUD, param get/set/expr/pulse, wiring, CHOP/TOP/SOP/DAT read + DAT write, timeline, render/export, project versions + snapshots, **batch**, layout, script introspection, measure — plus 3 live MCP resources |
| **Crash posture** | None | Self-heal onStart patterns, never-overwrite saves, "verify by measurement" ethos |
**The one-line pitch:** *every other TD-MCP is a design-time toy that talks to
TD over a socket in TD's own thread; touchbridge is a live-show control plane
that keeps working while TD renders and can't destroy your project.*
## Quickstart
1. **Install the bridge into a TouchDesigner project.** Get the prebuilt
`TouchBridge.tox` (from this repo) and drop it into any
project network — or build your own:
```
# in TouchDesigner's Textport (Alt+T):
exec(open(r"<repo>\td-mcp\tools\build_touchbridge_tox.py").read())
```
2. **Install the Python side:**
```
pip install touchbridge
```
3. **Point your MCP client at it** (stdio; the bridge folder defaults to
`~/.touchbridge/bridge` or `$TOUCHBRIDGE_DIR`):
```
touchbridge-mcp --name MyShow [--bridge-dir <path>]
```
Add `--allow-destructive` only if you want `node_delete` / `script_exec` /
`project_save` exposed. Safe mode (default) hides them entirely.
That's the whole setup: one `.tox`, one pip install, one command.
## How the bridge works
```
┌─────────────┐ commands/{id}.json ┌─────────────────────────────┐
│ MCP client │ ───────────────────────► │ TouchDesigner (TouchBridge)│
│ (Claude, …) │ status.json (heartbeat)│ bridge_poll() every ~0.5 s │
│ touchbridge-│ ◄─────────────────────── │ - eval / python │
│ mcp (stdio) │ results/{id}.json │ - 50+ router methods │
└─────────────┘ └─────────────────────────────┘
```
- The poll loop runs on a timer CHOP — **never on TD's main thread
synchronously**, so a stall in the file system cannot wedge the render.
- `owner.json` ensures exactly **one** TD instance services the queue (two
instances fighting over commands was a real ClipSense incident; the claim
lapses if its heartbeat goes stale).
- `status.json` is the truth of "is TD actually alive" (age < 5 s), not the
HTTP-response lie.
## The tool surface (67)
- **system** — `ping`, `info` (version, methods)
- **node** — `create`, `delete` ◊, `list`, `get`, `copy`, `rename`, `find`,
`errors`, `errors_deep`, `clear_script_errors`, `snapshot`, `set_flags`
- **par** — `get`, `set`, `get_all`, `info`, `set_expression`, `pulse`
- **conn** — `create`, `delete`, `get` (+ `connection.*` aliases)
- **data** — `chop`, `top` (base64 PNG), `pixel_sample` (luma stats + dark/solid flags), `sop`, `dat`, `dat_write`
- **script** — `exec` ◊, `class_list`, `class_detail`, `module_help`
- **timeline** — `get`, `set`, `play`, `pause`
- **render** — `screenshot`, `export`
- **project** — `info`, `save` ◊, `snapshot`, `versions`, `import_tox`, `export_tox` (never overwrites)
- **measure** ⭐ — `gpu`, `chain`, `fps`, `cooktimes`, `verify` (the moat)
- **layout** — `set_position`, `align`
- **batch** — `execute` (N router ops in ONE bridge round trip)
- **bridge (host-side)** — `bridge_status`, `bridge_state`,
`bridge_save_increment`, `bridge_snapshot`/`bridge_snapshots`/
`bridge_restore`, `bridge_ensure_alive`, `bridge_open_show`,
`bridge_router_call`, `bridge_router_version`, `bridge_send`
- **resources (live)** — `bridge://project`, `bridge://versions`,
`bridge://snapshots`
◊ = destructive, hidden in safe mode.
MCP tool name = router method with `.` → `_` (`node.list` → `node_list`).
## Safety model
- **Never-overwrite saves.** `bridge_save_increment` writes the next unused
`Show.<N+1>.toe` and **refuses** to write a path that already exists.
`bridge_snapshot` copies the live `.toe` to a timestamped backup.
- **Capability tiers.** Safe mode (default) drops `node_delete`,
`script_exec`, `project_save` from the surface *entirely* — not just a
warning. `bridge_router_call` enforces the same gate on raw method calls.
- **Exec sandbox.** `script.exec` runs in a copy of TD's globals, never into
the bridge's own module namespace — a payload cannot shadow `_log` and
brick the command channel (a real 5-hour outage that built this rule).
- **Verify by effect.** The server's instructions tell the model to read
parameters back / sample pixels / run `measure_verify` before claiming
success — the ClipSense lesson: an HTTP 200 is the process that *dispatched*
work reporting success, not the effect landing.
## Development
- `python -m pytest tests/` — **60 tests, no TouchDesigner required**:
the logic and router run against a mock op-graph; the file-bridge transport
is exercised with the real poll loop in a thread over a temp folder.
- The TouchDesigner integration layer (live `.tox` in a headless TD) is a
local/manual gate, labelled as such: TD has no headless "run this Python on
an empty project" mode, so a first build is `exec(open(...))` in a Textport.
## Repo layout
```
td-mcp/
├── touchbridge/
│ ├── command_router.py ← in-TD handler (58 methods), generic
│ ├── bridge_logic.py ← in-TD poll loop (eval/python/router dispatch)
│ ├── client.py ← host-side TouchBridge (send/eval/save_increment/snapshot)
│ └── mcp_server.py ← the MCP server (direct-to-bridge, no HTTP service)
├── tools/build_touchbridge_tox.py ← builds TouchBridge.tox in TD
├── tests/ ← off-TD suite (43)
├── PLAN.md ← extraction plan + roadmap
└── pyproject.toml
```
## MCP client setup
- **Registry + Claude Desktop / Cursor / omp config**: see `docs/MCP_REGISTRY.md`
(publish-ready entry + every client's JSON).
- **Project-level example**: `<repo>/td-mcp/.mcp.json` (copy the `touchbridge`
block into your own `.mcp.json`).
## Status / roadmap
The extraction is complete and fully tested off-TD. Feature roadmap in
`PLAN.md` §3 (WebSocket transport alongside the file bridge, streaming
thumbnails, batch ops, MCP resources, docs bridge, …). The one operator-gated
step before a first community release: build `TouchBridge.tox` in a real TD
Textport and verify one live round-trip.
## License
MIT — see `LICENSE`. Extracted from the TouchDesigner control layer of ClipSense; the show-specific parts (rack intent store, reconciler, clip matcher) stay private.
TDQS
Scored across 64 tools
Several tool clusters have unclear boundaries: conn_* tools have exact connection_* aliases, system_ping/bridge_status/bridge_state/measure_verify all report health-ish state, and project_snapshot/bridge_snapshot/bridge_snapshots overlap heavily. Descriptions are detailed, but an agent can easily select the wrong health or backup tool.
Nearly all tools follow a predictable snake_case domain-prefix + action pattern (node_create, par_set, timeline_play, bridge_restore), which makes the surface navigable. The main inconsistencies are the connection_* alias duplicates, batch_execute being verb-first, and data_chop/top/sop using family names rather than actions.
64 tools is far above the typical well-scoped MCP surface, and the set includes duplicate aliases plus raw escape-hatch methods that inflate the count. The broad TouchDesigner domain justifies many distinct operations, but the surface would be cleaner and more usable at roughly half this size.
The server covers node, parameter, connection, data, script, timeline, render, project, measure, and bridge operations, so most workflows are supported. However, there are notable gaps: no node_delete/destroy tool, no dedicated script execution except the raw bridge_send, and backup functionality is split across overlapping snapshot tools.