2.17 · MCP Server (Hazel-Bridge)
A protocol-only static library that hosts JSON-RPC 2.0 tools over TCP and streamable HTTP, so anything linking the engine can expose the same wire format to local clients without dragging editor or UI code with it.
Overview
The JSON-RPC 2.0 server lives in its own static library, Hazel-Bridge, under Net/Hazel-Bridge/Source/Hazel/Bridge/ in namespace Hazel::Bridge. It links into Hazel and carries the protocol surface only — no editor, UI, or scene-mutation coupling. Anything that links Hazel can pull Hazel-Bridge in and stand up the same wire format (a future runtime or headless host would do exactly this).
Registered McpTool instances are exposed simultaneously over TCP and streamable HTTP. Both transports come up together or not at all — there is no half-started state. The HTTP side is powered by cpp-httplib, vendored at Net/Hazel-Bridge/vendor/cpp-httplib/.
Threading model
One accept thread receives connections; each accepted connection gets its own worker thread. Tools are invoked on those connection-worker threads. Any tool that needs to touch the scene, editor, or anything else with main-thread affinity must hop back via MainThreadDispatcher::Execute, which is drained by the editor's OnUpdate tick. This keeps the wire format responsive while keeping engine mutations single-threaded.
The server binds to 127.0.0.1. There is no auth layer — anything that can connect from the local box can call any registered tool. Do not expose the ports externally.
Consumers
| Consumer | Surface | Notes |
|---|---|---|
LuckyEditor-Agent (§ 2.18) |
harness/*, tool/<name> |
Registers via its HarnessBridge / ToolBridge adapters. Started by EditorAgent::Attach; ports default to 19330 / 19331 unless overridden by --mcp=PORT. |
tests/agent-eval/runner/mcp_client.py |
harness/* |
TCP client. Lower overhead for large screenshot payloads, which is why the runner stays on TCP. |
tests/agent-eval/runner/claude_driver.py |
tool/* |
Emits an --mcp-config JSON pointing at http://127.0.0.1:<http_port>/mcp so claude-code talks streamable-HTTP directly. |
The previous mcp_stdio_bridge.py Python relay was removed in #681 once streamable HTTP landed. claude-code now speaks to the engine directly.
Process-wide accessor
Hazel::Bridge::McpServer::GetGlobal() returns the live server instance (or nullptr). The editor only ever constructs one. The accessor exists so features outside the McpServer subsystem can discover the live HTTP port without plumbing a reference through the UI layers; today the only caller is LocalClaudeRunner, which needs to point its spawned claude subprocess at the right URL.
Pitfalls
Tool callbacks run on the connection worker. Anything that reads or writes ECS, NVRHI, or ImGui state must go through MainThreadDispatcher::Execute. Forgetting this looks fine at low traffic and explodes the first time two tools race the main thread.
If TCP or HTTP fails to bind, the whole server fails to start — there is no degraded mode. If you add a new transport, preserve that property.
Extending
- New tool — implement an
McpToolsubclass, register it on a liveMcpServerinstance during your subsystem's startup. The Agent'sHarnessBridgeandToolBridgeare the reference adapters. - New consumer — link
Hazel-Bridge, register your tools, let the editor (or another host) instantiate the singleton. You do not stand up a second server. - Headless host — link
Hazel+Hazel-Bridgefrom a runtime app, construct oneMcpServer, register tools, drain a main-thread queue. No editor needed.