# Your own tools and UIs Speak the wire protocol directly with a Python client and a working browser panel, each under 50 lines. The CLI is one client of a boring, stable wire: **JSON-RPC 2.0 over a local WebSocket** hosted by the editor addon. Anything that can open a WebSocket can be a Godot tool, whether that is a script, a test rig, or a web UI. Three facts cover the whole protocol: - **Find the port** in `/.godot/swallowtail.json`, which the addon writes on bind (auto range 9080-9095). - **Send** `{"jsonrpc": "2.0", "id": 1, "method": ".", "params": {…}}` as a text frame, and **match the response by `id`**. Frames without your `id` are heartbeats, so ignore them. - **Errors** come back as `{"error": {"code", "message", "data"}}` with JSON-RPC-style codes (see [How it works](https://regiellis.github.io/godot-mcp-go/docs/how-it-works)). ## A Python client ```python # pip install websockets def port(project="."): f = pathlib.Path(project) / ".godot" / "swallowtail.json" return json.loads(f.read_text())["port"] if f.exists() else 9080 async def call(method, params=None, project="."): async with websockets.connect(f"ws://127.0.0.1:{port(project)}") as ws: await ws.send(json.dumps( {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}})) async for raw in ws: msg = json.loads(raw) if msg.get("id") == 1: # heartbeat frames carry no id: skip them if "error" in msg: raise RuntimeError(msg["error"]["message"]) return msg["result"] if __name__ == "__main__": params = json.loads(sys.argv[2]) if len(sys.argv) > 2 else None print(json.dumps(asyncio.run(call(sys.argv[1], params)), indent=2)) ``` ```bash python godot.py scene.tree python godot.py node.add '{"type": "Sprite2D", "name": "Player", "parent_path": "."}' ``` ## A browser panel A WebSocket from a page needs no server and no CORS exception, so this single file is a working Godot control panel: ```html swallowtail panel


```

The exchange above is verified against a live editor. Grow it in any direction. The [live dashboard](https://regiellis.github.io/godot-mcp-go/docs/dashboard) that ships with the CLI is exactly this pattern taken further: one persistent connection polling `stats.snapshot`, an htmx UI, embedded in the Go binary.

## Or skip the socket

- **Shell out to the CLI** and parse JSON or TSV. [Scripting and CI](https://regiellis.github.io/godot-mcp-go/docs/automation) covers it.
- **Speak MCP**: `swallowtail serve` exposes every command as a typed tool for AI clients. See [Use with an AI client](https://regiellis.github.io/godot-mcp-go/docs/mcp-setup).
- **Generate from the catalog**: `engine commands --docs` returns every command with typed params, enough to build forms, bindings, or a command-palette UI without hardcoding a single command name.