Skip to main content
Glama
README.md
# touchbridge

**The TouchDesigner MCP you can leave connected during a show.**

Most TouchDesigner MCP servers are built for working in the patch: a designer
at the keyboard, TD mostly idle, an agent building networks. touchbridge is
built for the moment the show is running: TD under full render load, a project
file you can't lose, and an operator who needs to know the second something
breaks.

touchbridge is the TouchDesigner control layer from ClipSense, a live-visuals
system, pulled out into its own project. Every rule in it comes from a real
failure on a real show.

### What only touchbridge does

- **Show mode: a performance lock enforced inside TD.** One call (or one
  command) locks the bridge: the agent can still read everything, and it can
  change only the parameters you allowlisted, e.g. `/project1/master:Opacity`.
  Creating, deleting, wiring, scripting and saving are all refused **by TD
  itself**, so the lock holds for every client, not just this server. Leaving
  show mode takes the operator typing `END SHOW`. The mode survives restarts.
- **A show watchdog.** `touchbridge-watch` (or the `bridge_health` tool)
  reports TD crashed, TD frozen, FPS below the cook rate, frame time over
  budget, dropped frames, and output black, solid or frozen. The numbers come
  from TD's own Perform CHOP. `--reopen` brings the last saved `.toe` back up
  after a crash.
- **No server inside TD.** Commands are files. TD picks them up on a 0.1 s
  tick at its own pace (an idle tick costs 0.1 ms), and a busy TD answers late
  instead of dropping connections. A heartbeat tells busy from dead. Measured
  round trip: about 86 ms median.
- **It can't overwrite your project.** Saves go to a new numbered `.toe`.
  Snapshots copy files and never Save As. Exports refuse `.toe`/`.tox`
  targets. Safe mode is on by default: delete, raw Python and in-place save
  are removed from the tool list, and the batch and raw routes are gated too.

### And everything you'd expect

70 tools covering nodes, parameters, wiring, CHOP/TOP/SOP/DAT data, timeline,
render, project versions, batch, layout, and measurement (cook-time ranking,
GPU, pixel statistics, a full-network error sweep). Results come back as
compact YAML: about 30% fewer tokens than JSON with nothing lost, and about
80% fewer with `detail="summary"`.

> How it compares to the other TouchDesigner MCP servers, honestly:
> [docs/COMPARISON.md](docs/COMPARISON.md).

## Quickstart

