@satelliteoflove/godot-mcp 2.9.0 → 2.10.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 (38) hide show
  1. package/README.md +1 -0
  2. package/addon/command_router.gd +1 -0
  3. package/addon/commands/input_commands.gd +193 -0
  4. package/addon/commands/input_commands.gd.uid +1 -0
  5. package/addon/core/mcp_debugger_plugin.gd +82 -0
  6. package/addon/game_bridge/mcp_game_bridge.gd +191 -0
  7. package/addon/plugin.cfg +1 -1
  8. package/dist/__tests__/schema.test.js +12 -42
  9. package/dist/__tests__/schema.test.js.map +1 -1
  10. package/dist/__tests__/tools/animation.test.js +53 -153
  11. package/dist/__tests__/tools/animation.test.js.map +1 -1
  12. package/dist/__tests__/tools/docs.test.js +73 -75
  13. package/dist/__tests__/tools/docs.test.js.map +1 -1
  14. package/dist/__tests__/tools/editor.test.js +71 -285
  15. package/dist/__tests__/tools/editor.test.js.map +1 -1
  16. package/dist/__tests__/tools/input.test.d.ts +2 -0
  17. package/dist/__tests__/tools/input.test.d.ts.map +1 -0
  18. package/dist/__tests__/tools/input.test.js +137 -0
  19. package/dist/__tests__/tools/input.test.js.map +1 -0
  20. package/dist/__tests__/tools/node.test.js +104 -268
  21. package/dist/__tests__/tools/node.test.js.map +1 -1
  22. package/dist/__tests__/tools/resource.test.js +44 -94
  23. package/dist/__tests__/tools/resource.test.js.map +1 -1
  24. package/dist/__tests__/tools/scene.test.js +28 -75
  25. package/dist/__tests__/tools/scene.test.js.map +1 -1
  26. package/dist/__tests__/tools/tilemap.test.js +71 -229
  27. package/dist/__tests__/tools/tilemap.test.js.map +1 -1
  28. package/dist/__tests__/utils/logger.test.js +36 -201
  29. package/dist/__tests__/utils/logger.test.js.map +1 -1
  30. package/dist/tools/index.d.ts +1 -0
  31. package/dist/tools/index.d.ts.map +1 -1
  32. package/dist/tools/index.js +3 -0
  33. package/dist/tools/index.js.map +1 -1
  34. package/dist/tools/input.d.ts +63 -0
  35. package/dist/tools/input.d.ts.map +1 -0
  36. package/dist/tools/input.js +85 -0
  37. package/dist/tools/input.js.map +1 -0
  38. package/package.json +1 -1
package/README.md CHANGED
@@ -81,6 +81,7 @@ Open your Godot project (with addon enabled), restart your AI assistant, and sta
81
81
  | `resource` | Manage Godot resources: inspect Resource files by path |
82
82
  | `scene3d` | Get spatial information for 3D nodes: global transforms, bounding boxes, visibility |
83
83
  | `godot_docs` | Fetch Godot Engine documentation |
84
+ | `input` | Inject input into a running Godot game for testing |
84
85
 
85
86
  See [docs/](docs/) for detailed API reference, including the [Claude Code Setup Guide](docs/claude-code-setup.md).
86
87
 
@@ -19,6 +19,7 @@ func setup(plugin: EditorPlugin) -> void:
19
19
  _register_handler(MCPTilemapCommands.new(), plugin)
20
20
  _register_handler(MCPResourceCommands.new(), plugin)
21
21
  _register_handler(MCPScene3DCommands.new(), plugin)
22
+ _register_handler(MCPInputCommands.new(), plugin)
22
23
 
23
24
 
24
25
  func _register_handler(handler: MCPBaseCommand, plugin: EditorPlugin) -> void:
