Appearance
Wire protocol
Frames are JSONL: one JSON-RPC 2.0 object per \n-terminated line (in-process framing is per-message, no newline). Requests carry an id and get exactly one response; notifications don't. The engine drains the whole inbound queue in order, once per frame, on the main thread.
json
→ {"jsonrpc":"2.0","id":7,"method":"set","params":{"oid":"123","property":"text","value":"hi"}}
← {"jsonrpc":"2.0","id":7,"result":{}}
← {"jsonrpc":"2.0","id":7,"error":{"code":-32001,"message":"no such property: txet"}}Values
Godot Variants map to JSON with $-tagged wrappers for non-JSON types: {"$v2":[x,y]}, {"$v3":[x,y,z]}, {"$col":[r,g,b,a]}, {"$rect":[x,y,w,h]}, {"$oid":"…"} (object reference), {"$bytes":"base64…"}, {"$sn":"name"} (StringName), {"$np":"path"} (NodePath). Plain strings/numbers/bools/arrays/ dicts pass through.
Object handles (oid) are decimal strings — 64-bit ObjectIDs exceed JSON consumers' safe-integer range (RefCounted handles set bit 63). Treat them as opaque.
Ops
| Method | Params | Result | Notes |
|---|---|---|---|
handshake | clientInfo {name, version}, token? | protocolVersion, engineVersion, apiHash, buildFlavor, rootId, features[] | must be first when --bridge-token is set |
instantiate | className | {oid} | you own it until addChild or free |
free | oid | {} | queue_free if in tree; broadcasts objectFreed |
set | oid, property, value | {} | |
get | oid, property | {value} | |
call | oid, method, args?[] | {result} | return value under result (objects as {"$oid":…}); RefCounted returns are pinned in the registry until freed |
connect | oid, signal, once?, throttleMs? | {subId} | emissions arrive as signal notifications |
disconnect | subId | {} | |
addChild / removeChild / reparent | parent/newParent, child | {} | |
root | {oid} | SceneTree root Window (also in handshake) | |
singleton | name | {oid} | e.g. "Input", "DisplayServer" |
load | path | {oid} | load a Resource — res:// from a mounted pack, or an absolute filesystem path |
instantiateScene | path or oid | {oid} | PackedScene → instance; oid reuses a loaded scene |
queryTree | oid, depth? | {tree} | recursive {oid, class, name, children} |
batch | ops[{method, params}] | {results[]} | send many fire-and-forget ops in one message |
sync | {} | ordering barrier: everything sent before it has been applied | |
loadPack | path, replace? | {ok} | mount a .pck at runtime (absolute path — not resolved against the client's cwd) |
classes | contains?, instantiableOnly? | {classes[]} | what exists in this build |
classInfo | className, ownOnly? | {parent, instantiable, properties[], signals[], methods[]} | ClassDB reflection |
quit | code? | {} | asks the engine to exit |
Notifications (engine → client)
| Method | Params | When |
|---|---|---|
signal | subId, args[] | a subscribed signal fired (after throttleMs conflation) |
objectFreed | oid | an object you tracked was freed |
quit | code | engine is shutting down — invalidate all proxies |
SDKs surface window-close as a convenience (onWindowCloseRequested) by subscribing to the root Window's close_requested signal at connect time.
Errors
JSON-RPC standard codes plus: -32001 bad property/call, -32002 unknown or freed oid, -32003 invalid or missing token (auth), -32602 bad params / unknown class.
Feature detection
handshake.features lists optional capabilities of the runtime you actually connected to: "classes", "classInfo", "inproc", "stderrLogs". Check it rather than parsing versions; condensed builds differ.