mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-26 01:01:40 +00:00
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:
parent
c49a58a6d0
commit
7a5371b20d
9 changed files with 3277 additions and 0 deletions
140
skills/creative/touchdesigner/scripts/custom_api_handler.py
Normal file
140
skills/creative/touchdesigner/scripts/custom_api_handler.py
Normal 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")
|
||||
152
skills/creative/touchdesigner/scripts/setup.sh
Normal file
152
skills/creative/touchdesigner/scripts/setup.sh
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#!/usr/bin/env bash
|
||||
# TouchDesigner MCP Setup Verification Script
|
||||
# Checks all prerequisites and guides configuration
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
pass() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||
fail() { echo -e " ${RED}✗${NC} $1"; }
|
||||
warn() { echo -e " ${YELLOW}!${NC} $1"; }
|
||||
info() { echo -e " ${BLUE}→${NC} $1"; }
|
||||
|
||||
echo ""
|
||||
echo "TouchDesigner MCP Setup Check"
|
||||
echo "=============================="
|
||||
echo ""
|
||||
|
||||
ERRORS=0
|
||||
|
||||
# 1. Check Node.js
|
||||
echo "1. Node.js"
|
||||
if command -v node &>/dev/null; then
|
||||
NODE_VER=$(node --version 2>/dev/null || echo "unknown")
|
||||
MAJOR=$(echo "$NODE_VER" | sed 's/^v//' | cut -d. -f1)
|
||||
if [ "$MAJOR" -ge 18 ] 2>/dev/null; then
|
||||
pass "Node.js $NODE_VER (>= 18 required)"
|
||||
else
|
||||
fail "Node.js $NODE_VER (>= 18 required, please upgrade)"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
else
|
||||
fail "Node.js not found"
|
||||
info "Install: https://nodejs.org/ or 'brew install node'"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
# 2. Check npm/npx
|
||||
echo "2. npm/npx"
|
||||
if command -v npx &>/dev/null; then
|
||||
NPX_VER=$(npx --version 2>/dev/null || echo "unknown")
|
||||
pass "npx $NPX_VER"
|
||||
else
|
||||
fail "npx not found (usually comes with Node.js)"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
# 3. Check MCP Python package
|
||||
echo "3. MCP Python package"
|
||||
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
|
||||
VENV_PYTHON=""
|
||||
|
||||
# Try to find the Hermes venv Python
|
||||
if [ -f "$HERMES_HOME/hermes-agent/.venv/bin/python" ]; then
|
||||
VENV_PYTHON="$HERMES_HOME/hermes-agent/.venv/bin/python"
|
||||
elif [ -f "$HERMES_HOME/hermes-agent/venv/bin/python" ]; then
|
||||
VENV_PYTHON="$HERMES_HOME/hermes-agent/venv/bin/python"
|
||||
fi
|
||||
|
||||
if [ -n "$VENV_PYTHON" ]; then
|
||||
if $VENV_PYTHON -c "import mcp" 2>/dev/null; then
|
||||
MCP_VER=$($VENV_PYTHON -c "import importlib.metadata; print(importlib.metadata.version('mcp'))" 2>/dev/null || echo "installed")
|
||||
pass "mcp package ($MCP_VER) in Hermes venv"
|
||||
else
|
||||
fail "mcp package not installed in Hermes venv"
|
||||
info "Install: $VENV_PYTHON -m pip install mcp"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
else
|
||||
warn "Could not find Hermes venv — check mcp package manually"
|
||||
fi
|
||||
|
||||
# 4. Check TouchDesigner
|
||||
echo "4. TouchDesigner"
|
||||
TD_FOUND=false
|
||||
|
||||
# macOS
|
||||
if [ -d "/Applications/TouchDesigner.app" ]; then
|
||||
TD_FOUND=true
|
||||
pass "TouchDesigner found at /Applications/TouchDesigner.app"
|
||||
fi
|
||||
|
||||
# Linux (common install locations)
|
||||
if command -v TouchDesigner &>/dev/null; then
|
||||
TD_FOUND=true
|
||||
pass "TouchDesigner found in PATH"
|
||||
fi
|
||||
|
||||
if [ -d "$HOME/TouchDesigner" ]; then
|
||||
TD_FOUND=true
|
||||
pass "TouchDesigner found at ~/TouchDesigner"
|
||||
fi
|
||||
|
||||
if [ "$TD_FOUND" = false ]; then
|
||||
warn "TouchDesigner not detected (may be installed elsewhere)"
|
||||
info "Download from: https://derivative.ca/download"
|
||||
info "Free Non-Commercial license available"
|
||||
fi
|
||||
|
||||
# 5. Check TD WebServer DAT reachability
|
||||
echo "5. TouchDesigner WebServer DAT"
|
||||
TD_URL="${TD_API_URL:-http://127.0.0.1:9981}"
|
||||
if command -v curl &>/dev/null; then
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 3 "$TD_URL/api/td/server/td" 2>/dev/null || echo "000")
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
TD_INFO=$(curl -s --connect-timeout 3 "$TD_URL/api/td/server/td" 2>/dev/null || echo "{}")
|
||||
pass "TD WebServer DAT responding at $TD_URL"
|
||||
info "Response: $TD_INFO"
|
||||
elif [ "$HTTP_CODE" = "000" ]; then
|
||||
warn "Cannot reach TD WebServer DAT at $TD_URL"
|
||||
info "Make sure TouchDesigner is running with mcp_webserver_base.tox imported"
|
||||
else
|
||||
warn "TD WebServer DAT returned HTTP $HTTP_CODE at $TD_URL"
|
||||
fi
|
||||
else
|
||||
warn "curl not found — cannot test TD connection"
|
||||
fi
|
||||
|
||||
# 6. Check Hermes config
|
||||
echo "6. Hermes MCP config"
|
||||
CONFIG_FILE="$HERMES_HOME/config.yaml"
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
if grep -q "touchdesigner" "$CONFIG_FILE" 2>/dev/null; then
|
||||
pass "TouchDesigner MCP server configured in config.yaml"
|
||||
else
|
||||
warn "No 'touchdesigner' entry found in mcp_servers config"
|
||||
info "Add a touchdesigner entry under mcp_servers: in $CONFIG_FILE"
|
||||
info "See references/mcp-tools.md for the configuration block"
|
||||
fi
|
||||
else
|
||||
warn "No Hermes config.yaml found at $CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "=============================="
|
||||
if [ $ERRORS -eq 0 ]; then
|
||||
echo -e "${GREEN}All critical checks passed!${NC}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Open TouchDesigner and import mcp_webserver_base.tox"
|
||||
echo " 2. Add the MCP server config to Hermes (see references/mcp-tools.md)"
|
||||
echo " 3. Restart Hermes and test: 'Get TouchDesigner server info'"
|
||||
else
|
||||
echo -e "${RED}$ERRORS critical issue(s) found.${NC}"
|
||||
echo "Fix the issues above, then re-run this script."
|
||||
fi
|
||||
echo ""
|
||||
Loading…
Add table
Add a link
Reference in a new issue