@beremaran/godot-agent-loop 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +925 -0
  3. package/addons/godot_agent_loop/LICENSE +23 -0
  4. package/addons/godot_agent_loop/README.md +31 -0
  5. package/addons/godot_agent_loop/plugin.cfg +8 -0
  6. package/addons/godot_agent_loop/plugin.gd +417 -0
  7. package/agent-plugin/.claude-plugin/plugin.json +19 -0
  8. package/agent-plugin/.codex-plugin/plugin.json +37 -0
  9. package/agent-plugin/.mcp.json +14 -0
  10. package/agent-plugin/adapter-manifest.json +45 -0
  11. package/agent-plugin/pi/extension.ts +136 -0
  12. package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
  13. package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
  14. package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
  15. package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
  16. package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
  17. package/build/authoring-session-manager.js +237 -0
  18. package/build/domain-tool-registries.js +207 -0
  19. package/build/editor-bridge-protocol.js +1 -0
  20. package/build/editor-connection.js +112 -0
  21. package/build/editor-mutation-guard.js +30 -0
  22. package/build/editor-plugin-installer.js +220 -0
  23. package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
  24. package/build/game-command-service.js +49 -0
  25. package/build/game-connection.js +337 -0
  26. package/build/godot-executable.js +156 -0
  27. package/build/godot-process-manager.js +112 -0
  28. package/build/godot-subprocess.js +23 -0
  29. package/build/headless-operation-runner.js +68 -0
  30. package/build/headless-operation-service.js +65 -0
  31. package/build/index.js +478 -0
  32. package/build/interaction-server-installer.js +234 -0
  33. package/build/opencode-setup.js +199 -0
  34. package/build/project-support.js +219 -0
  35. package/build/runtime-protocol.js +202 -0
  36. package/build/scripts/godot_operations.gd +1534 -0
  37. package/build/scripts/mcp_editor_plugin.gd +417 -0
  38. package/build/scripts/mcp_interaction_server.gd +1255 -0
  39. package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
  40. package/build/scripts/mcp_runtime/command_params.gd +339 -0
  41. package/build/scripts/mcp_runtime/core_domain.gd +591 -0
  42. package/build/scripts/mcp_runtime/input_domain.gd +695 -0
  43. package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
  44. package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
  45. package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
  46. package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
  47. package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
  48. package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
  49. package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
  50. package/build/scripts/mcp_runtime/system_domain.gd +277 -0
  51. package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
  52. package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
  53. package/build/scripts/validate_script.gd +28 -0
  54. package/build/server-instructions.js +11 -0
  55. package/build/session-timing.js +20 -0
  56. package/build/tool-argument-validation.js +120 -0
  57. package/build/tool-definitions.js +2727 -0
  58. package/build/tool-handlers/game-tool-handlers.js +1345 -0
  59. package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
  60. package/build/tool-handlers/project-handler-services.js +1458 -0
  61. package/build/tool-handlers/project-tool-handlers.js +1506 -0
  62. package/build/tool-handlers/visual-regression-service.js +139 -0
  63. package/build/tool-manifest.js +1193 -0
  64. package/build/tool-mutation-policy.js +122 -0
  65. package/build/tool-registry.js +34 -0
  66. package/build/tool-surface.js +70 -0
  67. package/build/utils.js +273 -0
  68. package/package.json +110 -0
  69. package/product.json +52 -0
  70. package/scripts/prepare-package.js +42 -0