@@ -0,0 +1,193 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPInputCommands
4
+
5
+ const INPUT_TIMEOUT := 30.0
6
+
7
+ var _input_map_result: Dictionary = {}
8
+ var _input_map_pending: bool = false
9
+
10
+ var _sequence_result: Dictionary = {}
11
+ var _sequence_pending: bool = false
12
+
13
+
14
+ var _type_text_result: Dictionary = {}
15
+ var _type_text_pending: bool = false
16
+
17
+
18
+ func get_commands() -> Dictionary:
19
+ return {
20
+ "get_input_map": get_input_map,
21
+ "execute_input_sequence": execute_input_sequence,
22
+ "type_text": type_text,
23
+ }
24
+
25
+
26
+ func get_input_map(_params: Dictionary) -> Dictionary:
27
+ if not EditorInterface.is_playing_scene():
28
+ return _get_editor_input_map()
29
+
30
+ var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
31
+ if debugger_plugin == null or not debugger_plugin.has_active_session():
32
+ return _get_editor_input_map()
33
+
34
+ _input_map_pending = true
35
+ _input_map_result = {}
36
+
37
+ debugger_plugin.input_map_received.connect(_on_input_map_received, CONNECT_ONE_SHOT)
38
+ debugger_plugin.request_input_map()
39
+
40
+ var start_time := Time.get_ticks_msec()
41
+ while _input_map_pending:
42
+ await Engine.get_main_loop().process_frame
43
+ if (Time.get_ticks_msec() - start_time) / 1000.0 > INPUT_TIMEOUT:
44
+ _input_map_pending = false
45
+ if debugger_plugin.input_map_received.is_connected(_on_input_map_received):
46
+ debugger_plugin.input_map_received.disconnect(_on_input_map_received)
47
+ return _get_editor_input_map()
48
+
49
+ return _success(_input_map_result)
50
+
51
+
52
+ func _get_editor_input_map() -> Dictionary:
53
+ var actions: Array[Dictionary] = []
54
+ for action_name in InputMap.get_actions():
55
+ if action_name.begins_with("ui_"):
56
+ continue
57
+ var events := InputMap.action_get_events(action_name)
58
+ var event_strings: Array[String] = []
59
+ for event in events:
60
+ event_strings.append(_event_to_string(event))
61
+ actions.append({
62
+ "name": action_name,
63
+ "events": event_strings,
64
+ })
65
+ return _success({"actions": actions, "source": "editor"})
66
+
67
+
68
+ func _event_to_string(event: InputEvent) -> String:
69
+ if event is InputEventKey:
70
+ var key_event := event as InputEventKey
71
+ var key_name := OS.get_keycode_string(key_event.keycode)
72
+ if key_event.ctrl_pressed:
73
+ key_name = "Ctrl+" + key_name
74
+ if key_event.alt_pressed:
75
+ key_name = "Alt+" + key_name
76
+ if key_event.shift_pressed:
77
+ key_name = "Shift+" + key_name
78
+ return key_name
79
+ elif event is InputEventMouseButton:
80
+ var mouse_event := event as InputEventMouseButton
81
+ match mouse_event.button_index:
82
+ MOUSE_BUTTON_LEFT:
83
+ return "Mouse Left"
84
+ MOUSE_BUTTON_RIGHT:
85
+ return "Mouse Right"
86
+ MOUSE_BUTTON_MIDDLE:
87
+ return "Mouse Middle"
88
+ _:
89
+ return "Mouse Button %d" % mouse_event.button_index
90
+ elif event is InputEventJoypadButton:
91
+ var joy_event := event as InputEventJoypadButton
92
+ return "Joypad Button %d" % joy_event.button_index
93
+ elif event is InputEventJoypadMotion:
94
+ var joy_motion := event as InputEventJoypadMotion
95
+ return "Joypad Axis %d" % joy_motion.axis
96
+ return event.as_text()
97
+
98
+
99
+ func _on_input_map_received(actions: Array, error: String) -> void:
100
+ _input_map_pending = false
101
+ if error.is_empty():
102
+ _input_map_result = {"actions": actions, "source": "game"}
103
+ else:
104
+ _input_map_result = {"error": error}
105
+
106
+
107
+ func execute_input_sequence(params: Dictionary) -> Dictionary:
108
+ var inputs: Array = params.get("inputs", [])
109
+ if inputs.is_empty():
110
+ return _error("INVALID_PARAMS", "inputs array is required and must not be empty")
111
+
112
+ if not EditorInterface.is_playing_scene():
113
+ return _error("NOT_RUNNING", "No game is currently running")
114
+
115
+ var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
116
+ if debugger_plugin == null or not debugger_plugin.has_active_session():
117
+ return _error("NO_SESSION", "No active debug session")
118
+
119
+ var max_end_time: float = 0.0
120
+ for input in inputs:
121
+ var start_ms: float = input.get("start_ms", 0.0)
122
+ var duration_ms: float = input.get("duration_ms", 0.0)
123
+ max_end_time = max(max_end_time, start_ms + duration_ms)
124
+
125
+ var timeout := max(INPUT_TIMEOUT, (max_end_time / 1000.0) + 5.0)
126
+
127
+ _sequence_pending = true
128
+ _sequence_result = {}
129
+
130
+ debugger_plugin.input_sequence_completed.connect(_on_sequence_completed, CONNECT_ONE_SHOT)
131
+ debugger_plugin.request_input_sequence(inputs)
132
+
133
+ var start_time := Time.get_ticks_msec()
134
+ while _sequence_pending:
135
+ await Engine.get_main_loop().process_frame
136
+ if (Time.get_ticks_msec() - start_time) / 1000.0 > timeout:
137
+ _sequence_pending = false
138
+ if debugger_plugin.input_sequence_completed.is_connected(_on_sequence_completed):
139
+ debugger_plugin.input_sequence_completed.disconnect(_on_sequence_completed)
140
+ return _error("TIMEOUT", "Timed out waiting for input sequence to complete")
141
+
142
+ if _sequence_result.has("error"):
143
+ return _error("SEQUENCE_ERROR", _sequence_result.get("error", "Unknown error"))
144
+
145
+ return _success(_sequence_result)
146
+
147
+
148
+ func _on_sequence_completed(result: Dictionary) -> void:
149
+ _sequence_pending = false
150
+ _sequence_result = result
151
+
152
+
153
+ func type_text(params: Dictionary) -> Dictionary:
154
+ var text: String = params.get("text", "")
155
+ var delay_ms: int = int(params.get("delay_ms", 50))
156
+ var submit: bool = params.get("submit", false)
157
+
158
+ if text.is_empty():
159
+ return _error("INVALID_PARAMS", "text is required and must not be empty")
160
+
161
+ if not EditorInterface.is_playing_scene():
162
+ return _error("NOT_RUNNING", "No game is currently running")
163
+
164
+ var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
165
+ if debugger_plugin == null or not debugger_plugin.has_active_session():
166
+ return _error("NO_SESSION", "No active debug session")
167
+
168
+ var timeout := max(INPUT_TIMEOUT, (text.length() * delay_ms / 1000.0) + 5.0)
169
+
170
+ _type_text_pending = true
171
+ _type_text_result = {}
172
+
173
+ debugger_plugin.type_text_completed.connect(_on_type_text_completed, CONNECT_ONE_SHOT)
174
+ debugger_plugin.request_type_text(text, delay_ms, submit)
175
+
176
+ var start_time := Time.get_ticks_msec()
177
+ while _type_text_pending:
178
+ await Engine.get_main_loop().process_frame
179
+ if (Time.get_ticks_msec() - start_time) / 1000.0 > timeout:
180
+ _type_text_pending = false
181
+ if debugger_plugin.type_text_completed.is_connected(_on_type_text_completed):
182
+ debugger_plugin.type_text_completed.disconnect(_on_type_text_completed)
183
+ return _error("TIMEOUT", "Timed out waiting for text input to complete")
184
+
185
+ if _type_text_result.has("error"):
186
+ return _error("TYPE_TEXT_ERROR", _type_text_result.get("error", "Unknown error"))
187
+
188
+ return _success(_type_text_result)
189
+
190
+
191
+ func _on_type_text_completed(result: Dictionary) -> void:
192
+ _type_text_pending = false
193
+ _type_text_result = result
@@ -0,0 +1 @@
1
+ uid://doirdhuupjsk
@@ -6,12 +6,18 @@ signal screenshot_received(success: bool, image_base64: String, width: int, heig
6
6
  signal debug_output_received(output: PackedStringArray)
7
7
  signal performance_metrics_received(metrics: Dictionary)
8
8
  signal find_nodes_received(matches: Array, count: int, error: String)
9
+ signal input_map_received(actions: Array, error: String)
10
+ signal input_sequence_completed(result: Dictionary)
11
+ signal type_text_completed(result: Dictionary)
9
12
 
10
13
  var _active_session_id: int = -1
11
14
  var _pending_screenshot: bool = false
12
15
  var _pending_debug_output: bool = false
13
16
  var _pending_performance_metrics: bool = false
14
17
  var _pending_find_nodes: bool = false
18
+ var _pending_input_map: bool = false
19
+ var _pending_input_sequence: bool = false
20
+ var _pending_type_text: bool = false
15
21
 
16
22
 
17
23
  func _has_capture(prefix: String) -> bool:
@@ -32,6 +38,15 @@ func _capture(message: String, data: Array, session_id: int) -> bool:
32
38
  "godot_mcp:find_nodes_result":
33
39
  _handle_find_nodes_result(data)
34
40
  return true
41
+ "godot_mcp:input_map_result":
42
+ _handle_input_map_result(data)
43
+ return true
44
+ "godot_mcp:input_sequence_result":
45
+ _handle_input_sequence_result(data)
46
+ return true
47
+ "godot_mcp:type_text_result":
48
+ _handle_type_text_result(data)
49
+ return true
35
50
  return false
36
51
 
37
52
 
@@ -53,6 +68,15 @@ func _session_stopped() -> void:
53
68
  if _pending_find_nodes:
54
69
  _pending_find_nodes = false
55
70
  find_nodes_received.emit([], 0, "Game session ended")
71
+ if _pending_input_map:
72
+ _pending_input_map = false
73
+ input_map_received.emit([], "Game session ended")
74
+ if _pending_input_sequence:
75
+ _pending_input_sequence = false
76
+ input_sequence_completed.emit({"error": "Game session ended"})
77
+ if _pending_type_text:
78
+ _pending_type_text = false
79
+ type_text_completed.emit({"error": "Game session ended"})
56
80
 
57
81
 
58
82
  func has_active_session() -> bool:
@@ -147,3 +171,61 @@ func _handle_find_nodes_result(data: Array) -> void:
147
171
  var count: int = data[1] if data.size() > 1 else 0
148
172
  var error: String = data[2] if data.size() > 2 else ""
149
173
  find_nodes_received.emit(matches, count, error)
174
+
175
+
176
+ func request_input_map() -> void:
177
+ if _active_session_id < 0:
178
+ input_map_received.emit([], "No active game session")
179
+ return
180
+ _pending_input_map = true
181
+ var session := get_session(_active_session_id)
182
+ if session:
183
+ session.send_message("godot_mcp:get_input_map", [])
184
+ else:
185
+ _pending_input_map = false
186
+ input_map_received.emit([], "Could not get debugger session")
187
+
188
+
189
+ func _handle_input_map_result(data: Array) -> void:
190
+ _pending_input_map = false
191
+ var actions: Array = data[0] if data.size() > 0 else []
192
+ var error: String = data[1] if data.size() > 1 else ""
193
+ input_map_received.emit(actions, error)
194
+
195
+
196
+ func request_input_sequence(inputs: Array) -> void:
197
+ if _active_session_id < 0:
198
+ input_sequence_completed.emit({"error": "No active game session"})
199
+ return
200
+ _pending_input_sequence = true
201
+ var session := get_session(_active_session_id)
202
+ if session:
203
+ session.send_message("godot_mcp:execute_input_sequence", [inputs])
204
+ else:
205
+ _pending_input_sequence = false
206
+ input_sequence_completed.emit({"error": "Could not get debugger session"})
207
+
208
+
209
+ func _handle_input_sequence_result(data: Array) -> void:
210
+ _pending_input_sequence = false
211
+ var result: Dictionary = data[0] if data.size() > 0 else {}
212
+ input_sequence_completed.emit(result)
213
+
214
+
215
+ func request_type_text(text: String, delay_ms: int, submit: bool) -> void:
216
+ if _active_session_id < 0:
217
+ type_text_completed.emit({"error": "No active game session"})
218
+ return
219
+ _pending_type_text = true
220
+ var session := get_session(_active_session_id)
221
+ if session:
222
+ session.send_message("godot_mcp:type_text", [text, delay_ms, submit])
223
+ else:
224
+ _pending_type_text = false
225
+ type_text_completed.emit({"error": "Could not get debugger session"})
226
+
227
+
228
+ func _handle_type_text_result(data: Array) -> void:
229
+ _pending_type_text = false
230
+ var result: Dictionary = data[0] if data.size() > 0 else {}
231
+ type_text_completed.emit(result)
@@ -20,6 +20,38 @@ func _exit_tree() -> void:
20
20
  EngineDebugger.unregister_message_capture("godot_mcp")
21
21
 
22
22
 
23
+ func _process(_delta: float) -> void:
24
+ if not _sequence_running or _sequence_events.is_empty():
25
+ return
26
+
27
+ var elapsed := Time.get_ticks_msec() - _sequence_start_time
28
+
29
+ while _sequence_events.size() > 0 and _sequence_events[0].time <= elapsed:
30
+ var seq_event: Dictionary = _sequence_events.pop_front()
31
+ var input_event := InputEventAction.new()
32
+ input_event.action = seq_event.action
33
+ input_event.pressed = seq_event.is_press
34
+ input_event.strength = 1.0 if seq_event.is_press else 0.0
35
+ Input.parse_input_event(input_event)
36
+ if not seq_event.is_press:
37
+ _actions_completed += 1
38
+
39
+ if _sequence_events.is_empty():
40
+ _sequence_running = false
41
+ set_process(false)
42
+ EngineDebugger.send_message("godot_mcp:input_sequence_result", [{
43
+ "completed": true,
44
+ "actions_executed": _actions_completed,
45
+ }])
46
+
47
+
48
+ var _sequence_events: Array = []
49
+ var _sequence_start_time: int = 0
50
+ var _sequence_running: bool = false
51
+ var _actions_completed: int = 0
52
+ var _actions_total: int = 0
53
+
54
+
23
55
  func _on_debugger_message(message: String, data: Array) -> bool:
24
56
  match message:
25
57
  "take_screenshot":
@@ -34,6 +66,15 @@ func _on_debugger_message(message: String, data: Array) -> bool:
34
66
  "find_nodes":
35
67
  _handle_find_nodes(data)
36
68
  return true
69
+ "get_input_map":
70
+ _handle_get_input_map()
71
+ return true
72
+ "execute_input_sequence":
73
+ _handle_execute_input_sequence(data)
74
+ return true
75
+ "type_text":
76
+ _handle_type_text(data)
77
+ return true
37
78
  return false
38
79
 
39
80
 
@@ -193,3 +234,153 @@ class _MCPGameLogger extends Logger:
193
234
  _mutex.lock()
194
235
  _output.clear()
195
236
  _mutex.unlock()
237
+
238
+
239
+ func _handle_get_input_map() -> void:
240
+ var actions: Array = []
241
+ for action_name in InputMap.get_actions():
242
+ if action_name.begins_with("ui_"):
243
+ continue
244
+ var events := InputMap.action_get_events(action_name)
245
+ var event_strings: Array = []
246
+ for event in events:
247
+ event_strings.append(_event_to_string(event))
248
+ actions.append({
249
+ "name": action_name,
250
+ "events": event_strings,
251
+ })
252
+ EngineDebugger.send_message("godot_mcp:input_map_result", [actions, ""])
253
+
254
+
255
+ func _event_to_string(event: InputEvent) -> String:
256
+ if event is InputEventKey:
257
+ var key_event := event as InputEventKey
258
+ var key_name := OS.get_keycode_string(key_event.keycode)
259
+ if key_event.ctrl_pressed:
260
+ key_name = "Ctrl+" + key_name
261
+ if key_event.alt_pressed:
262
+ key_name = "Alt+" + key_name
263
+ if key_event.shift_pressed:
264
+ key_name = "Shift+" + key_name
265
+ return key_name
266
+ elif event is InputEventMouseButton:
267
+ var mouse_event := event as InputEventMouseButton
268
+ match mouse_event.button_index:
269
+ MOUSE_BUTTON_LEFT:
270
+ return "Mouse Left"
271
+ MOUSE_BUTTON_RIGHT:
272
+ return "Mouse Right"
273
+ MOUSE_BUTTON_MIDDLE:
274
+ return "Mouse Middle"
275
+ _:
276
+ return "Mouse Button %d" % mouse_event.button_index
277
+ elif event is InputEventJoypadButton:
278
+ var joy_event := event as InputEventJoypadButton
279
+ return "Joypad Button %d" % joy_event.button_index
280
+ elif event is InputEventJoypadMotion:
281
+ var joy_motion := event as InputEventJoypadMotion
282
+ return "Joypad Axis %d" % joy_motion.axis
283
+ return event.as_text()
284
+
285
+
286
+ func _handle_execute_input_sequence(data: Array) -> void:
287
+ var inputs: Array = data[0] if data.size() > 0 else []
288
+
289
+ if inputs.is_empty():
290
+ EngineDebugger.send_message("godot_mcp:input_sequence_result", [{
291
+ "error": "No inputs provided",
292
+ }])
293
+ return
294
+
295
+ _sequence_events.clear()
296
+ _actions_completed = 0
297
+ _actions_total = inputs.size()
298
+
299
+ for input in inputs:
300
+ var action_name: String = input.get("action_name", "")
301
+ var start_ms: int = int(input.get("start_ms", 0))
302
+ var duration_ms: int = int(input.get("duration_ms", 0))
303
+
304
+ if action_name.is_empty():
305
+ continue
306
+
307
+ if not InputMap.has_action(action_name):
308
+ EngineDebugger.send_message("godot_mcp:input_sequence_result", [{
309
+ "error": "Unknown action: %s" % action_name,
310
+ }])
311
+ return
312
+
313
+ _sequence_events.append({
314
+ "time": start_ms,
315
+ "action": action_name,
316
+ "is_press": true,
317
+ })
318
+ _sequence_events.append({
319
+ "time": start_ms + duration_ms,
320
+ "action": action_name,
321
+ "is_press": false,
322
+ })
323
+
324
+ _sequence_events.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
325
+ return a.time < b.time
326
+ )
327
+
328
+ _sequence_start_time = Time.get_ticks_msec()
329
+ _sequence_running = true
330
+ set_process(true)
331
+
332
+
333
+ func _handle_type_text(data: Array) -> void:
334
+ var text: String = data[0] if data.size() > 0 else ""
335
+ var delay_ms: int = int(data[1]) if data.size() > 1 else 50
336
+ var submit: bool = data[2] if data.size() > 2 else false
337
+
338
+ if text.is_empty():
339
+ EngineDebugger.send_message("godot_mcp:type_text_result", [{
340
+ "error": "No text provided",
341
+ }])
342
+ return
343
+
344
+ _type_text_async(text, delay_ms, submit)
345
+
346
+
347
+ func _type_text_async(text: String, delay_ms: int, submit: bool) -> void:
348
+ for i in text.length():
349
+ var char_code := text.unicode_at(i)
350
+
351
+ var press := InputEventKey.new()
352
+ press.keycode = char_code
353
+ press.unicode = char_code
354
+ press.pressed = true
355
+ Input.parse_input_event(press)
356
+
357
+ var release := InputEventKey.new()
358
+ release.keycode = char_code
359
+ release.unicode = char_code
360
+ release.pressed = false
361
+ Input.parse_input_event(release)
362
+
363
+ if delay_ms > 0 and i < text.length() - 1:
364
+ await get_tree().create_timer(delay_ms / 1000.0).timeout
365
+
366
+ if submit:
367
+ if delay_ms > 0:
368
+ await get_tree().create_timer(delay_ms / 1000.0).timeout
369
+
370
+ var enter_press := InputEventKey.new()
371
+ enter_press.keycode = KEY_ENTER
372
+ enter_press.physical_keycode = KEY_ENTER
373
+ enter_press.pressed = true
374
+ Input.parse_input_event(enter_press)
375
+
376
+ var enter_release := InputEventKey.new()
377
+ enter_release.keycode = KEY_ENTER
378
+ enter_release.physical_keycode = KEY_ENTER
379
+ enter_release.pressed = false
380
+ Input.parse_input_event(enter_release)
381
+
382
+ EngineDebugger.send_message("godot_mcp:type_text_result", [{
383
+ "completed": true,
384
+ "chars_typed": text.length(),
385
+ "submitted": submit,
386
+ }])
package/addon/plugin.cfg CHANGED
@@ -3,6 +3,6 @@
3
3
  name="Godot MCP"
