Skip to main content
Glama
README.md
# 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

C2.6/5.0

Scored across 64 tools

Disambiguation2/5

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.

Naming Consistency4/5

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.

Tool Count2/5

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.

Completeness3/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues