mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
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).
140 lines
4.9 KiB
Python
140 lines
4.9 KiB
Python
"""
|
|
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")
|