td-mcp
by XTNX
README.md
# TouchDesigner ↔ MCP bridge
Local bridge that lets an AI agent read and build **real** TouchDesigner
networks: create operators, wire them, set parameters and expressions, lay the
network out, validate it, **look at the rendered result as an image**, and
iterate on what it sees.
Works with any MCP-compatible host — Claude Code, Claude Desktop, Cursor,
Antigravity, VS Code Copilot, Windsurf, Cline, Zed, Gemini CLI, Codex CLI.
Nothing in the server is vendor-specific; it imports only `mcp` and `httpx`.
**→ [Connecting an AI agent](docs/agents.md)** — per-host setup, verified.
```powershell
python docs/verify_stdio.py # proves the server works with any MCP host
```
---
## 1. What this is
```
your AI agent
│ MCP over stdio
▼
C:\td-claude-mcp\server.py ← separate Python 3.11 venv
│ HTTP POST http://127.0.0.1:9980/mcp
▼
TouchDesigner Web Server DAT (mcp_webserver)
│
▼
mcp_callbacks (td_webserver_callback.py)
│
▼
TouchDesigner Python API → your live project
```
Two hard rules the design keeps:
* **The MCP server never imports TouchDesigner.** It only speaks HTTP.
* **The TouchDesigner side never needs a pip package.** Standard library only.
Only TouchDesigner itself ever mutates the network, so nothing can get out of
sync.
**No fake capabilities.** Every mutating tool re-reads the object from the live
network after the change and returns what TouchDesigner actually holds — not
what was requested. A reported success is a verified success.
---
## 2. Architecture
| Piece | File | Runs in |
|---|---|---|
| MCP tool surface (32 tools) | `server.py` | venv Python 3.11 |
| HTTP client | `td_bridge.py` | venv Python 3.11 |
| Configuration | `config.py`, `config.json` | venv Python 3.11 |
| HTTP API + TouchDesigner logic (35 actions) | `td_webserver_callback.py` | TouchDesigner |
| Pasteable Textport stub (4 statements) | `td_bootstrap.py` | TouchDesigner Textport |
| Bootstrap implementation it exec's | `td_bootstrap_impl.py` | TouchDesigner |
| Tests | `tests/` | venv Python 3.11 |
### HTTP protocol
```
POST /mcp
{"id": "...", "action": "...", "args": {...}}
→ {"ok": true, "id": "...", "result": {...}, "error": null}
→ {"ok": false, "id": "...", "result": null,
"error": {"type": "...", "message": "...", "details": {...}, "traceback": "..."}}
```
`GET /health` · `GET /info` · `GET /api` · `GET /version`
Error types: `OperatorNotFound`, `OperatorTypeInvalid`, `ParameterNotFound`,
`ParameterInvalid`, `ConnectionInvalid`, `ConnectorNotFound`,
`UnsupportedOperation`, `TouchDesignerError`, `ValidationError`,
`ProjectNotSaved`, `PreviewError`, `BatchError`, `PythonExecutionError`,
`ServerUnavailable`, `TimeoutError`.
---
## 3. Requirements
* Windows
* TouchDesigner 2025.32460 (developed and tested against this build)
* System Python **3.10+**, 3.11 preferred — **not** TouchDesigner's Python
* Claude Code
* No Node.js. No Docker. No database. No cloud.
Python dependencies: `mcp[cli]>=2.1.1` and `httpx`. That is all.
> **MCP SDK note.** This bridge targets **MCP Python SDK 2.x**, where `FastMCP`
> was renamed to `MCPServer` (`from mcp.server.mcpserver import MCPServer`).
> Code copied from older FastMCP examples will not run here.
---
## 4. Installation
```powershell
powershell -ExecutionPolicy Bypass -File C:\td-claude-mcp\install.ps1
```
Creates the directories, builds `.venv`, upgrades pip, installs
`requirements.txt`, and verifies that every module imports and all 32 MCP tools
register.
Rebuild the environment from scratch:
```powershell
powershell -ExecutionPolicy Bypass -File C:\td-claude-mcp\install.ps1 -Recreate
```
---
## 5. TouchDesigner setup
**One step:**
> Open TouchDesigner → press **Alt+T** for the Textport → paste the entire
> contents of `C:\td-claude-mcp\td_bootstrap.py` → press **Enter**.
Then press **Ctrl+S** to save the project, so the bridge operators persist.
The bootstrap creates, inside `/project1`:
| Operator | Type | Purpose |
|---|---|---|
| `mcp_callbacks` | Text DAT | holds `td_webserver_callback.py` |
| `mcp_webserver` | Web Server DAT | listens on port 9980 |
It introspects the running build, sets only parameters that actually exist,
and prints an exact manual instruction for anything it cannot set safely.
It is idempotent — re-run it any time to refresh the callback code.
`td_bootstrap.py` is deliberately tiny: the Textport behaves like a Python
REPL, where a blank line inside an indented block ends the block, so a large
script cannot be pasted into it reliably. The stub reads and `exec`s
`td_bootstrap_impl.py` from disk instead, which also means re-running it always
picks up the current bridge code.
### Manual setup (if you prefer, or if the bootstrap reports a manual step)
1. Open TouchDesigner and create/open a project.
2. In `/project1`, create a **Text DAT** named `mcp_callbacks`.
3. Paste the entire contents of `C:\td-claude-mcp\td_webserver_callback.py`
into it.
4. In `/project1`, create a **Web Server DAT** named `mcp_webserver`.
5. Set **Port** = `9980`.
6. Set **Callbacks DAT** = `mcp_callbacks`.
7. If the DAT has a **Local Address** parameter, set it to `127.0.0.1`.
8. Turn **Active** **On**.
9. Save the project (Ctrl+S).
---
## 6. Bootstrap verification
```powershell
Invoke-RestMethod http://127.0.0.1:9980/health
```
Expected:
```
ok id result error
-- -- ------ -----
True @{status=ok; bridge_version=1.0.0; project=...}
```
Full self-test:
```powershell
powershell -ExecutionPolicy Bypass -File C:\td-claude-mcp\start_test.ps1
```
---
## 7. Connect your AI agent
Full per-host instructions, including the hosts whose config shape differs:
**[docs/agents.md](docs/agents.md)**.
Most hosts take this block — Claude Desktop, Cursor, Antigravity, Windsurf,
Cline, Gemini CLI, JetBrains AI Assistant:
```json
{
"mcpServers": {
"touchdesigner": {
"command": "C:\\td-claude-mcp\\.venv\\Scripts\\python.exe",
"args": ["C:\\td-claude-mcp\\server.py"]
}
}
}
```
Claude Code, from the CLI:
```powershell
claude mcp add touchdesigner --scope user -- "C:\td-claude-mcp\.venv\Scripts\python.exe" "C:\td-claude-mcp\server.py"
```
Two hosts want a different shape — **VS Code** uses `servers` instead of
`mcpServers`, **Zed** uses `context_servers`, and **Codex CLI** uses TOML.
Those are written out in [docs/agents.md](docs/agents.md).
Whatever the host: use the **absolute path** to `.venv\Scripts\python.exe`.
The host does not inherit your shell's PATH or your active virtualenv, and it
must never be pointed at TouchDesigner's own Python.
Restart the host afterwards — most read MCP config only at startup — then ask
it: *"Check the connection to TouchDesigner."* It should call `td_status`.
To check the server independently of any host:
```powershell
python docs\verify_stdio.py
```
---
## 8. First test
Ask Claude Code:
> Build a simple visual system: Noise → Blur → Level. Configure it, lay it out
> neatly, check for errors and show me a preview.
Claude should inspect, create, connect, configure, lay out, validate, preview,
fix anything broken, and report what it changed.
---
## 9. Available tools
**Connection** — `td_status`, `td_get_project_info`
**Inspection** — `td_list_operators`, `td_find_operators`,
`td_inspect_operator`, `td_get_network_graph`, `td_list_operator_types`
**Parameters** — `td_get_parameters`, `td_get_parameter`, `td_set_parameter`,
`td_set_expression`, `td_reset_parameter`, `td_pulse_parameter`
**DAT text** — `td_get_text`, `td_set_text`
**Creation** — `td_create_operator`, `td_delete_operator`,
`td_rename_operator`, `td_move_operator`, `td_set_node_properties`
**Connections** — `td_connect`, `td_disconnect`
**Copy** — `td_copy_operator`, `td_copy_operators`
**Layout** — `td_layout` (`auto` / `horizontal` / `vertical` / `grid` / `native`)
**Validation & cook** — `td_validate_network`, `td_cook`
**Project** — `td_backup_project`, `td_save_project`
**Preview** — `td_preview`
**Escape hatch** — `td_execute_python`
**Batch** — `td_batch`
---
## 10. Preview
`td_preview(operator_path)` force-cooks a TOP and returns a real MCP
`ImageContent` block, so Claude can **see** the render and judge it, not just
read a file path.
* `TOP.saveByteArray()` is used when available; otherwise the tool falls back
to `TOP.save()` into `previews\`, reads the bytes back and deletes the file.
The response reports which path was taken in `method`.
* Formats: `png` (default), `jpg`/`jpeg`, `exr`.
* Only TOPs can be previewed. For a CHOP/SOP/COMP result, wire it into a TOP
(a Null TOP works) and preview that.
---
## 11. Backup
`td_backup_project()` writes a timestamped restore point into
`C:\td-claude-mcp\backups\` and **never moves the live project**.
> **Why it does not use `project.save(path)`.** TouchDesigner's own docstring
> for it is *"Save the current session to disk"*, and `project.name` /
> `project.folder` are read-only properties that follow the save. Saving to a
> backup path therefore makes the backup the current project — the user's next
> Ctrl+S would land in the backups folder. This build offers no save-a-copy
> call, so backups are made two other ways instead.
| Artifact | How | Includes unsaved edits? | Restore |
|---|---|---|---|
| `<project>_<root>_<stamp>.tox` | `COMP.save()` on the root component | **yes** | Load into a COMP's Load Tox parameter, or right-click a network → Import .tox |
| `<project>_<stamp>.toe` | byte copy of the last saved `.toe` | no | open directly in TouchDesigner |
`mode` selects one (`snapshot` / `copy`); the default `auto` does both and
reports whatever succeeded. Existing backups are never overwritten. The
response carries `project_has_unsaved_changes` plus a per-artifact
`includes_unsaved_changes`, and warns when unsaved work is not covered.
Note that the true root `/` refuses `COMP.save()` ("Error creating file."), so
the snapshot targets the configured root component (`/project1`) instead.
`td_batch` takes a backup automatically before any batch containing a delete.
If the project has never been saved there is no `project.folder`, so backup
returns `ProjectNotSaved` and you must save the project once from the
TouchDesigner UI first.
### Saving
`td_save_project()` with no path saves in place. Passing a `path` is **not** a
save-a-copy — it repoints the live project, so it is refused unless you also
pass `confirm_repoint=true`. The response reports `project_path_before`,
`project_path_after` and `project_repointed`.
---
## 12. Security
* **The Web Server DAT in 2025.32460 has no bind-address parameter.** Its
parameters are `pageindex, active, restart, port, callbacks, secure,
privatekey, certificate, password, verifyclient, minprotocol` plus the common
DAT ones — there is no Local Address. The socket is therefore opened on
`0.0.0.0` and that cannot be changed from the DAT.
The bridge enforces loopback-only access itself instead: `onHTTPRequest`
reads the request's `clientAddress` and refuses anything that is not on the
loopback interface with **403 Forbidden**, before the request reaches the
dispatcher. Verified live — a request to the machine's LAN address is
refused while `127.0.0.1` is served.
`GET /health` reports the posture under `security`: `enforced`,
`client_address_key`, `rejected_requests` and the request keys observed. If
`enforced` is ever `false`, the client address could not be identified and
requests are **not** being filtered — do not open port 9980 in the firewall.
* Do not open port 9980 in the Windows firewall.
* No password is stored in the source. Authentication is not needed for a
loopback-only endpoint; if you add it later, put it in `config.json`.
* `td_execute_python` runs arbitrary Python inside TouchDesigner. It is
reachable only over loopback, and it is documented to the model as an escape
hatch of last resort.
* Protected paths (`/`, `/project1`, `/local`, `/sys`, `/ui`, `/perform`) and
the bridge's own operators cannot be deleted without an explicit `force`.
* Deletion always requires `confirm=true`.
---
## 13. Troubleshooting
| Symptom | Cause / fix |
|---|---|
| **Port 9980 already in use** | `Get-NetTCPConnection -LocalPort 9980` to find the owner. Either stop it, or change the port in `config.json` *and* on the `mcp_webserver` DAT. |
| **TouchDesigner not running** | `td_status` returns `ServerUnavailable`. Start TouchDesigner and open the project containing the bridge. |
| **Web Server DAT inactive** | Select `mcp_webserver`, turn **Active** on. Re-run `td_bootstrap.py` to fix it automatically. |
| **Wrong Local Address** | This build's Web Server DAT has no such parameter — see Security above. The bridge filters by client address instead; check `security.enforced` in `GET /health`. |
| **Project ended up in the backups folder** | Something called `project.save(<path>)`, which repoints the live project. Fix with File → Save As back to your intended `.toe`. `td_backup_project` no longer does this, and `td_save_project` refuses a new path without `confirm_repoint=true`. |
| **`OP.type` looks wrong** | `OP.type` is the SHORT name (`noise`, `webserver`); the full class name used by `create()` is `OP.opType` (`noiseTOP`, `webserverDAT`). The bridge returns the full name as `type` and the short one as `short_type`. |
| **Callbacks DAT not assigned** | The bridge answers with HTML instead of JSON → `ProtocolError` mentioning the Callbacks DAT. Set **Callbacks DAT** = `mcp_callbacks`. |
| **MCP server not registered** | `claude mcp list` does not show `touchdesigner`. Re-run the `claude mcp add` command with absolute paths. |
| **Python not found** | `install.ps1` aborts. Install Python 3.11 from python.org and re-run. Do **not** point it at TouchDesigner's Python. |
| **`mcp` import error** | `No module named 'mcp.server.fastmcp'` means you are on mcp 2.x — that is expected, this project uses `mcp.server.mcpserver`. `No module named 'mcp'` means the venv is not installed: re-run `install.ps1`. |
| **`httpx` import error** | `& C:\td-claude-mcp\.venv\Scripts\python.exe -m pip install -r C:\td-claude-mcp\requirements.txt` |
| **Claude does not see the tools** | Restart Claude Code after registering. Check `logs\mcp_server.log` for a startup line. Confirm the command path points at `.venv\Scripts\python.exe`. |
| **Preview unavailable** | Only TOPs can be previewed. `PreviewError` naming a family means you pointed at a CHOP/SOP/COMP — wire it into a Null TOP first. |
| **Operator type invalid** | `OperatorTypeInvalid` includes similar valid names. Call `td_list_operator_types` instead of guessing. |
| **Parameter not found** | `ParameterNotFound` includes similar parameter names. Call `td_get_parameters` for the real list. Multi-value parameters are per-component: `resolutionw`, not `resolution`. |
| **TouchDesigner errors after a change** | `td_validate_network` lists them per node with the parameter that broke. Fix, re-validate, then save. |
| **Project not saved** | `ProjectNotSaved` from backup/save means the .toe has no folder yet. Save it once from the TouchDesigner UI. |
| **Windows path escaping** | In JSON use `C:/td-claude-mcp/...` or `C:\\td-claude-mcp\\...`. In PowerShell quote every path containing spaces. |
| **UTF-8 problems** | The TD-side callback is pure ASCII and all JSON is emitted with `ensure_ascii=True`, so it survives any console codepage. If PowerShell mangles output, set `$env:PYTHONIOENCODING='utf-8'`. |
| **Timeout on a heavy cook** | Raise `timeout` on `td_cook`/`td_batch`, or `TD_TIMEOUT` for everything. |
Logs: `C:\td-claude-mcp\logs\mcp_server.log` (stderr is mirrored there; stdout
is reserved for the MCP protocol and stays clean).
---
## 14. Development
Run the tests:
```powershell
cd C:\td-claude-mcp
& .\.venv\Scripts\python.exe -m unittest discover -s tests -t . -v
```
Static tests only (no TouchDesigner needed):
```powershell
& .\.venv\Scripts\python.exe -m unittest discover -s tests -t . -p "test_[cehs]*.py"
```
Live integration test (creates and deletes `__mcp_test_*` operators only):
```powershell
& .\.venv\Scripts\python.exe -m unittest tests.test_integration -v
```
Environment variables override `config.json`:
| Variable | Default |
|---|---|
| `TD_HOST` | `127.0.0.1` |
| `TD_PORT` | `9980` |
| `TD_PATH` | `/mcp` |
| `TD_TIMEOUT` | `10` |
| `TD_ROOT` | `/project1` |
| `TD_PREVIEW_FORMAT` | `png` |
| `TD_LOG_LEVEL` | `INFO` |
| `TD_INSTANCE` | `main` |
Multi-instance is prepared but not enabled in v1: add an `instances` block to
`config.json` and select one with `TD_INSTANCE`. WebSocket transport is
deliberately not implemented in v1; the action dispatcher is transport-agnostic
so it can be added later without touching the tools.
---
## 15. Updating
After editing `td_webserver_callback.py`, push it into the running project
without restarting TouchDesigner:
```powershell
$body = @{ id = "reload"; action = "reload_callbacks"; args = @{} } | ConvertTo-Json -Compress
Invoke-RestMethod -Uri http://127.0.0.1:9980/mcp -Method Post -Body $body -ContentType application/json
```
Or ask Claude to run `td_execute_python` with the same action. Re-running
`td_bootstrap.py` in the Textport does the same thing and also re-checks the
Web Server DAT configuration.
After editing `server.py`, `td_bridge.py` or `config.py`, restart Claude Code —
the MCP server process is launched by Claude and is only reloaded on restart.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues