feat: add TouchDesigner integration skill

New skill: creative/touchdesigner — control a running TouchDesigner
instance via REST API. Build real-time visual networks programmatically.

Architecture:
  Hermes Agent -> HTTP REST (curl) -> TD WebServer DAT -> TD Python env

Key features:
- Custom API handler (scripts/custom_api_handler.py) that creates a
  self-contained WebServer DAT + callback in TD. More reliable than the
  official mcp_webserver_base.tox which frequently fails module imports.
- Discovery-first workflow: never hardcode TD parameter names. Always
  probe the running instance first since names change across versions.
- Persistent setup: save the TD project once with the API handler baked
  in. TD auto-opens the last project on launch, so port 9981 is live
  with zero manual steps after first-time setup.
- Works via curl in execute_code (no MCP dependency required).
- Optional MCP server config for touchdesigner-mcp-server npm package.

Skill structure (2823 lines total):
  SKILL.md (209 lines) — setup, workflow, key rules, operator reference
  references/pitfalls.md (276 lines) — 24 hard-won lessons
  references/operators.md (239 lines) — all 6 operator families
  references/network-patterns.md (589 lines) — audio-reactive, generative,
    video processing, GLSL, instancing, live performance recipes
  references/mcp-tools.md (501 lines) — 13 MCP tool schemas
  references/python-api.md (443 lines) — TD Python scripting patterns
  references/troubleshooting.md (274 lines) — connection diagnostics
  scripts/custom_api_handler.py (140 lines) — REST API handler for TD
  scripts/setup.sh (152 lines) — prerequisite checker

Tested on TouchDesigner 099 Non-Commercial (macOS/darwin).
This commit is contained in:
kshitijk4poor 2026-04-15 10:33:15 +05:30 committed by Teknium
parent c49a58a6d0
commit 7a5371b20d
9 changed files with 3277 additions and 0 deletions

View file

@ -0,0 +1,140 @@
"""
Custom API Handler for TouchDesigner WebServer DAT
===================================================
Use this when mcp_webserver_base.tox fails to load its modules
(common the .tox relies on relative paths to a modules/ folder
that often break during import).
Paste into TD Textport or run via exec(open('...').read()):
Creates a WebServer DAT + Text DAT callback handler on port 9981.
Implements the core endpoints the MCP server expects.
After running, test with:
curl http://127.0.0.1:9981/api/td/server/td
"""
root = op('/project1')
# Remove broken webserver if present
old = op('/project1/mcp_webserver_base')
if old and old.valid:
old.destroy()
# Create WebServer DAT
ws = root.create(webserverDAT, 'api_server')
ws.par.port = 9981
ws.par.active = True
ws.nodeX = -800; ws.nodeY = 500
# Create callback handler
cb = root.create(textDAT, 'api_handler')
cb.nodeX = -800; cb.nodeY = 400
cb.text = r'''
import json, traceback, io, sys
def onHTTPRequest(webServerDAT, request, response):
uri = request.get('uri', '')
method = request.get('method', 'GET')
response['statusCode'] = 200
response['statusReason'] = 'OK'
response['headers'] = {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'}
try:
# TD sends POST body as bytes in request['data']
raw = request.get('data', request.get('body', ''))
if isinstance(raw, bytes):
raw = raw.decode('utf-8')
body = {}
if raw and isinstance(raw, str) and raw.strip():
body = json.loads(raw)
pars = request.get('pars', {})
if uri == '/api/td/server/td':
response['data'] = json.dumps({
'version': str(app.version),
'osName': sys.platform,
'apiVersion': '1.4.3',
'product': 'TouchDesigner'
})
elif uri == '/api/td/server/exec':
script = body.get('script', '')
old_stdout = sys.stdout
sys.stdout = buf = io.StringIO()
result_val = None
err_text = ''
try:
globs = {'op': op, 'ops': ops, 'me': webServerDAT, 'parent': parent,
'project': project, 'td': td, 'result': None,
'app': app, 'absTime': absTime}
lines = script.strip().split('\n')
if len(lines) == 1:
try:
result_val = eval(script, globs)
except SyntaxError:
exec(script, globs)
result_val = globs.get('result')
else:
exec(script, globs)
result_val = globs.get('result')
except Exception as e:
err_text = traceback.format_exc()
finally:
captured = buf.getvalue()
sys.stdout = old_stdout
response['data'] = json.dumps({
'result': _serialize(result_val),
'stdout': captured,
'stderr': err_text
})
elif uri == '/api/nodes':
pp = pars.get('parentPath', ['/project1'])[0]
p = op(pp)
nodes = []
if p:
for c in p.children:
nodes.append({'name': c.name, 'path': c.path,
'opType': c.OPType, 'family': c.family})
response['data'] = json.dumps({'data': nodes})
elif uri == '/api/nodes/errors':
np = pars.get('nodePath', ['/project1'])[0]
n = op(np)
errors = []
if n:
def _collect(node, depth=0):
if depth > 10: return
e = node.errors()
if e:
errors.append({'nodePath': node.path, 'nodeName': node.name,
'opType': node.OPType, 'errors': str(e)})
if hasattr(node, 'children'):
for c in node.children: _collect(c, depth+1)
_collect(n)
response['data'] = json.dumps({'data': errors, 'hasErrors': len(errors)>0,
'errorCount': len(errors)})
else:
response['statusCode'] = 404
response['data'] = json.dumps({'error': 'Unknown: ' + uri})
except Exception as e:
response['statusCode'] = 500
response['data'] = json.dumps({'error': str(e), 'trace': traceback.format_exc()})
return response
def _serialize(v):
if v is None: return None
if isinstance(v, (int, float, bool, str)): return v
if isinstance(v, (list, tuple)): return [_serialize(i) for i in v]
if isinstance(v, dict): return {str(k): _serialize(vv) for k, vv in v.items()}
return str(v)
'''
# Point webserver to callback
ws.par.callbacks = cb.path
print("Custom API server created on port 9981")
print("Test: curl http://127.0.0.1:9981/api/td/server/td")