@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.
- package/LICENSE +23 -0
- package/README.md +925 -0
- package/addons/godot_agent_loop/LICENSE +23 -0
- package/addons/godot_agent_loop/README.md +31 -0
- package/addons/godot_agent_loop/plugin.cfg +8 -0
- package/addons/godot_agent_loop/plugin.gd +417 -0
- package/agent-plugin/.claude-plugin/plugin.json +19 -0
- package/agent-plugin/.codex-plugin/plugin.json +37 -0
- package/agent-plugin/.mcp.json +14 -0
- package/agent-plugin/adapter-manifest.json +45 -0
- package/agent-plugin/pi/extension.ts +136 -0
- package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
- package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
- package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
- package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
- package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
- package/build/authoring-session-manager.js +237 -0
- package/build/domain-tool-registries.js +207 -0
- package/build/editor-bridge-protocol.js +1 -0
- package/build/editor-connection.js +112 -0
- package/build/editor-mutation-guard.js +30 -0
- package/build/editor-plugin-installer.js +220 -0
- package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
- package/build/game-command-service.js +49 -0
- package/build/game-connection.js +337 -0
- package/build/godot-executable.js +156 -0
- package/build/godot-process-manager.js +112 -0
- package/build/godot-subprocess.js +23 -0
- package/build/headless-operation-runner.js +68 -0
- package/build/headless-operation-service.js +65 -0
- package/build/index.js +478 -0
- package/build/interaction-server-installer.js +234 -0
- package/build/opencode-setup.js +199 -0
- package/build/project-support.js +219 -0
- package/build/runtime-protocol.js +202 -0
- package/build/scripts/godot_operations.gd +1534 -0
- package/build/scripts/mcp_editor_plugin.gd +417 -0
- package/build/scripts/mcp_interaction_server.gd +1255 -0
- package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
- package/build/scripts/mcp_runtime/command_params.gd +339 -0
- package/build/scripts/mcp_runtime/core_domain.gd +591 -0
- package/build/scripts/mcp_runtime/input_domain.gd +695 -0
- package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
- package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
- package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
- package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
- package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
- package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
- package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
- package/build/scripts/mcp_runtime/system_domain.gd +277 -0
- package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
- package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
- package/build/scripts/validate_script.gd +28 -0
- package/build/server-instructions.js +11 -0
- package/build/session-timing.js +20 -0
- package/build/tool-argument-validation.js +120 -0
- package/build/tool-definitions.js +2727 -0
- package/build/tool-handlers/game-tool-handlers.js +1345 -0
- package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
- package/build/tool-handlers/project-handler-services.js +1458 -0
- package/build/tool-handlers/project-tool-handlers.js +1506 -0
- package/build/tool-handlers/visual-regression-service.js +139 -0
- package/build/tool-manifest.js +1193 -0
- package/build/tool-mutation-policy.js +122 -0
- package/build/tool-registry.js +34 -0
- package/build/tool-surface.js +70 -0
- package/build/utils.js +273 -0
- package/package.json +110 -0
- package/product.json +52 -0
- 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
|
+
}
|