Value shapes
Godot values cross the bridge as JSON. The coercion lives in one addon helper (type_coerce.gd) — never inline in a handler — and the same shapes work for setting properties, batch values, and tool params.
Accepted input shapes (JSON → Godot)
| Godot type | Accepted JSON |
|---|---|
Vector2 / Vector3 | {"x":1,"y":2} · [1,2] · "Vector2(100, 200)" |
Vector4 | {"x":…,"y":…,"z":…,"w":…} · [1,2,3,4] · "Vector4(1, 2, 3, 4)" |
Color | {"r":1,"g":0,"b":0,"a":1} · "#ff0000" |
Rect2 | {"position":{…},"size":{…}} |
Quaternion | {"x":…,"y":…,"z":…,"w":…} · "Quaternion(…)" |
AABB | {"position":{x,y,z},"size":{x,y,z}} · "AABB(…)" |
Plane | {"normal":{x,y,z},"d":…} · "Plane(…)" |
Basis | {"x":{x,y,z},"y":{…},"z":{…}} · "Basis(…)" |
Transform2D | {"x":{x,y},"y":{x,y},"origin":{x,y}} · "Transform2D(…)" |
Transform3D | {"basis":{…},"origin":{x,y,z}} · "Transform3D(…)" |
Projection | {"x":{x,y,z,w},"y":{…},"z":{…},"w":{…}} · "Projection(…)" |
PackedByteArray | [0,1,255] |
PackedVector2Array / PackedVector3Array / PackedColorArray | [{…}] (element shape as above) |
NodePath / StringName | a string |
| Primitives | as-is (true, 3, "text") |
| Non-finite floats | "__nan__" · "__inf__" · "__-inf__" (see below) |
| Object-typed properties | "res://path" or "uid://…" (loaded via ResourceLoader), or null to clear |
Example:
// set_node_property(node_path="Player", property="modulate", value="#ff8800")
{ "id": "7", "command": "cmd_set_node_property", "params": {
"node_path": "Player", "property": "modulate", "value": "#ff8800" } }Output shapes (Godot → JSON)
The same coercion runs in reverse for every result: a Vector2 comes back as {"x":…, "y":…}, a Color as {"r":…, "g":…, "b":…, "a":…}, a Transform3D as {"basis":{…},"origin":{…}}, and so on (the full set is the "Accepted JSON" table above). Everything that crosses the bridge is JSON-safe — no Godot objects, ever (serialized trees are {name, type, path, script, children} dicts; large trees support max_depth). The runtime probe reuses this same module (type_coerce.gd), so the editor and the running game emit identical shapes — there is no second coercion implementation.
Non-finite floats
JSON has no literal for NaN/INF, and JSON.stringify maps them lossily (NaN→null, INF→1e99999). They are therefore encoded as explicit string sentinels — "__nan__", "__inf__", "__-inf__" — in both directions, so a round-trip is faithful (#641).
Writes are verified by read-back
set_node_property coerces the value to the property's declared type, applies it, then compares the value read back to the coerced value; a mismatch fails with a structured VALIDATION_ERROR naming the expected type. A value the engine silently ignores (an unsupported type, a dimension mismatch) can no longer come back as the old value under set: true (#641). Group / category / subgroup headings are rejected as properties (they have no backing storage).
Coercion rules the addon follows
- Coerce to the property's declared type when it already exists (a vector dict becomes a
Vector2, a string form is parsed). - Unknown keys are refused, not defaulted — a malformed value is a
VALIDATION_ERRORnaming the property, never a silent zero (which would target the wrong cell/property). - Type-check up front for polymorphic setters:
setup_collisionrejects a 2D shape on a 3D body (and vice versa) with a dimension-naming hint, instead of shipping collisionless levels behindok: true.
Where it lives
- Addon:
godot/addons/godot_mcp/type_coerce.gd(to_json/from_json) - Server:
mcp_server/coercion_middleware.py(per-call param coercion) - Full per-property table:
docs/tool-contracts.md§ value-shapes
The type-coercion logic is deliberately single-sourced — a copy-pasted coercion layer (or one living inline in a handler) is a drift bug the addon rules call out as blocking, and would drift from this table within weeks. It is pinned by a headless behavior test (godot/tests/type_coerce_smoke.gd, wired into pytest via tests/integration/test_addon_read_smokes.py) that round-trips every shape in this document against the real Godot runtime.