Lucky Robots Blog Open Roles

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.

Net/Hazel-Bridge/Source/Hazel/Bridge/ namespace Hazel::Bridge StaticLib JSON-RPC 2.0 TCP + streamable HTTP Localhost only
Clients mcp_client.py harness/* via TCP claude-code --mcp-config → HTTP LocalClaudeRunner discovers via GetGlobal() Transports TCP listener port 19330 HTTP (cpp-httplib) port 19331 /mcp Hazel-Bridge McpServer 1 accept thread 1 worker / connection McpTool registry GetGlobal() Engine main thread drains queue scene / editor mutations MainThreadDispatcher::Execute Connection worker thread McpTool::Invoke(params) marshals to main thread when touching scene / UI
Both transports come up together; tools run on connection workers and hop back to the main thread for any engine mutation.

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.

Localhost-only

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

ConsumerSurfaceNotes
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.
No more stdio relay

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

Don't mutate the scene from a worker thread

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.

Both transports or neither

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 McpTool subclass, register it on a live McpServer instance during your subsystem's startup. The Agent's HarnessBridge and ToolBridge are 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-Bridge from a runtime app, construct one McpServer, register tools, drain a main-thread queue. No editor needed.