**1. Put the bridge in your project.** Drag
[`touchdesigner/TouchBridge.tox`](touchdesigner/) (or get it from the
[latest release](https://github.com/GaNotchVFX/touchbridge/releases)) into any
TouchDesigner project and save. Nothing needs to be installed on the TD side;
the bridge code is embedded in the `.tox`. To try it right away, open
`touchdesigner/TouchBridge.toe`, which already has the `.tox` in it.

**2. Install the MCP server** (Python 3.11+). `pipx` puts `touchbridge-mcp`
on your PATH, where Claude Desktop and Cursor can find it:

```bash
pipx install "git+https://github.com/GaNotchVFX/touchbridge@v0.4.0"
# or: pip install "touchbridge @ git+https://github.com/GaNotchVFX/touchbridge@v0.4.0"
```

If your client can't find the command, put the full path to
`touchbridge-mcp` (`where touchbridge-mcp` / `which touchbridge-mcp`) in
`"command"`.

**3. Point your MCP client at it.** For Claude Desktop, Cursor, and anything
else that reads `mcpServers`:

```json
{
  "mcpServers": {
    "touchbridge": { "command": "touchbridge-mcp" }
  }
}
```

That's it. Both sides default to the same bridge folder
(`~/.touchbridge/bridge`), so there's nothing to configure. Ask your agent to
run `bridge_status`; you should see `alive: true`.

Useful flags: `--allow-destructive` (turns off safe mode), `--bridge-dir PATH`
(or `$TOUCHBRIDGE_DIR`, set for **both** TD and the server), `--format json`,
`--detail summary`. See [docs/MCP_REGISTRY.md](docs/MCP_REGISTRY.md) for more
client configs.

## How it works

```
┌──────────────┐  commands/{id}.json       ┌──────────────────────────────┐
│  MCP client  │ ────────────────────────► │ TouchDesigner + TouchBridge  │
│ (Claude, …)  │  mode.json (show / prep)  │  timer CHOP, every 0.1 s:    │
│ touchbridge- │                           │   show-mode gate, then run   │
│ mcp  (stdio) │  status.json  (heartbeat) │   ≤4 queued commands;        │
│              │ ◄──────────────────────── │   heartbeat + Perform stats  │
└──────────────┘  results/{id}.json        └──────────────────────────────┘
```

- **TD pulls work; nothing is pushed into it.** There's no listening socket and
  no request handler waiting on TD's main thread. Each tick picks up at most 4
  commands, in arrival order, so a flood of calls can't pile into one frame
  (a single heavy command, such as a big `batch_execute` or a full-project
  error sweep, still costs what it costs). If TD is slow, answers arrive
  later; nothing breaks.
- **The heartbeat is the source of truth.** `status.json` is rewritten every
  tick. `bridge_state` reports `ok`, `starting`, `down` (TD running but not
  answering) or `off`.
- **One owner.** With two TD instances open, exactly one services the queue
  (`owner.json`). The claim hands over automatically when the owner goes quiet.
  This fixed a real incident where two instances were fighting over commands.
- **Cost and speed, measured** on TD 2025.32820 (Windows, NVIDIA) at 60 fps:
  an idle tick costs 0.1 ms of TD's frame, and FPS and frame time were
  unchanged with the bridge running. A round trip is 86 ms median (p90
  125 ms). `measure_verify` reports yours as `round_trip_ms`. For 60 Hz
  streaming control, use OSC for that stream and touchbridge for everything
  else.

## Show mode

```bash
touchbridge-mcp --set-mode show --allow "/project1/master:Opacity" --allow "/project1/fx*:Mix"
touchbridge-mcp --set-mode prep          # operator ends the show
```

Or from the agent: `bridge_set_mode(mode="show", allow=[...])`. Leaving from
the agent needs `confirm="END SHOW"`, and the server tells the model that
phrase must come from the human.

| In show mode | |
|---|---|
| Always allowed | every read: nodes, params, CHOP/TOP/SOP/DAT data, errors, measure_*, read-only expressions |
| Allowed if allowlisted | `par_set` / `par_pulse` on `"/path:ParName"` globs you set when entering show mode |
| Refused **inside TD** | create, delete, copy, rename, wire, flags, expressions, DAT writes, scripts, exports, timeline changes, saves, snapshots taken inside TD |
| Refused by the server | `bridge_save_increment` (a `.toe` save stalls the frame for as long as the write takes) |

The mode is `<bridge>/mode.json`, read by TD on every tick. It survives
restarts, and an unreadable mode file counts as **show**, so the lock fails
closed. The allowlist can't be widened mid-show.

## Watchdog

```bash
touchbridge-watch --output /project1/out1            # alerts on change, logs to <bridge>/watchdog.log
touchbridge-watch --output /project1/out1 --reopen   # + reopen the last save if TD crashes
touchbridge-watch --once                             # one check; exit code 1 if anything is wrong
```

```
21:54:03  ok  fps 61.0/60.0  3.3 ms  all clear
```

(When something breaks, the line ends in `ALERT:` followed by the issues,
e.g. `fps_low: 38.0 of 60; frame_time_high: 26.1 ms (budget 16.7 ms)`.)

It checks TD running / heartbeating (`td_not_running`, `bridge_down`), real
FPS vs cook rate, frame time vs budget, dropped frames, realtime off, and, with
`--output`, an output that's black, solid, or showing identical stats twice.
It never kills or restarts a TD that's still running; `--reopen` only acts
once the process is gone. The same snapshot is available to agents as
`bridge_health`.

## Safety model

| Guard | What it does |
|---|---|
| **Safe mode (default on)** | `node_delete`, `script_exec`, `project_save` are **removed from the tool list**, not just flagged with a warning. The same gate covers the indirect routes too: an op inside `batch_execute` (nested batches included), `bridge_router_call`, and `bridge_send`'s raw `python`/`eval`. Unknown methods are refused, and `measure_verify` only accepts read-only expressions. |
| **Never-overwrite saves** | `bridge_save_increment` saves to the next unused `Name.<N+1>.toe` and never targets an existing path. TD's own version counter decides the final name, which is read back and reported. |
| **Snapshots / restore** | `bridge_snapshot` / `project_snapshot` copy the last saved `.toe` to a timestamped backup; they never Save As, so the live session stays on your file. `bridge_restore` lands as a **new** `Name.restored.<N>.toe` next to it. `node_snapshot` saves a node's full parameter state for diffing. |
| **Export guards** | `render_export` / `render_screenshot` / `project_export_tox` refuse to overwrite existing files unless you pass `overwrite=true`. Image exports **always** refuse `.toe`/`.tox` targets. `project_import_tox` only loads from the project folder (or `$TOUCHBRIDGE_TOX_ROOTS`). |
| **Exec isolation** | `python`/`eval` payloads run in their own namespace, never the bridge's globals. A payload once replaced a helper the poll loop depends on and silently killed the command channel for 5 hours while the heartbeat looked fine. This is isolation, **not a security sandbox**: when safe mode is off, code runs with TD's full powers. |
| **Measure, don't assume** | The server tells the model to confirm results by effect (read params back, `data_pixel_sample`, `measure_verify`) before reporting success. |

**What safe mode is and isn't.** It's a guardrail against accidents: a
confused agent can't delete a network, run a script, or save over your file
with a single call. It is **not** a security boundary against an agent that's
trying to get around it. Write-tier tools can still set Python parameter
expressions or build an Execute DAT. If you don't trust the model or the
prompt, don't connect it to a show machine.

The bridge folder is local: any process running as your user can drop commands
in it, the same trust level as a localhost port. Don't share it over a network
drive with people you don't trust.

## Tools (70; 67 in safe mode)

Tool name = router method with `.` → `_` (`node.list` → `node_list`).
◊ = destructive, hidden in safe mode.

- **system**: `ping`, `info`
- **node**: `create`, `delete`◊, `list`, `get`, `copy`, `rename`, `find`,
  `set_flags`, `errors`, `errors_deep` (subtree sweep: cook, warning,
  expression and script errors), `clear_script_errors` (tells a live problem
  from a stale one), `snapshot` (full param state to diff)
- **par**: `get`, `set`, `get_all`, `info`, `set_expression`, `pulse`
- **conn**: `create`, `delete`, `get` (plus `connection.*` aliases)
- **data**: `chop`, `top` (optional base64 PNG), `sop`, `dat`, `dat_write`,
  `pixel_sample` (luma mean/min/max/std with dark/solid/clipped flags)
- **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`
- **measure**: `cooktimes` (per-op cost, most expensive first), `fps`
  (FPS, realtime, throttle queue), `gpu` (NVIDIA util/VRAM measured host-side +
  "is TD really rendering"), `verify` (real round trip plus a live TD value),
  `chain` (Src/Srcpath topology and orphan detection for COMP chains that use
  those custom pars)
- **batch**: `execute` (N ops in one round trip; not a rollback transaction)
- **layout**: `set_position`, `align`
- **show / health (host side)**: `bridge_mode`, `bridge_set_mode`, `bridge_health`
- **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**: `bridge://project`, `bridge://versions`, `bridge://snapshots`
- **prompts**: `diagnose_dark_output`, `wire_feedback_safely`, `save_before_editing`

### Compact results

Every tool accepts two optional arguments:

- `response_format`: `yaml` (default) or `json`. The YAML is lossless: it
  parses back to exactly the same data.
- `detail`: `full` (default), `summary` (lists cut to 25 items plus a
  `... N more` line) or `minimal` (top-level values only; containers become
  `<N items>`). Long strings such as base64 images are never cut.

On sample payloads (80-parameter `par_get_all`, 120-child `node_list`), YAML is
about 30% smaller than the JSON the MCP SDK sends, and `summary` is 78–86%
smaller. Set server-wide defaults with `--format` / `--detail` or
`$TOUCHBRIDGE_FORMAT` / `$TOUCHBRIDGE_DETAIL`.

## Development

```bash
pip install -e ".[dev]"
python -m pytest            # 195 tests, no TouchDesigner needed
```

The router runs against a mock op graph, the file-bridge transport runs the
real poll loop in a thread, and the `.tox` bodies run against a mock COMP.

Rebuild `TouchBridge.tox` after changing `bridge_logic.py` or
`command_router.py`. In TD's Textport (Alt+T):

```python
TB_REPO = r"C:\path\to\touchbridge"
exec(open(TB_REPO + "/tools/build_touchbridge_tox.py").read())
```

Then run `python tools/verify_touchbridge.py` against real TD. It checks the
heartbeat, a `node_get` round trip, round-trip timing and the router version. For a live-edit
loop without rebuilding, set `TOUCHBRIDGE_PKG=<repo>/touchbridge` on the
process that launches TD; the `.tox` then loads the `.py` files from disk.

```
touchbridge/
  command_router.py   in-TD router: 56 handlers (node/par/data/measure/…)
  bridge_logic.py     in-TD poll loop, show-mode gate, heartbeat, owner claim
  client.py           host-side client (send, save_increment, snapshots)
  mcp_server.py       MCP server: tool catalog, safe-mode gating, resources
  watch.py            touchbridge-watch: the show watchdog
  formatting.py       compact YAML / detail shaping
tools/build_touchbridge_tox.py   builds the self-contained TouchBridge.tox
touchdesigner/                   TouchBridge.tox, example TouchBridge.toe, usage notes
tools/verify_touchbridge.py      live post-install check
```

## Status and limits

- Developed and used on **Windows** with current TouchDesigner builds. Nothing
  in the code is Windows-only, but macOS hasn't been tested yet; reports are
  welcome.
- GPU stats need an NVIDIA GPU (`nvidia-smi`). Everything else is vendor-neutral.
- The live-TD integration check is manual (`tools/verify_touchbridge.py`):
  TouchDesigner has no headless mode for CI.
- Roadmap: [PLAN.md](PLAN.md). Changes: [CHANGELOG.md](CHANGELOG.md).

## License

MIT; see [LICENSE](LICENSE).

TDQS

B3/5.0

Scored across 67 tools

Disambiguation3/5

There are several pairs of tools with overlapping purposes: node_get vs node_snapshot (both read node state), par_get vs par_info vs par_get_all (all read parameter info), and the connection_* aliases duplicate conn_* tools. However, many tools have distinct purposes (node_create vs node_copy vs node_rename). The presence of aliases and similar read endpoints creates some ambiguity.

Naming Consistency3/5

The naming is mostly consistent with verb_noun pattern (e.g., node_get, par_set, data_chop), but there are deviations: some tools use 'get' (node_get) while others use 'list' (node_list), and the use of 'connection_*' aliases alongside 'conn_*' breaks consistency. Also, 'par_get_all' versus 'par_info' are not clearly differentiated by name.

Tool Count2/5

With 57 tools, this is far beyond the typical 3-15 range and even exceeds the 25+ threshold. The server covers a broad domain (TouchDesigner automation), but many tools are highly specialized (e.g., measure_chain, data_pixel_sample) and could be consolidated. The count feels overwhelming and undermines usability.

Completeness4/5

The tool surface is quite comprehensive: it covers node CRUD (create, copy, rename, delete? no delete but set_flags), parameter operations (get, set, expression, pulse), connections, data reading for all families, timeline control, rendering, project save/restore, and performance measurement. Minor gaps include no explicit node delete or connection listing beyond conn_get, but the breadth is strong.

Maintenance

ActivityMaintained
ResponsivenessNo issues