Architecture¶
Dual-Thread Design¶
Envoy uses a two-thread architecture to bridge the MCP protocol with TouchDesigner's single-threaded main loop:
- Worker thread: Runs the MCP server (FastMCP with Streamable HTTP transport via uvicorn) — no TouchDesigner imports allowed
- Main thread: Executes all TD operations via
EnvoyExt._onRefresh()callback - Communication:
threading.Event+Queuefor request/response between threads - Thread management: Uses
op.TDResources.ThreadManager(TDTask pattern)
┌──────────────────┐ Event + Queue ┌──────────────────┐
│ Worker Thread │ ◄──────────────────► │ Main Thread │
│ │ │ │
│ FastMCP Server │ MCP Request ──► │ _onRefresh() │
│ (uvicorn) │ │ Execute TD ops │
│ │ ◄── TD Result │ │
│ 127.0.0.1:9870 │ │ Cook cycle │
└──────────────────┘ └──────────────────┘
Design Decisions¶
Stateless HTTP Transport¶
Envoy uses stateless_http=True because TouchDesigner's single-threaded model means concurrent sessions would queue on the same main-thread execution path anyway. Stateless mode simplifies the implementation and avoids session management overhead.
30-Second Operation Timeout¶
_execute_in_td() times out at 30 seconds. This prevents indefinite hangs if the main thread is blocked (e.g., modal dialog), while allowing enough time for heavy operations like .tox saves. If a TD operation takes longer, the MCP tool returns a timeout error — the operation may need to be broken into smaller steps.
Localhost-Only Binding¶
Envoy binds exclusively to 127.0.0.1 as a security requirement. Binding to 0.0.0.0 would expose the MCP server to the local network and enable DNS rebinding attacks from malicious websites.
Standalone Thread¶
The MCP server runs as a standalone=True TDTask because it is long-lived (runs for the entire session), unlike pool tasks which are meant for short-lived work units.
Queue-Based Communication¶
Uses threading.Event + Queue rather than locks because TD's cook cycle is frame-based — the main thread can only process requests once per frame via the RefreshHook.
Graceful Shutdown¶
The shutdown sequence ensures clean port release:
EnvoyExt.Stop()is called (from UI toggle,onExit, or project close)shutdown_event.set()signals the worker thread's uvicorn server to stop- Uvicorn completes its shutdown (stops accepting connections, drains existing)
- Worker thread's target function returns
SuccessHookorExceptHookfires on the main thread for cleanup- Port is released and available for rebinding
Thread Safety Rules¶
Critical
The worker thread (EnvoyMCPServer) must never import or call TouchDesigner modules. All TD access goes through _execute_in_td() which routes operations to the main thread.
- All TD operations execute on the main thread via the
_onRefresh()callback - The worker thread only handles HTTP/MCP protocol logic
- Data crosses the thread boundary only through the
Queueobjects op.TDResources.ThreadManagermust not be accessed from within a worker thread (triggers THREAD CONFLICT)
Error Handling¶
Envoy handles two error categories:
- Protocol errors (JSON-RPC level) — unknown tools, invalid arguments, or server errors. FastMCP handles these automatically.
- Tool execution errors — returned in tool results via
{'error': str(e)}dicts. These indicate the tool ran but encountered a problem (missing operator, invalid path, etc.).
All tool handlers validate inputs before passing to TD operations and return structured error information rather than raising exceptions.