Skip to content

Bridge & transport ​

Topology ​

The server listens, the editor connects out (#276). Both are configurable via the same env var, GODOT_MCP_BRIDGE_URL (default ws://127.0.0.1:9080) — never hard-coded in library code.

Why the editor dials:

  • The editor is the party that comes and goes; the server is a headless service (often spawned per session by the MCP client, or a long-lived Docker service).
  • Launch order never matters: the server may bind the port before Godot exists.
  • Reconnection with exponential backoff (start ~500 ms, capped) lives on the addon side — the only party with enough knowledge to survive restarts.

Framing & lifecycle ​

  • Framing: one JSON object per WebSocket text message, UTF-8. No headers.
  • One active peer: a new addon connection replaces the old one (a stale editor can't ghost the bridge).
  • Auth is opt-in (#538): localhost-only by default — the zero-config path has no handshake. Set GODOT_MCP_BRIDGE_TOKEN on both sides for shared/remote-editor setups: the addon authenticates as its first message, a mismatch is refused at the handshake, tokens are never logged. Separately, a non-loopback HTTP bind requires GODOT_MCP_AUTH_TOKEN (#226) — a fail-fast guard refuses to start otherwise.
  • cmd_ping → {pong: true} is the health check, both directions.
  • Server→addon pushes: besides commands the addon answers, the server sends a few fire-and-forget control messages the addon consumes without a waiter — cmd_server_hello (server version, #521) and cmd_toolsets_update (the enabled toolset set, #592, so the dock's Toolsets row mirrors the server). A replaced peer also receives a PEER_REPLACED notice before its link closes (#593).

Threat model ​

Loopback is reachable by more than the editor, so the listeners are hardened by default (#636):

  • A browser page. WebSockets are exempt from the same-origin policy, and DNS rebinding lets an attacker's hostname resolve to 127.0.0.1. The bridge therefore accepts only Origin-less handshakes (origins=[None]) — the Godot addon's WebSocketPeer sends no Origin, so a page is refused at the handshake; the HTTP transport's Host/Origin guard is likewise on by default (GODOT_MCP_HTTP_HOST_ORIGIN_PROTECTION=auto; foreign Host → 421, foreign Origin → 403).
  • Another local process. Any local process can connect. The bridge is localhost-only; for shared/remote setups the opt-in token (#538) is the gate, and a bad/missing token is refused before it can displace the authenticated editor (#635). A non-loopback HTTP bind still requires GODOT_MCP_AUTH_TOKEN (#226).

Timeouts, everywhere ​

A request that outlives its timeout resolves to a TIMEOUT envelope — it does not hang the agent. Concretely:

TimeoutWhereValue
Bridge requestBridge.send(command, timeout=…)per-call; tool defaults vary (3–30 s)
Toolset enable's version fetch_fetch_godot_version3 s, best-effort
HTTP transportGODOT_MCP_HTTP_* via httpx clienttool-call bound (#422)
Readiness pollspoll_ready(…, timeout_ms)per-tool (timeout_ms param)
Reconnect backoffaddon mcp_bridge.gd~500 ms start, capped

A send with no connected peer resolves to BRIDGE_DISCONNECTED immediately — it does not wait for an editor that isn't coming.

Concurrency model ​

  • Id correlation is concurrency-safe: many in-flight commands, each resolved to its own waiter keyed by id. No shared mutable state across requests.
  • Single-writer semantics: one editor per bridge. A second connection replaces the first (documented; the server is single-user local by design).
  • The server is sessionless by design (MCP 2026-07-28 shape): no per-session state, works on every transport, and enable_toolset writes a server-global set shared by all clients (see gating).
  • No blocking sync I/O inside async handlers; no import-time sockets.

Configuration ​

Env varDefaultMeaning
GODOT_MCP_BRIDGE_URLws://127.0.0.1:9080bridge listen/connect URL (both sides)
GODOT_MCP_TRANSPORTstdiostdio or http
GODOT_MCP_HTTP_HOST / _PORT127.0.0.1:9090HTTP bind
GODOT_MCP_AUTH_TOKENunsetBearer token — required for non-loopback HTTP binds (#226)
GODOT_MCP_HTTP_HOST_ORIGIN_PROTECTIONautoHTTP Host/Origin (DNS-rebinding) guard (#636) — auto protects loopback, false opts out
GODOT_MCP_BRIDGE_TOKENunsetopt-in shared secret for the bridge handshake (#538) — set on both sides; the addon authenticates as its first message; a mismatch is refused at the handshake; never logged
GODOT_MCP_PROJECT_DIRunsetexplicit project dir (else the connected editor's)

The full table lives in env-vars.

Testing the seam ​

The bridge is the seam most worth testing, so it gets two layers:

  1. Fake addon connection (tests/fakes.py): an in-memory transport that validates real envelope shapes, correlating ids, and canned responses — the entire contract suite runs with no sockets and no editor.
  2. Live e2e (tests/integration/test_*_e2e.py): a real editor on a self-hosted runner; ephemeral per-run bridge ports (#444) so concurrent jobs can't contend; the addon's reconnect behavior is exercised by dedicated tests (#276).

The bridge inversion was itself landed with a red-first e2e: the old "addon-hosts" topology hung on every server restart; the "server-listens" topology survives any restart order.

194 tools · 29 toolsets · contract version 1