@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.
- package/README.md +1 -0
- package/addon/command_router.gd +1 -0
- package/addon/commands/input_commands.gd +193 -0
- package/addon/commands/input_commands.gd.uid +1 -0
- package/addon/core/mcp_debugger_plugin.gd +82 -0
- package/addon/game_bridge/mcp_game_bridge.gd +191 -0
- package/addon/plugin.cfg +1 -1
- package/dist/__tests__/schema.test.js +12 -42
- package/dist/__tests__/schema.test.js.map +1 -1
- package/dist/__tests__/tools/animation.test.js +53 -153
- package/dist/__tests__/tools/animation.test.js.map +1 -1
- package/dist/__tests__/tools/docs.test.js +73 -75
- package/dist/__tests__/tools/docs.test.js.map +1 -1
- package/dist/__tests__/tools/editor.test.js +71 -285
- package/dist/__tests__/tools/editor.test.js.map +1 -1
- package/dist/__tests__/tools/input.test.d.ts +2 -0
- package/dist/__tests__/tools/input.test.d.ts.map +1 -0
- package/dist/__tests__/tools/input.test.js +137 -0
- package/dist/__tests__/tools/input.test.js.map +1 -0
- package/dist/__tests__/tools/node.test.js +104 -268
- package/dist/__tests__/tools/node.test.js.map +1 -1
- package/dist/__tests__/tools/resource.test.js +44 -94
- package/dist/__tests__/tools/resource.test.js.map +1 -1
- package/dist/__tests__/tools/scene.test.js +28 -75
- package/dist/__tests__/tools/scene.test.js.map +1 -1
- package/dist/__tests__/tools/tilemap.test.js +71 -229
- package/dist/__tests__/tools/tilemap.test.js.map +1 -1
- package/dist/__tests__/utils/logger.test.js +36 -201
- package/dist/__tests__/utils/logger.test.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/input.d.ts +63 -0
- package/dist/tools/input.d.ts.map +1 -0
- package/dist/tools/input.js +85 -0
- package/dist/tools/input.js.map +1 -0
- 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
|
|
package/addon/command_router.gd
CHANGED
|
@@ -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
|
@@ -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
|
|
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
|
|
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
|
|
41
|
-
|
|
42
|
-
expect(
|
|
43
|
-
expect(
|
|
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
|
|
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":"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
|
|
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"}
|