4
4
  description="Model Context Protocol server for AI assistant integration"
5
5
  author="godot-mcp"
6
- version="2.8.0"
6
+ version="2.10.0"
7
7
  script="plugin.gd"
8
8
  godot_version_min="4.5"
@@ -2,66 +2,36 @@ import { describe, it, expect } from 'vitest';
2
2
  import { z } from 'zod';
3
3
  import { toInputSchema } from '../core/schema.js';
4
4
  describe('toInputSchema', () => {
5
- it('converts simple object schema', () => {
6
- const schema = z.object({
7
- name: z.string(),
8
- });
9
- const result = toInputSchema(schema);
10
- expect(result).toHaveProperty('type', 'object');
11
- expect(result).toHaveProperty('properties');
12
- expect(result.properties).toHaveProperty('name');
13
- });
14
- it('converts schema with optional fields', () => {
5
+ it('converts object schema with required/optional fields and descriptions', () => {
15
6
  const schema = z.object({
16
7
  required_field: z.string(),
17
8
  optional_field: z.string().optional(),
9
+ path: z.string().describe('The file path'),
18
10
  });
19
11
  const result = toInputSchema(schema);
12
+ expect(result.type).toBe('object');
20
13
  expect(result.required).toContain('required_field');
21
14
  expect(result.required).not.toContain('optional_field');
15
+ const props = result.properties;
16
+ expect(props.path.description).toBe('The file path');
22
17
  });
23
- it('converts schema with descriptions', () => {
24
- const schema = z.object({
25
- path: z.string().describe('The file path'),
26
- });
27
- const result = toInputSchema(schema);
28
- const properties = result.properties;
29
- const pathProp = properties.path;
30
- expect(pathProp.description).toBe('The file path');
31
- });
32
- it('converts nested object schema', () => {
18
+ it('converts nested objects and primitive types', () => {
33
19
  const schema = z.object({
20
+ enabled: z.boolean(),
34
21
  node: z.object({
35
22
  name: z.string(),
36
23
  type: z.string(),
37
24
  }),
38
25
  });
39
26
  const result = toInputSchema(schema);
40
- const properties = result.properties;
41
- const nodeProp = properties.node;
42
- expect(nodeProp.type).toBe('object');
43
- expect(nodeProp.properties).toHaveProperty('name');
44
- expect(nodeProp.properties).toHaveProperty('type');
45
- });
46
- it('converts boolean schema', () => {
47
- const schema = z.object({
48
- enabled: z.boolean(),
49
- });
50
- const result = toInputSchema(schema);
51
- const properties = result.properties;
52
- const enabledProp = properties.enabled;
53
- expect(enabledProp.type).toBe('boolean');
27
+ const props = result.properties;
28
+ expect(props.enabled.type).toBe('boolean');
29
+ expect(props.node.type).toBe('object');
30
+ expect(props.node.properties).toHaveProperty('name');
54
31
  });
55
32
  it('removes $schema property from output', () => {
56
- const schema = z.object({ test: z.string() });
57
- const result = toInputSchema(schema);
33
+ const result = toInputSchema(z.object({ test: z.string() }));
58
34
  expect(result).not.toHaveProperty('$schema');
59
35
  });
60
- it('converts empty object schema', () => {
61
- const schema = z.object({});
62
- const result = toInputSchema(schema);
63
- expect(result.type).toBe('object');
64
- expect(result.properties).toEqual({});
65
- });
66
36
  });
67
37
  //# sourceMappingURL=schema.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.test.js","sourceRoot":"","sources":["../../src/__tests__/schema.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.test.js","sourceRoot":"","sources":["../../src/__tests__/schema.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;YACtB,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE;YAC1B,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;YACrC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC;SAC3C,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAA4B,CAAC;QAEhE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QACpD,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;QAExD,MAAM,KAAK,GAAG,MAAM,CAAC,UAAqD,CAAC;QAC3E,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;YACtB,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;YACpB,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;gBACb,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;gBAChB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;aACjB,CAAC;SACH,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAA4B,CAAC;QAChE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAqD,CAAC;QAE3E,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC3C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,CAAE,KAAK,CAAC,IAAI,CAAC,UAAsC,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,MAAM,GAAG,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;QAC7D,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}