Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/davidbuenov/dbv-mcp-server/llms.txt

Use this file to discover all available pages before exploring further.

bridge.py is a lightweight stdio-to-HTTP proxy that makes Unreal Engine’s MCP server consumable by any standard MCP client — Claude Desktop, Claude Code, or any other tool that speaks JSON-RPC 2.0 over stdin/stdout. It handles session negotiation, tool discovery caching, and namespace resolution entirely transparently, so the client never needs to know about Unreal’s internal transport format.

Role in the architecture

Any MCP Client (Claude Desktop, etc.)
       │  JSON-RPC 2.0 on stdin / stdout

  bridge.py
  ┌──────────────────────────────────────────────────────────┐
  │  tools/list  → intercept, flatten all toolset tools      │
  │  tools/call  → rewrite dotted names → call_tool          │
  │  all others  → forward as-is                             │
  │  debug logs  → stderr (stdout is JSON-RPC only)          │
  └──────────────────────────┬───────────────────────────────┘
                             │ HTTP POST  (Content-Type: application/json)
                             │ SSE response (text/event-stream)

              Unreal Engine MCP Server — http://localhost:8000/mcp
bridge.py uses only Python’s standard library (http.client, json, sys, os, re, hashlib) — no external packages are needed.

Session management

Unreal’s MCP server requires a stable session identifier to be injected as an Mcp-Session-Id HTTP header after the initial handshake. bridge.py manages this transparently with a single global variable:
session_id = None
On the very first request (the initialize handshake), Unreal returns an Mcp-Session-Id response header. bridge.py stores it immediately:
mcp_sess_id = res.getheader("Mcp-Session-Id")
if mcp_sess_id:
    session_id = mcp_sess_id
    log(f"Sesión establecida / actualizada: {session_id}")
Every subsequent HTTP request injects that value automatically:
if session_id:
    headers["Mcp-Session-Id"] = session_id
The MCP client never needs to handle session headers — they are invisible at the stdio layer.

tools/list interception and toolset flattening

Unreal’s MCP server exposes tools through a multi-level toolset hierarchy. Most MCP clients expect a flat list of individually named tools. When bridge.py receives a tools/list request, it does not proxy it to Unreal — instead it intercepts it and calls get_flattened_tools():
  1. Calls list_toolsets internally to get all registered toolset names.
  2. Computes an MD5 hash of the toolset list and checks .mcp_tools_cache.json on disk.
  3. If the hash matches the cache, returns tools directly from disk (zero network calls).
  4. If not, calls describe_toolset for each toolset, extracts every tool entry, and appends them to the flat list alongside three built-in navigation tools (list_toolsets, describe_toolset, call_tool).
  5. Saves the rebuilt list to .mcp_tools_cache.json for future use.
The cache is automatically invalidated whenever the set of registered toolsets changes — no manual cache busting is needed.

tools/call translation and namespace rewriting

Unreal tool names are fully-qualified dotted paths like editor_toolset.toolsets.scene.SceneTools.SpawnActor. When an MCP client calls a tool by that name, bridge.py intercepts the tools/call request and splits the name on the last dot:
if "." in tool_name:
    parts = tool_name.rsplit(".", 1)
    if len(parts) == 2:
        toolset_name, actual_tool_name = parts  # e.g. ("editor_toolset.toolsets.scene.SceneTools", "SpawnActor")
        toolset_name = translate_toolset_name(toolset_name)
        payload["params"]["name"] = "call_tool"
        payload["params"]["arguments"] = {
            "toolset_name": toolset_name,
            "tool_name": actual_tool_name,
            "arguments": params.get("arguments", {})
        }
The same rewriting applies when a client calls call_tool or describe_toolset directly and passes a toolset_name argument — those are also passed through translate_toolset_name.

Namespace translation (translate_toolset_name)

MCP clients sometimes use short or partial toolset names (e.g. SceneTools) rather than fully-qualified names (e.g. editor_toolset.toolsets.scene.SceneTools). translate_toolset_name resolves these automatically by reading .mcp_tools_cache.json:
  1. Builds the set of all registered toolset prefixes from cached tool names (everything before the last .).
  2. Checks for an exact match first.
  3. If no exact match, performs case-insensitive suffix matching on the last segment:
requested_suffix = toolset_name.split(".")[-1].lower()   # "scenetools"
for reg in registered_toolsets:
    reg_suffix = reg.split(".")[-1].lower()
    if requested_suffix == reg_suffix:
        log(f"Autotraduciendo toolset: {toolset_name} -> {reg}")
        return reg
This means SceneTools, scenetools, and editor_toolset.toolsets.scene.SceneTools all resolve to the same fully-qualified name without any manual configuration.

SSE response handling

Unreal streams tool call responses as text/event-stream. bridge.py reads the stream line-by-line, extracts data: lines, validates the JSON, and writes a single compact line to stdout:
if "text/event-stream" in content_type:
    while True:
        line = res.readline()
        if not line:
            break
        line_str = line.decode("utf-8", errors="replace").strip()
        if line_str.startswith("data:"):
            data_payload = line_str[5:].strip()
            if data_payload:
                data_json = json.loads(data_payload)
                print(json.dumps(data_json), flush=True)   # compact single line → stdout
                break
The flush=True ensures every response is delivered to the MCP client immediately without buffering.

Main loop

The bridge reads one JSON-RPC message per line from stdin, processes it, and loops:
def main():
    log("Bridge stdio-to-http-sse iniciado correctamente.")
    while True:
        line = sys.stdin.readline()
        if not line:
            break          # stdin closed — parent process exited
        line = line.strip()
        if not line:
            continue
        log(f"Procesando comando: {line[:100]}...")
        handle_request(line)

if __name__ == "__main__":
    main()
handle_request dispatches each line to the appropriate path: interception for tools/list, translation for tools/call, or transparent proxy for everything else (including the initial initialize / notifications/initialized handshake). MCP notifications (messages without an id field) are forwarded to Unreal but their responses are consumed silently — no stdout write is performed, matching the JSON-RPC 2.0 notification contract.

Error handling

If an HTTP error or network exception occurs, bridge.py emits a well-formed JSON-RPC error response to stdout rather than crashing:
err_resp = {
    "jsonrpc": "2.0",
    "id": payload.get("id"),
    "error": {
        "code": -32603,
        "message": f"Bridge error: {str(e)}"
    }
}
print(json.dumps(err_resp), flush=True)
This keeps the MCP client in a valid protocol state even when Unreal is unreachable.
All debug output from bridge.py is written to stderr, never stdout. This is a hard requirement of the MCP stdio transport: stdout is reserved exclusively for JSON-RPC 2.0 messages. Any non-JSON byte on stdout would corrupt the protocol framing. You can view bridge logs by redirecting stderr in your terminal, or by checking the MCP server log output in Claude Desktop’s developer tools.

Build docs developers (and LLMs) love