@@ -0,0 +1,335 @@
1
+ extends RefCounted
2
+
3
+ # Typed boundary between Godot Variants and values accepted by JSON.stringify().
4
+ # A codec instance is shared by the server and its domains. Runtime commands are
5
+ # serialized, so the small last-error slot is never observed concurrently.
6
+
7
+ const CommandParams = preload("res://mcp_runtime/command_params.gd")
8
+
9
+ const SUPPORTED_TYPE_HINTS: Array[String] = [
10
+ "", "String", "Vector2", "Vector2i", "Vector3", "Vector3i", "Color",
11
+ "Quaternion", "Rect2", "AABB", "Basis", "Transform3D", "Transform2D",
12
+ ]
13
+
14
+ var max_depth: int
15
+ var max_collection_items: int
16
+ var _last_error: Dictionary = {}
17
+
18
+
19
+ func _init(depth_limit: int = 32, collection_limit: int = 1024) -> void:
20
+ max_depth = depth_limit
21
+ max_collection_items = collection_limit
22
+
23
+
24
+ func configure(depth_limit: int, collection_limit: int) -> void:
25
+ max_depth = depth_limit
26
+ max_collection_items = collection_limit
27
+
28
+
29
+ func take_error() -> Dictionary:
30
+ var error: Dictionary = _last_error
31
+ _last_error = {}
32
+ return error
33
+
34
+
35
+ func encode(value: Variant) -> Variant:
36
+ _last_error = {}
37
+ return _encode(value, 0, [])
38
+
39
+
40
+ func _fail(reason: String, message: String, details: Dictionary = {}) -> Variant:
41
+ if _last_error.is_empty():
42
+ _last_error = {"reason": reason, "message": message}
43
+ _last_error.merge(details)
44
+ return null
45
+
46
+
47
+ func _encode(value: Variant, depth: int, ancestors: Array) -> Variant:
48
+ if depth > max_depth:
49
+ return _fail("codec_depth_exceeded", "Variant nesting exceeds the configured limit", {"max_depth": max_depth})
50
+ if value == null or value is bool or value is int or value is float or value is String:
51
+ return value
52
+ # Each math type is narrowed to a typed local before its members are read, so
53
+ # an integer vector still encodes its members as integers.
54
+ if value is Vector2:
55
+ var vector2: Vector2 = value
56
+ return {"x": vector2.x, "y": vector2.y}
57
+ if value is Vector2i:
58
+ var vector2i: Vector2i = value
59
+ return {"x": vector2i.x, "y": vector2i.y}
60
+ if value is Vector3:
61
+ var vector3: Vector3 = value
62
+ return {"x": vector3.x, "y": vector3.y, "z": vector3.z}
63
+ if value is Vector3i:
64
+ var vector3i: Vector3i = value
65
+ return {"x": vector3i.x, "y": vector3i.y, "z": vector3i.z}
66
+ if value is Color:
67
+ var color: Color = value
68
+ return {"r": color.r, "g": color.g, "b": color.b, "a": color.a}
69
+ if value is Quaternion:
70
+ var quaternion: Quaternion = value
71
+ return {"x": quaternion.x, "y": quaternion.y, "z": quaternion.z, "w": quaternion.w}
72
+ if value is Basis:
73
+ var basis_value: Basis = value
74
+ var basis_x: Variant = _encode_child(basis_value.x, depth + 1, ancestors)
75
+ if not _last_error.is_empty(): return null
76
+ var basis_y: Variant = _encode_child(basis_value.y, depth + 1, ancestors)
77
+ if not _last_error.is_empty(): return null
78
+ var basis_z: Variant = _encode_child(basis_value.z, depth + 1, ancestors)
79
+ if not _last_error.is_empty(): return null
80
+ return {"x": basis_x, "y": basis_y, "z": basis_z}
81
+ if value is Transform3D:
82
+ var transform3d: Transform3D = value
83
+ var basis: Variant = _encode_child(transform3d.basis, depth + 1, ancestors)
84
+ if not _last_error.is_empty(): return null
85
+ var origin: Variant = _encode_child(transform3d.origin, depth + 1, ancestors)
86
+ if not _last_error.is_empty(): return null
87
+ return {"basis": basis, "origin": origin}
88
+ if value is Transform2D:
89
+ var transform2d: Transform2D = value
90
+ var x_axis: Variant = _encode_child(transform2d.x, depth + 1, ancestors)
91
+ if not _last_error.is_empty(): return null
92
+ var y_axis: Variant = _encode_child(transform2d.y, depth + 1, ancestors)
93
+ if not _last_error.is_empty(): return null
94
+ var transform_origin: Variant = _encode_child(transform2d.origin, depth + 1, ancestors)
95
+ if not _last_error.is_empty(): return null
96
+ return {"x": x_axis, "y": y_axis, "origin": transform_origin}
97
+ if value is Rect2:
98
+ var rect: Rect2 = value
99
+ var rect_position: Variant = _encode_child(rect.position, depth + 1, ancestors)
100
+ if not _last_error.is_empty(): return null
101
+ var rect_size: Variant = _encode_child(rect.size, depth + 1, ancestors)
102
+ if not _last_error.is_empty(): return null
103
+ return {"position": rect_position, "size": rect_size}
104
+ if value is AABB:
105
+ var aabb: AABB = value
106
+ var aabb_position: Variant = _encode_child(aabb.position, depth + 1, ancestors)
107
+ if not _last_error.is_empty(): return null
108
+ var aabb_size: Variant = _encode_child(aabb.size, depth + 1, ancestors)
109
+ if not _last_error.is_empty(): return null
110
+ return {"position": aabb_position, "size": aabb_size}
111
+ if value is NodePath or value is StringName:
112
+ return str(value)
113
+ if _is_packed_array(value):
114
+ # Bounded before the copy, so an oversized packed array is rejected
115
+ # without materializing it.
116
+ var packed_size: int = _packed_size(value)
117
+ if packed_size > max_collection_items:
118
+ return _fail("codec_collection_exceeded", "Packed array exceeds the configured limit", {"max_collection_items": max_collection_items})
119
+ var items: Array = _packed_to_array(value)
120
+ if value is PackedVector2Array or value is PackedVector3Array or value is PackedColorArray:
121
+ var packed_result: Array = []
122
+ for item: Variant in items:
123
+ packed_result.append(_encode_child(item, depth + 1, ancestors))
124
+ if not _last_error.is_empty(): return null
125
+ return packed_result
126
+ return items
127
+ if value is Array or value is Dictionary:
128
+ for ancestor: Variant in ancestors:
129
+ if is_same(ancestor, value):
130
+ return _fail("codec_cycle", "Cyclic arrays and dictionaries cannot be encoded")
131
+ var next_ancestors: Array = ancestors.duplicate()
132
+ next_ancestors.append(value)
133
+ if value is Array:
134
+ var source_array: Array = value
135
+ if source_array.size() > max_collection_items:
136
+ return _fail("codec_collection_exceeded", "Variant collection exceeds the configured limit", {"max_collection_items": max_collection_items})
137
+ var array_result: Array = []
138
+ for item: Variant in source_array:
139
+ array_result.append(_encode_child(item, depth + 1, next_ancestors))
140
+ if not _last_error.is_empty(): return null
141
+ return array_result
142
+ var source_dictionary: Dictionary = value
143
+ if source_dictionary.size() > max_collection_items:
144
+ return _fail("codec_collection_exceeded", "Variant collection exceeds the configured limit", {"max_collection_items": max_collection_items})
145
+ var dictionary_result: Dictionary = {}
146
+ for key: Variant in source_dictionary:
147
+ dictionary_result[str(key)] = _encode_child(source_dictionary[key], depth + 1, next_ancestors)
148
+ if not _last_error.is_empty(): return null
149
+ return dictionary_result
150
+ if value is Node:
151
+ var node: Node = value
152
+ return {"_type": "Node", "class": node.get_class(), "name": node.name, "path": str(node.get_path())}
153
+ if value is Resource:
154
+ var resource: Resource = value
155
+ return {"_type": "Resource", "class": resource.get_class(), "path": resource.resource_path}
156
+ if value is Object:
157
+ var object: Object = value
158
+ return {"_type": "Object", "class": object.get_class(), "id": object.get_instance_id()}
159
+ return _fail("unsupported_variant", "Variant type is not supported by the runtime codec", {"variant_type": type_string(typeof(value))})
160
+
161
+
162
+ func _encode_child(value: Variant, depth: int, ancestors: Array) -> Variant:
163
+ var encoded: Variant = _encode(value, depth, ancestors)
164
+ return null if not _last_error.is_empty() else encoded
165
+
166
+
167
+ # --- Packed arrays ---
168
+ # The packed array types share no common static type, so each is narrowed to a
169
+ # typed local before it is measured or copied.
170
+
171
+ const PACKED_ARRAY_TYPES: Array[int] = [
172
+ TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY,
173
+ TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY,
174
+ TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, TYPE_PACKED_COLOR_ARRAY,
175
+ ]
176
+
177
+
178
+ func _is_packed_array(value: Variant) -> bool:
179
+ return PACKED_ARRAY_TYPES.has(typeof(value))
180
+
181
+
182
+ func _packed_size(value: Variant) -> int:
183
+ if value is PackedByteArray:
184
+ var packed: PackedByteArray = value
185
+ return packed.size()
186
+ if value is PackedInt32Array:
187
+ var packed: PackedInt32Array = value
188
+ return packed.size()
189
+ if value is PackedInt64Array:
190
+ var packed: PackedInt64Array = value
191
+ return packed.size()
192
+ if value is PackedFloat32Array:
193
+ var packed: PackedFloat32Array = value
194
+ return packed.size()
195
+ if value is PackedFloat64Array:
196
+ var packed: PackedFloat64Array = value
197
+ return packed.size()
198
+ if value is PackedStringArray:
199
+ var packed: PackedStringArray = value
200
+ return packed.size()
201
+ if value is PackedVector2Array:
202
+ var packed: PackedVector2Array = value
203
+ return packed.size()
204
+ if value is PackedVector3Array:
205
+ var packed: PackedVector3Array = value
206
+ return packed.size()
207
+ if value is PackedColorArray:
208
+ var packed: PackedColorArray = value
209
+ return packed.size()
210
+ return -1
211
+
212
+
213
+ func _packed_to_array(value: Variant) -> Array:
214
+ if value is PackedByteArray:
215
+ var packed: PackedByteArray = value
216
+ return Array(packed)
217
+ if value is PackedInt32Array:
218
+ var packed: PackedInt32Array = value
219
+ return Array(packed)
220
+ if value is PackedInt64Array:
221
+ var packed: PackedInt64Array = value
222
+ return Array(packed)
223
+ if value is PackedFloat32Array:
224
+ var packed: PackedFloat32Array = value
225
+ return Array(packed)
226
+ if value is PackedFloat64Array:
227
+ var packed: PackedFloat64Array = value
228
+ return Array(packed)
229
+ if value is PackedStringArray:
230
+ var packed: PackedStringArray = value
231
+ return Array(packed)
232
+ if value is PackedVector2Array:
233
+ var packed: PackedVector2Array = value
234
+ return Array(packed)
235
+ if value is PackedVector3Array:
236
+ var packed: PackedVector3Array = value
237
+ return Array(packed)
238
+ if value is PackedColorArray:
239
+ var packed: PackedColorArray = value
240
+ return Array(packed)
241
+ return []
242
+
243
+
244
+ func decode(value: Variant, type_hint: String = "") -> Variant:
245
+ _last_error = {}
246
+ if not type_hint in SUPPORTED_TYPE_HINTS:
247
+ return _fail("invalid_type_hint", "Unknown Variant type hint", {"type_hint": type_hint, "allowed": SUPPORTED_TYPE_HINTS})
248
+ return _decode(value, type_hint, 0)
249
+
250
+
251
+ func _decode(value: Variant, type_hint: String, depth: int) -> Variant:
252
+ if depth > max_depth:
253
+ return _fail("codec_depth_exceeded", "Variant nesting exceeds the configured limit", {"max_depth": max_depth})
254
+ if value is String and type_hint != "" and type_hint != "String":
255
+ var text: String = value
256
+ var trimmed: String = text.strip_edges()
257
+ if trimmed.begins_with("{") or trimmed.begins_with("["):
258
+ var parsed: Variant = JSON.parse_string(trimmed)
259
+ if parsed != null:
260
+ value = parsed
261
+ if not value is Dictionary:
262
+ return value
263
+ var dict: Dictionary = value
264
+ match type_hint:
265
+ "Vector2": return _vector2(dict)
266
+ "Vector2i": return Vector2i(CommandParams.json_int(dict, "x"), CommandParams.json_int(dict, "y"))
267
+ "Vector3": return _vector3(dict)
268
+ "Vector3i": return Vector3i(CommandParams.json_int(dict, "x"), CommandParams.json_int(dict, "y"), CommandParams.json_int(dict, "z"))
269
+ "Color": return Color(CommandParams.json_float(dict, "r"), CommandParams.json_float(dict, "g"), CommandParams.json_float(dict, "b"), CommandParams.json_float(dict, "a", 1.0))
270
+ "Quaternion": return Quaternion(CommandParams.json_float(dict, "x"), CommandParams.json_float(dict, "y"), CommandParams.json_float(dict, "z"), CommandParams.json_float(dict, "w", 1.0))
271
+ "Rect2":
272
+ var rect_position: Vector2 = _vector2(CommandParams.json_dictionary(dict, "position"))
273
+ var rect_size: Vector2 = _vector2(CommandParams.json_dictionary(dict, "size"))
274
+ return Rect2(rect_position, rect_size)
275
+ "AABB":
276
+ var aabb_position: Vector3 = _vector3(CommandParams.json_dictionary(dict, "position"))
277
+ var aabb_size: Vector3 = _vector3(CommandParams.json_dictionary(dict, "size"))
278
+ return AABB(aabb_position, aabb_size)
279
+ "Basis":
280
+ return Basis(
281
+ _vector3(_member_or(dict, "x", {"x": 1})),
282
+ _vector3(_member_or(dict, "y", {"y": 1})),
283
+ _vector3(_member_or(dict, "z", {"z": 1})),
284
+ )
285
+ "Transform3D":
286
+ var basis: Basis = _decode(CommandParams.json_dictionary(dict, "basis"), "Basis", depth + 1)
287
+ return Transform3D(basis, _vector3(CommandParams.json_dictionary(dict, "origin")))
288
+ "Transform2D":
289
+ return Transform2D(
290
+ _vector2(_member_or(dict, "x", {"x": 1})),
291
+ _vector2(_member_or(dict, "y", {"y": 1})),
292
+ _vector2(CommandParams.json_dictionary(dict, "origin")),
293
+ )
294
+ if dict.has("basis") and dict.has("origin"): return _decode(dict, "Transform3D", depth + 1)
295
+ if dict.has("r") and dict.has("g") and dict.has("b"): return _decode(dict, "Color", depth + 1)
296
+ if dict.has("x") and dict.has("y") and dict.has("z") and dict.has("w"): return _decode(dict, "Quaternion", depth + 1)
297
+ if dict.has("position") and dict.has("size"):
298
+ var position: Dictionary = CommandParams.json_dictionary(dict, "position")
299
+ var size: Dictionary = CommandParams.json_dictionary(dict, "size")
300
+ return _decode(dict, "AABB" if position.has("z") or size.has("z") else "Rect2", depth + 1)
301
+ if dict.has("x") and dict.has("y") and dict.has("z"): return _decode(dict, "Vector3", depth + 1)
302
+ if dict.has("x") and dict.has("y") and dict.size() == 2: return _decode(dict, "Vector2", depth + 1)
303
+ return value
304
+
305
+
306
+ func decode_for_property(object: Object, property: String, value: Variant) -> Variant:
307
+ for property_info: Dictionary in object.get_property_list():
308
+ if property_info.get("name", "") != property:
309
+ continue
310
+ var hints: Dictionary = {TYPE_VECTOR2: "Vector2", TYPE_VECTOR2I: "Vector2i", TYPE_VECTOR3: "Vector3", TYPE_VECTOR3I: "Vector3i", TYPE_COLOR: "Color", TYPE_QUATERNION: "Quaternion", TYPE_RECT2: "Rect2", TYPE_AABB: "AABB", TYPE_BASIS: "Basis", TYPE_TRANSFORM3D: "Transform3D", TYPE_TRANSFORM2D: "Transform2D"}
311
+ var type_id: int = property_info.get("type", TYPE_NIL)
312
+ if hints.has(type_id):
313
+ var hint: String = hints[type_id]
314
+ return decode(value, hint)
315
+ if type_id == TYPE_BOOL: return CommandParams.to_bool(value)
316
+ if type_id == TYPE_INT: return CommandParams.to_int(value)
317
+ if type_id == TYPE_FLOAT: return CommandParams.to_float(value)
318
+ break
319
+ return decode(value)
320
+
321
+
322
+ # A member that is only defaulted when the key is absent; a present object still
323
+ # defaults its own missing members to zero.
324
+ func _member_or(source: Dictionary, key: String, default_value: Dictionary) -> Dictionary:
325
+ if not source.has(key):
326
+ return default_value
327
+ return CommandParams.json_dictionary(source, key)
328
+
329
+
330
+ func _vector2(value: Dictionary) -> Vector2:
331
+ return Vector2(CommandParams.json_float(value, "x"), CommandParams.json_float(value, "y"))
332
+
333
+
334
+ func _vector3(value: Dictionary) -> Vector3:
335
+ return Vector3(CommandParams.json_float(value, "x"), CommandParams.json_float(value, "y"), CommandParams.json_float(value, "z"))
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env -S godot --headless --script
2
+ extends SceneTree
3
+ ## Compiles one project GDScript after project autoloads are registered.
4
+ ##
5
+ ## Godot's `--check-only --script <target>` path compiles the target before
6
+ ## Main registers every project autoload singleton, which can report valid
7
+ ## singleton references as unknown identifiers. SceneTree `_initialize()` runs
8
+ ## after autoload bootstrap. CACHE_MODE_IGNORE still forces a fresh parse,
9
+ ## analysis, and compile instead of accepting a resource cached by bootstrap.
10
+
11
+
12
+ func _initialize() -> void:
13
+ var args: PackedStringArray = OS.get_cmdline_args()
14
+ var script_index: int = args.find("--script")
15
+ var target_index: int = script_index + 2
16
+
17
+ if script_index == -1 or args.size() <= target_index:
18
+ printerr("SCRIPT ERROR: usage: godot --headless --path <project> --script validate_script.gd <res://path/to/script.gd>")
19
+ quit(1)
20
+ return
21
+
22
+ var target: String = args[target_index]
23
+ var _loaded_script: Resource = ResourceLoader.load(
24
+ target,
25
+ "GDScript",
26
+ ResourceLoader.CACHE_MODE_IGNORE,
27
+ )
28
+ quit()
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Initialization guidance is paid for in every MCP session. Keep this to the
3
+ * durable operating method; detailed procedures belong in plugin skills.
4
+ */
5
+ export const SERVER_INSTRUCTIONS = `Build and verify Godot changes with an author → run → observe → assert loop.
6
+
7
+ 1. Author scenes, scripts, resources, and project settings with project tools.
8
+ 2. Run with run_project, then observe through game_get_scene_tree, game_get_ui, game_screenshot, and get_debug_output.
9
+ 3. Assert outcomes with verify_project or run_project_tests instead of assembling fragile manual checks. Stop the project when finished.
10
+
11
+ Prefer compound tools when they cover the task. Runtime bridge injection and cleanup are automatic; do not add MCP autoloads or addon files to the project. Reflection, code execution, and network tools are denied by default. For trusted local work, enable only required groups with GODOT_MCP_PRIVILEGED_GROUPS (reflection, code-execution, network), or explicitly enable all with GODOT_MCP_ALLOW_PRIVILEGED_COMMANDS=true.`;
@@ -0,0 +1,20 @@
1
+ /** Deterministic frame delta used by every MCP-owned long-running Godot process. */
2
+ export const GODOT_SESSION_FIXED_FPS = 60;
3
+ export const GODOT_SESSION_INITIAL_TIME_SCALE = 1;
4
+ export const GODOT_SESSION_FIXED_FPS_ENV = 'GODOT_MCP_FIXED_FPS';
5
+ /**
6
+ * Fixed FPS makes each rendered frame advance the same simulation delta.
7
+ * max-fps retains a wall-clock cap so an idle persistent session does not spin
8
+ * as fast as the CPU allows, and time-scale establishes a known reset value.
9
+ */
10
+ export function deterministicSessionArguments() {
11
+ return [
12
+ '--fixed-fps', String(GODOT_SESSION_FIXED_FPS),
13
+ '--max-fps', String(GODOT_SESSION_FIXED_FPS),
14
+ '--time-scale', String(GODOT_SESSION_INITIAL_TIME_SCALE),
15
+ ];
16
+ }
17
+ /** Metadata mirrored into the child because Godot strips recognized CLI flags from script-visible args. */
18
+ export function deterministicSessionEnvironment() {
19
+ return { [GODOT_SESSION_FIXED_FPS_ENV]: String(GODOT_SESSION_FIXED_FPS) };
20
+ }
@@ -0,0 +1,120 @@
1
+ /** Thrown when a tool call does not match its advertised input schema. */
2
+ export class ToolArgumentValidationError extends Error {
3
+ toolName;
4
+ issues;
5
+ constructor(toolName, issues) {
6
+ super(`Invalid arguments for ${toolName}: ${issues.join('; ')}`);
7
+ this.toolName = toolName;
8
+ this.issues = issues;
9
+ this.name = 'ToolArgumentValidationError';
10
+ }
11
+ }
12
+ /**
13
+ * Parses untrusted MCP arguments using the same schema advertised in list_tools.
14
+ * The supported JSON Schema subset intentionally matches the project tool
15
+ * definitions: object properties, required fields, primitive types, enums,
16
+ * string patterns, numeric/array bounds, and homogeneous arrays. Objects
17
+ * without declared properties remain free-form so tools such as `properties`
18
+ * can carry Godot values.
19
+ */
20
+ export function parseToolArguments(tool, value) {
21
+ const issues = [];
22
+ validate(value ?? {}, tool.inputSchema, 'arguments', issues, true);
23
+ if (issues.length > 0) {
24
+ throw new ToolArgumentValidationError(tool.name, issues);
25
+ }
26
+ return value;
27
+ }
28
+ export function createToolArgumentParser(definitions) {
29
+ const definitionsByName = new Map(definitions.map(definition => [definition.name, definition]));
30
+ return (name, value) => {
31
+ const definition = definitionsByName.get(name);
32
+ if (!definition)
33
+ throw new Error(`No input schema registered for tool: ${name}`);
34
+ return parseToolArguments(definition, value);
35
+ };
36
+ }
37
+ function validate(value, schema, path, issues, rejectUnknownProperties = false) {
38
+ if (schema.oneOf) {
39
+ const matchingBranches = schema.oneOf.filter(branch => {
40
+ const branchIssues = [];
41
+ validate(value, branch, path, branchIssues);
42
+ return branchIssues.length === 0;
43
+ });
44
+ if (matchingBranches.length !== 1)
45
+ issues.push(`${path} must match exactly one allowed schema`);
46
+ return;
47
+ }
48
+ if (!matchesType(value, schema.type)) {
49
+ issues.push(`${path} must be ${schema.type}`);
50
+ return;
51
+ }
52
+ if (schema.enum && !schema.enum.includes(value)) {
53
+ issues.push(`${path} must be one of: ${schema.enum.join(', ')}`);
54
+ }
55
+ if (schema.pattern && typeof value === 'string' && !new RegExp(schema.pattern).test(value)) {
56
+ issues.push(`${path} must match: ${schema.pattern}`);
57
+ }
58
+ if (typeof value === 'string') {
59
+ if (schema.minLength !== undefined && value.length < schema.minLength) {
60
+ issues.push(`${path} must contain at least ${schema.minLength} characters`);
61
+ }
62
+ if (schema.maxLength !== undefined && value.length > schema.maxLength) {
63
+ issues.push(`${path} must contain at most ${schema.maxLength} characters`);
64
+ }
65
+ }
66
+ if (typeof value === 'number') {
67
+ if (schema.minimum !== undefined && value < schema.minimum) {
68
+ issues.push(`${path} must be at least ${schema.minimum}`);
69
+ }
70
+ if (schema.maximum !== undefined && value > schema.maximum) {
71
+ issues.push(`${path} must be at most ${schema.maximum}`);
72
+ }
73
+ }
74
+ if (schema.type === 'object' && isRecord(value)) {
75
+ const properties = schema.properties;
76
+ for (const required of schema.required ?? []) {
77
+ if (!(required in value))
78
+ issues.push(`${path}.${required} is required`);
79
+ }
80
+ if (properties) {
81
+ for (const [key, propertyValue] of Object.entries(value)) {
82
+ const propertySchema = properties[key];
83
+ if (!propertySchema) {
84
+ if (rejectUnknownProperties)
85
+ issues.push(`${path}.${key} is not allowed`);
86
+ continue;
87
+ }
88
+ validate(propertyValue, propertySchema, `${path}.${key}`, issues);
89
+ }
90
+ }
91
+ }
92
+ if (schema.type === 'array' && Array.isArray(value)) {
93
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
94
+ issues.push(`${path} must contain at least ${schema.minItems} items`);
95
+ }
96
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) {
97
+ issues.push(`${path} must contain at most ${schema.maxItems} items`);
98
+ }
99
+ if (schema.items) {
100
+ value.forEach((item, index) => {
101
+ validate(item, schema.items, `${path}[${index}]`, issues);
102
+ });
103
+ }
104
+ }
105
+ }
106
+ function matchesType(value, type) {
107
+ if (!type)
108
+ return true;
109
+ switch (type) {
110
+ case 'object': return isRecord(value);
111
+ case 'array': return Array.isArray(value);
112
+ case 'string': return typeof value === 'string';
113
+ case 'boolean': return typeof value === 'boolean';
114
+ case 'number': return typeof value === 'number' && Number.isFinite(value);
115
+ case 'integer': return typeof value === 'number' && Number.isInteger(value);
116
+ }
117
+ }
118
+ function isRecord(value) {
119
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
120
+ }