@baixfeng/godot-mcp-cli 1.0.11

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 (115) hide show
  1. package/README.md +187 -0
  2. package/addons/godot_mcp/command_handler.gd +161 -0
  3. package/addons/godot_mcp/command_handler.gd.uid +1 -0
  4. package/addons/godot_mcp/commands/base_command_processor.gd +221 -0
  5. package/addons/godot_mcp/commands/base_command_processor.gd.uid +1 -0
  6. package/addons/godot_mcp/commands/debugger_commands.gd +221 -0
  7. package/addons/godot_mcp/commands/debugger_commands.gd.uid +1 -0
  8. package/addons/godot_mcp/commands/editor_commands.gd +237 -0
  9. package/addons/godot_mcp/commands/editor_commands.gd.uid +1 -0
  10. package/addons/godot_mcp/commands/editor_script_commands.gd +365 -0
  11. package/addons/godot_mcp/commands/editor_script_commands.gd.uid +1 -0
  12. package/addons/godot_mcp/commands/input_commands.gd +337 -0
  13. package/addons/godot_mcp/commands/input_commands.gd.uid +1 -0
  14. package/addons/godot_mcp/commands/node_commands.gd +222 -0
  15. package/addons/godot_mcp/commands/node_commands.gd.uid +1 -0
  16. package/addons/godot_mcp/commands/project_commands.gd +298 -0
  17. package/addons/godot_mcp/commands/project_commands.gd.uid +1 -0
  18. package/addons/godot_mcp/commands/scene_commands.gd +337 -0
  19. package/addons/godot_mcp/commands/scene_commands.gd.uid +1 -0
  20. package/addons/godot_mcp/commands/script_commands.gd +349 -0
  21. package/addons/godot_mcp/commands/script_commands.gd.uid +1 -0
  22. package/addons/godot_mcp/mcp_asset_commands.gd +153 -0
  23. package/addons/godot_mcp/mcp_asset_commands.gd.uid +1 -0
  24. package/addons/godot_mcp/mcp_debug_output_publisher.gd +1669 -0
  25. package/addons/godot_mcp/mcp_debug_output_publisher.gd.uid +1 -0
  26. package/addons/godot_mcp/mcp_debugger_bridge.gd +1455 -0
  27. package/addons/godot_mcp/mcp_debugger_bridge.gd.uid +1 -0
  28. package/addons/godot_mcp/mcp_enhanced_commands.gd +1083 -0
  29. package/addons/godot_mcp/mcp_enhanced_commands.gd.uid +1 -0
  30. package/addons/godot_mcp/mcp_input_handler.gd +545 -0
  31. package/addons/godot_mcp/mcp_input_handler.gd.uid +1 -0
  32. package/addons/godot_mcp/mcp_runtime_debugger_bridge.gd +464 -0
  33. package/addons/godot_mcp/mcp_runtime_debugger_bridge.gd.uid +1 -0
  34. package/addons/godot_mcp/mcp_script_resource_commands.gd +165 -0
  35. package/addons/godot_mcp/mcp_script_resource_commands.gd.uid +1 -0
  36. package/addons/godot_mcp/mcp_server.gd +260 -0
  37. package/addons/godot_mcp/mcp_server.gd.uid +1 -0
  38. package/addons/godot_mcp/plugin.cfg +7 -0
  39. package/addons/godot_mcp/runtime_debugger.gd +81 -0
  40. package/addons/godot_mcp/runtime_debugger.gd.uid +1 -0
  41. package/addons/godot_mcp/ui/mcp_panel.gd +94 -0
  42. package/addons/godot_mcp/ui/mcp_panel.gd.uid +1 -0
  43. package/addons/godot_mcp/ui/mcp_panel.tscn +96 -0
  44. package/addons/godot_mcp/utils/node_utils.gd +82 -0
  45. package/addons/godot_mcp/utils/node_utils.gd.uid +1 -0
  46. package/addons/godot_mcp/utils/resource_utils.gd +81 -0
  47. package/addons/godot_mcp/utils/resource_utils.gd.uid +1 -0
  48. package/addons/godot_mcp/utils/script_utils.gd +114 -0
  49. package/addons/godot_mcp/utils/script_utils.gd.uid +1 -0
  50. package/addons/godot_mcp/websocket_server.gd +197 -0
  51. package/addons/godot_mcp/websocket_server.gd.uid +1 -0
  52. package/dist/cli.d.ts +2 -0
  53. package/dist/cli.js +561 -0
  54. package/dist/cli.js.map +1 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +156 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/resources/asset_resources.d.ts +29 -0
  59. package/dist/resources/asset_resources.js +145 -0
  60. package/dist/resources/asset_resources.js.map +1 -0
  61. package/dist/resources/debug_resources.d.ts +11 -0
  62. package/dist/resources/debug_resources.js +106 -0
  63. package/dist/resources/debug_resources.js.map +1 -0
  64. package/dist/resources/debugger_resources.d.ts +62 -0
  65. package/dist/resources/debugger_resources.js +201 -0
  66. package/dist/resources/debugger_resources.js.map +1 -0
  67. package/dist/resources/editor_resources.d.ts +47 -0
  68. package/dist/resources/editor_resources.js +155 -0
  69. package/dist/resources/editor_resources.js.map +1 -0
  70. package/dist/resources/project_resources.d.ts +33 -0
  71. package/dist/resources/project_resources.js +137 -0
  72. package/dist/resources/project_resources.js.map +1 -0
  73. package/dist/resources/scene_resources.d.ts +33 -0
  74. package/dist/resources/scene_resources.js +160 -0
  75. package/dist/resources/scene_resources.js.map +1 -0
  76. package/dist/resources/script_resources.d.ts +51 -0
  77. package/dist/resources/script_resources.js +203 -0
  78. package/dist/resources/script_resources.js.map +1 -0
  79. package/dist/tools/asset_tools.d.ts +5 -0
  80. package/dist/tools/asset_tools.js +125 -0
  81. package/dist/tools/asset_tools.js.map +1 -0
  82. package/dist/tools/debugger_tools.d.ts +2 -0
  83. package/dist/tools/debugger_tools.js +342 -0
  84. package/dist/tools/debugger_tools.js.map +1 -0
  85. package/dist/tools/editor_tools.d.ts +2 -0
  86. package/dist/tools/editor_tools.js +165 -0
  87. package/dist/tools/editor_tools.js.map +1 -0
  88. package/dist/tools/enhanced_tools.d.ts +5 -0
  89. package/dist/tools/enhanced_tools.js +706 -0
  90. package/dist/tools/enhanced_tools.js.map +1 -0
  91. package/dist/tools/input_tools.d.ts +2 -0
  92. package/dist/tools/input_tools.js +408 -0
  93. package/dist/tools/input_tools.js.map +1 -0
  94. package/dist/tools/node_tools.d.ts +5 -0
  95. package/dist/tools/node_tools.js +217 -0
  96. package/dist/tools/node_tools.js.map +1 -0
  97. package/dist/tools/project_tools.d.ts +5 -0
  98. package/dist/tools/project_tools.js +162 -0
  99. package/dist/tools/project_tools.js.map +1 -0
  100. package/dist/tools/scene_tools.d.ts +5 -0
  101. package/dist/tools/scene_tools.js +260 -0
  102. package/dist/tools/scene_tools.js.map +1 -0
  103. package/dist/tools/script_resource_tools.d.ts +5 -0
  104. package/dist/tools/script_resource_tools.js +5 -0
  105. package/dist/tools/script_resource_tools.js.map +1 -0
  106. package/dist/tools/script_tools.d.ts +5 -0
  107. package/dist/tools/script_tools.js +154 -0
  108. package/dist/tools/script_tools.js.map +1 -0
  109. package/dist/utils/godot_connection.d.ts +30 -0
  110. package/dist/utils/godot_connection.js +285 -0
  111. package/dist/utils/godot_connection.js.map +1 -0
  112. package/dist/utils/types.d.ts +16 -0
  113. package/dist/utils/types.js +2 -0
  114. package/dist/utils/types.js.map +1 -0
  115. package/package.json +58 -0
@@ -0,0 +1,365 @@
1
+ @tool
2
+ class_name MCPEditorScriptCommands
3
+ extends MCPBaseCommandProcessor
4
+
5
+ const EXECUTION_TIMEOUT_SECONDS := 1.5
6
+ const MAX_LOG_TAIL_CHARS := 2048
7
+ const MAX_LOG_TAIL_LINES := 20
8
+
9
+ var _pending_executions := {}
10
+
11
+ func process_command(client_id: int, command_type: String, params: Dictionary, command_id: String) -> bool:
12
+ match command_type:
13
+ "execute_editor_script":
14
+ _execute_editor_script(client_id, params, command_id)
15
+ return true
16
+ return false # Command not handled
17
+
18
+ # Add API compatibility fixing function
19
+ func _fix_api_compatibility(code: String) -> String:
20
+ var modified_code = code
21
+
22
+ # Handle Directory API (replaced with DirAccess in Godot 4.x)
23
+ if "Directory.new()" in modified_code:
24
+ modified_code = modified_code.replace("Directory.new()", "DirAccess.open('res://')")
25
+ modified_code = modified_code.replace("dir.list_dir_begin(true, true)", "dir.list_dir_begin()")
26
+
27
+ # Handle File API (replaced with FileAccess in Godot 4.x)
28
+ if "File.new()" in modified_code:
29
+ modified_code = modified_code.replace("File.new()", "FileAccess.open('res://', FileAccess.READ)")
30
+ modified_code = modified_code.replace("file.open(", "file = FileAccess.open(")
31
+
32
+ return modified_code
33
+
34
+ func _execute_editor_script(client_id: int, params: Dictionary, command_id: String) -> void:
35
+ var code = params.get("code", "")
36
+
37
+ # Validation
38
+ if code.is_empty():
39
+ return _send_error(client_id, "Code cannot be empty", command_id)
40
+
41
+ # Fix common API incompatibilities
42
+ code = _fix_api_compatibility(code)
43
+
44
+ var parse_log_snapshot = _capture_log_snapshot()
45
+
46
+ # Create a temporary script node to execute the code
47
+ var script_node := Node.new()
48
+ script_node.name = "EditorScriptExecutor"
49
+ add_child(script_node)
50
+
51
+ # Create a temporary script
52
+ var script = GDScript.new()
53
+
54
+ var output = []
55
+ var error_message = ""
56
+ var execution_result = null
57
+
58
+ # Replace print() calls with custom_print() in the user code
59
+ var modified_code = _replace_print_calls(code)
60
+
61
+ # Use consistent tab indentation in the template
62
+ var script_content = """@tool
63
+ extends Node
64
+
65
+ signal execution_completed
66
+
67
+ # Variable to store the result
68
+ var result = null
69
+ var _output_array = []
70
+ var _error_message = ""
71
+ var _parent
72
+
73
+ # Custom print function that stores output in the array
74
+ func custom_print(values):
75
+ # Convert array of values to a single string
76
+ var output_str = ""
77
+ if values is Array:
78
+ for i in range(values.size()):
79
+ if i > 0:
80
+ output_str += " "
81
+ output_str += str(values[i])
82
+ else:
83
+ output_str = str(values)
84
+
85
+ _output_array.append(output_str)
86
+ print(output_str) # Still print to the console for debugging
87
+
88
+ func run():
89
+ print("Executing script... ready func")
90
+ _parent = get_parent()
91
+ var scene = get_tree().edited_scene_root
92
+
93
+ # Execute the provided code
94
+ var err = _execute_code()
95
+
96
+ # If there was an error, store it
97
+ if err != OK:
98
+ _error_message = "Failed to execute script with error: " + str(err)
99
+
100
+ # Signal that execution is complete
101
+ execution_completed.emit()
102
+
103
+ func _execute_code():
104
+ # USER CODE START
105
+ {user_code}
106
+ # USER CODE END
107
+ return OK
108
+ """
109
+
110
+ # Process the user code to ensure consistent indentation
111
+ # This helps prevent "mixed tabs and spaces" errors
112
+ var processed_lines = []
113
+ var lines = modified_code.split("\n")
114
+ for line in lines:
115
+ # Replace any spaces at the beginning with tabs
116
+ var processed_line = line
117
+
118
+ # If line starts with spaces, replace with a tab
119
+ var space_count = 0
120
+ for i in range(line.length()):
121
+ if line[i] == " ":
122
+ space_count += 1
123
+ else:
124
+ break
125
+
126
+ # If we found spaces at the beginning, replace with tabs
127
+ if space_count > 0:
128
+ # Create tabs based on space count (e.g., 4 spaces = 1 tab)
129
+ var tabs = ""
130
+ for _i in range(space_count / 4): # Integer division
131
+ tabs += "\t"
132
+ processed_line = tabs + line.substr(space_count)
133
+
134
+ processed_lines.append(processed_line)
135
+
136
+ var indented_code = ""
137
+ for line in processed_lines:
138
+ indented_code += "\t" + line + "\n"
139
+
140
+ script_content = script_content.replace("{user_code}", indented_code)
141
+ script.source_code = script_content
142
+
143
+ # Check for script errors during parsing
144
+ var error = script.reload()
145
+ if error != OK:
146
+ var parse_tail = _extract_log_tail(parse_log_snapshot)
147
+ var parse_message = "Script parsing error: " + str(error)
148
+ if not parse_tail.is_empty():
149
+ parse_message += "\n" + "\n".join(parse_tail)
150
+ remove_child(script_node)
151
+ script_node.queue_free()
152
+ return _send_error(client_id, parse_message, command_id)
153
+
154
+ # Assign the script to the node
155
+ script_node.set_script(script)
156
+
157
+ # Connect to the execution_completed signal
158
+ script_node.connect("execution_completed", _on_script_execution_completed.bind(script_node, client_id, command_id))
159
+
160
+ var execution_log_snapshot = _capture_log_snapshot()
161
+ _track_pending_execution(script_node, client_id, command_id, execution_log_snapshot)
162
+ script_node.run()
163
+
164
+
165
+ # Signal handler for when script execution completes
166
+ func _on_script_execution_completed(script_node: Node, client_id: int, command_id: String) -> void:
167
+ var pending = _pop_pending_execution(script_node)
168
+ var log_snapshot = pending.get("log_snapshot", {})
169
+ var log_tail = _extract_log_tail(log_snapshot)
170
+
171
+ # Collect results safely by checking if properties exist
172
+ var execution_result = script_node.get("result")
173
+ var output = script_node._output_array
174
+ var error_message = script_node._error_message
175
+
176
+ # Clean up
177
+ remove_child(script_node)
178
+ script_node.queue_free()
179
+
180
+ # Build the response
181
+ var result_data = {
182
+ "success": error_message.is_empty(),
183
+ "output": output
184
+ }
185
+
186
+ print("result_data: ", result_data)
187
+
188
+ if not error_message.is_empty():
189
+ result_data["error"] = error_message
190
+ if not log_tail.is_empty():
191
+ result_data["debug_log_tail"] = log_tail
192
+ elif execution_result != null:
193
+ result_data["result"] = execution_result
194
+
195
+ _send_success(client_id, result_data, command_id)
196
+
197
+ # Replace print() calls with custom_print() in the user code
198
+ func _replace_print_calls(code: String) -> String:
199
+ var modified_code := ""
200
+ var search_index := 0
201
+
202
+ while search_index < code.length():
203
+ var match_index = code.find("print", search_index)
204
+ if match_index == -1:
205
+ modified_code += code.substr(search_index)
206
+ break
207
+
208
+ modified_code += code.substr(search_index, match_index - search_index)
209
+ var prev_char = code.substr(match_index - 1, 1) if match_index > 0 else ""
210
+ var next_char = code.substr(match_index + 5, 1) if match_index + 5 < code.length() else ""
211
+
212
+ if _is_identifier_char(prev_char) or _is_identifier_char(next_char):
213
+ modified_code += "print"
214
+ search_index = match_index + 5
215
+ continue
216
+
217
+ var paren_index = _skip_whitespace(code, match_index + 5)
218
+ if paren_index >= code.length() or code[paren_index] != "(":
219
+ modified_code += "print"
220
+ search_index = match_index + 5
221
+ continue
222
+
223
+ var closing_index = _find_matching_paren(code, paren_index)
224
+ if closing_index == -1:
225
+ modified_code += code.substr(match_index)
226
+ break
227
+
228
+ var inner_content = code.substr(paren_index + 1, closing_index - paren_index - 1)
229
+ modified_code += "custom_print([" + inner_content + "])"
230
+ search_index = closing_index + 1
231
+
232
+ return modified_code
233
+
234
+ func _skip_whitespace(text: String, start_index: int) -> int:
235
+ var index = start_index
236
+ while index < text.length():
237
+ var char = text.substr(index, 1)
238
+ if char != " " and char != "\t" and char != "\n" and char != "\r":
239
+ break
240
+ index += 1
241
+ return index
242
+
243
+ func _is_identifier_char(char: String) -> bool:
244
+ if char.is_empty():
245
+ return false
246
+ var code_point = char.unicode_at(0)
247
+ var is_digit = code_point >= 48 and code_point <= 57
248
+ var is_lower = code_point >= 97 and code_point <= 122
249
+ var is_upper = code_point >= 65 and code_point <= 90
250
+ return is_digit or is_lower or is_upper or char == "_"
251
+
252
+ func _find_matching_paren(text: String, open_index: int) -> int:
253
+ var depth = 1
254
+ var index = open_index + 1
255
+ var in_string = false
256
+ var string_delimiter = ""
257
+ var escape_next = false
258
+
259
+ while index < text.length():
260
+ var char = text[index]
261
+ if in_string:
262
+ if escape_next:
263
+ escape_next = false
264
+ elif char == "\\":
265
+ escape_next = true
266
+ elif char == string_delimiter:
267
+ in_string = false
268
+ else:
269
+ if char == "\"" or char == "'":
270
+ in_string = true
271
+ string_delimiter = char
272
+ elif char == "(":
273
+ depth += 1
274
+ elif char == ")":
275
+ depth -= 1
276
+ if depth == 0:
277
+ return index
278
+ index += 1
279
+ return -1
280
+
281
+ func _get_debug_output_publisher():
282
+ if Engine.has_meta("MCPDebugOutputPublisher"):
283
+ var publisher = Engine.get_meta("MCPDebugOutputPublisher")
284
+ if publisher and publisher.has_method("get_full_log_text"):
285
+ return publisher
286
+ return null
287
+
288
+ func _capture_log_snapshot() -> Dictionary:
289
+ var publisher = _get_debug_output_publisher()
290
+ if publisher == null:
291
+ return {}
292
+ var text = publisher.get_full_log_text()
293
+ return {
294
+ "publisher": publisher,
295
+ "length": text.length()
296
+ }
297
+
298
+ func _extract_log_tail(snapshot: Dictionary) -> Array:
299
+ if snapshot.is_empty():
300
+ return []
301
+ if not snapshot.has("publisher") or not snapshot.has("length"):
302
+ return []
303
+ var publisher = snapshot["publisher"]
304
+ if publisher == null or not publisher.has_method("get_full_log_text"):
305
+ return []
306
+ var baseline = int(snapshot["length"])
307
+ var text = publisher.get_full_log_text()
308
+ if baseline < 0 or baseline > text.length():
309
+ baseline = max(0, text.length() - MAX_LOG_TAIL_CHARS)
310
+ var delta = text.substr(baseline)
311
+ if delta.length() > MAX_LOG_TAIL_CHARS:
312
+ delta = delta.substr(delta.length() - MAX_LOG_TAIL_CHARS)
313
+ delta = delta.strip_edges()
314
+ if delta.is_empty():
315
+ return []
316
+ var lines = delta.split("\n", false)
317
+ if lines.size() > MAX_LOG_TAIL_LINES:
318
+ lines = lines.slice(lines.size() - MAX_LOG_TAIL_LINES, lines.size())
319
+ return lines
320
+
321
+ func _track_pending_execution(script_node: Node, client_id: int, command_id: String, log_snapshot: Dictionary) -> void:
322
+ var execution_id = script_node.get_instance_id()
323
+ var timer := Timer.new()
324
+ timer.one_shot = true
325
+ timer.wait_time = EXECUTION_TIMEOUT_SECONDS
326
+ add_child(timer)
327
+ timer.connect("timeout", Callable(self, "_on_execution_timeout").bind(execution_id, client_id, command_id))
328
+ timer.start()
329
+ _pending_executions[execution_id] = {
330
+ "client_id": client_id,
331
+ "command_id": command_id,
332
+ "log_snapshot": log_snapshot,
333
+ "timer": timer,
334
+ "node": script_node
335
+ }
336
+
337
+ func _pop_pending_execution(script_node: Node) -> Dictionary:
338
+ var execution_id = script_node.get_instance_id()
339
+ if not _pending_executions.has(execution_id):
340
+ return {}
341
+ var pending: Dictionary = _pending_executions[execution_id]
342
+ _pending_executions.erase(execution_id)
343
+ var timer = pending.get("timer", null)
344
+ if timer and is_instance_valid(timer):
345
+ timer.queue_free()
346
+ return pending
347
+
348
+ func _on_execution_timeout(execution_id: int, client_id: int, command_id: String) -> void:
349
+ if not _pending_executions.has(execution_id):
350
+ return
351
+ var pending: Dictionary = _pending_executions[execution_id]
352
+ _pending_executions.erase(execution_id)
353
+ var timer = pending.get("timer", null)
354
+ if timer and is_instance_valid(timer):
355
+ timer.queue_free()
356
+ var script_node = pending.get("node", null)
357
+ if script_node and is_instance_valid(script_node):
358
+ if script_node.is_inside_tree():
359
+ remove_child(script_node)
360
+ script_node.queue_free()
361
+ var log_tail = _extract_log_tail(pending.get("log_snapshot", {}))
362
+ var message = "Script execution timed out before completion."
363
+ if not log_tail.is_empty():
364
+ message += "\n" + "\n".join(log_tail)
365
+ _send_error(client_id, message, command_id)
@@ -0,0 +1 @@
1
+ uid://caqthbi04b8ym
@@ -0,0 +1,337 @@
1
+ @tool
2
+ class_name MCPInputCommands
3
+ extends MCPBaseCommandProcessor
4
+
5
+ ## Command processor for input simulation in running games.
6
+ ## Sends input commands through the debugger bridge to the runtime input handler.
7
+
8
+ const INPUT_CAPTURE_NAME := "mcp_input"
9
+ const DEFAULT_TIMEOUT_MS := 2000
10
+
11
+ var _next_request_id: int = 1
12
+ var _pending_requests: Dictionary = {}
13
+
14
+ func _get_runtime_bridge() -> MCPRuntimeDebuggerBridge:
15
+ if Engine.has_meta("MCPRuntimeDebuggerBridge"):
16
+ return Engine.get_meta("MCPRuntimeDebuggerBridge") as MCPRuntimeDebuggerBridge
17
+ return null
18
+
19
+
20
+ func process_command(client_id: int, command_type: String, params: Dictionary, command_id: String) -> bool:
21
+ match command_type:
22
+ "simulate_action_press":
23
+ _handle_action_press(client_id, params, command_id)
24
+ return true
25
+ "simulate_action_release":
26
+ _handle_action_release(client_id, params, command_id)
27
+ return true
28
+ "simulate_action_tap":
29
+ _handle_action_tap(client_id, params, command_id)
30
+ return true
31
+ "simulate_mouse_click":
32
+ _handle_mouse_click(client_id, params, command_id)
33
+ return true
34
+ "simulate_mouse_move":
35
+ _handle_mouse_move(client_id, params, command_id)
36
+ return true
37
+ "simulate_drag":
38
+ _handle_drag(client_id, params, command_id)
39
+ return true
40
+ "simulate_key_press":
41
+ _handle_key_press(client_id, params, command_id)
42
+ return true
43
+ "simulate_input_sequence":
44
+ _handle_input_sequence(client_id, params, command_id)
45
+ return true
46
+ "get_input_actions":
47
+ _handle_get_input_actions(client_id, params, command_id)
48
+ return true
49
+
50
+ return false
51
+
52
+
53
+ func _handle_action_press(client_id: int, params: Dictionary, command_id: String) -> void:
54
+ var action := str(params.get("action", ""))
55
+ if action.is_empty():
56
+ _send_error(client_id, "Action name is required", command_id)
57
+ return
58
+
59
+ var strength := float(params.get("strength", 1.0))
60
+
61
+ var result := await _send_input_command("action_press", [action, strength])
62
+ if result.has("error"):
63
+ _send_error(client_id, result["error"], command_id)
64
+ else:
65
+ _send_success(client_id, result, command_id)
66
+
67
+
68
+ func _handle_action_release(client_id: int, params: Dictionary, command_id: String) -> void:
69
+ var action := str(params.get("action", ""))
70
+ if action.is_empty():
71
+ _send_error(client_id, "Action name is required", command_id)
72
+ return
73
+
74
+ var result := await _send_input_command("action_release", [action])
75
+ if result.has("error"):
76
+ _send_error(client_id, result["error"], command_id)
77
+ else:
78
+ _send_success(client_id, result, command_id)
79
+
80
+
81
+ func _handle_action_tap(client_id: int, params: Dictionary, command_id: String) -> void:
82
+ var action := str(params.get("action", ""))
83
+ if action.is_empty():
84
+ _send_error(client_id, "Action name is required", command_id)
85
+ return
86
+
87
+ var duration_ms := int(params.get("duration_ms", 100))
88
+
89
+ var result := await _send_input_command("action_tap", [action, duration_ms])
90
+ if result.has("error"):
91
+ _send_error(client_id, result["error"], command_id)
92
+ else:
93
+ _send_success(client_id, result, command_id)
94
+
95
+
96
+ func _handle_mouse_click(client_id: int, params: Dictionary, command_id: String) -> void:
97
+ var x := float(params.get("x", 0))
98
+ var y := float(params.get("y", 0))
99
+ var button_str := str(params.get("button", "left")).to_lower()
100
+ var double_click := bool(params.get("double_click", false))
101
+
102
+ var button := MOUSE_BUTTON_LEFT
103
+ match button_str:
104
+ "right":
105
+ button = MOUSE_BUTTON_RIGHT
106
+ "middle":
107
+ button = MOUSE_BUTTON_MIDDLE
108
+
109
+ var options := {
110
+ "x": x,
111
+ "y": y,
112
+ "button": button,
113
+ "double_click": double_click
114
+ }
115
+
116
+ var result := await _send_input_command("mouse_click", [options])
117
+ if result.has("error"):
118
+ _send_error(client_id, result["error"], command_id)
119
+ else:
120
+ _send_success(client_id, result, command_id)
121
+
122
+
123
+ func _handle_mouse_move(client_id: int, params: Dictionary, command_id: String) -> void:
124
+ var x := float(params.get("x", 0))
125
+ var y := float(params.get("y", 0))
126
+
127
+ var options := {
128
+ "x": x,
129
+ "y": y
130
+ }
131
+
132
+ var result := await _send_input_command("mouse_move", [options])
133
+ if result.has("error"):
134
+ _send_error(client_id, result["error"], command_id)
135
+ else:
136
+ _send_success(client_id, result, command_id)
137
+
138
+
139
+ func _handle_drag(client_id: int, params: Dictionary, command_id: String) -> void:
140
+ var start_x := float(params.get("start_x", 0))
141
+ var start_y := float(params.get("start_y", 0))
142
+ var end_x := float(params.get("end_x", 0))
143
+ var end_y := float(params.get("end_y", 0))
144
+ var duration_ms := int(params.get("duration_ms", 200))
145
+ var steps := int(params.get("steps", 10))
146
+
147
+ var button_str := str(params.get("button", "left")).to_lower()
148
+ var button := MOUSE_BUTTON_LEFT
149
+ match button_str:
150
+ "right":
151
+ button = MOUSE_BUTTON_RIGHT
152
+ "middle":
153
+ button = MOUSE_BUTTON_MIDDLE
154
+
155
+ var options := {
156
+ "start_x": start_x,
157
+ "start_y": start_y,
158
+ "end_x": end_x,
159
+ "end_y": end_y,
160
+ "duration_ms": duration_ms,
161
+ "steps": steps,
162
+ "button": button
163
+ }
164
+
165
+ # Drag operations can take longer, adjust timeout
166
+ var timeout := duration_ms + 1000
167
+
168
+ var result := await _send_input_command("drag", [options], timeout)
169
+ if result.has("error"):
170
+ _send_error(client_id, result["error"], command_id)
171
+ else:
172
+ _send_success(client_id, result, command_id)
173
+
174
+
175
+ func _handle_key_press(client_id: int, params: Dictionary, command_id: String) -> void:
176
+ var key := str(params.get("key", ""))
177
+ if key.is_empty():
178
+ _send_error(client_id, "Key is required", command_id)
179
+ return
180
+
181
+ var duration_ms := int(params.get("duration_ms", 100))
182
+ var modifiers := params.get("modifiers", {})
183
+ if typeof(modifiers) != TYPE_DICTIONARY:
184
+ modifiers = {}
185
+
186
+ var options := {
187
+ "key": key,
188
+ "duration_ms": duration_ms,
189
+ "modifiers": modifiers
190
+ }
191
+
192
+ var result := await _send_input_command("key_press", [options])
193
+ if result.has("error"):
194
+ _send_error(client_id, result["error"], command_id)
195
+ else:
196
+ _send_success(client_id, result, command_id)
197
+
198
+
199
+ func _handle_input_sequence(client_id: int, params: Dictionary, command_id: String) -> void:
200
+ var sequence := params.get("sequence", [])
201
+ if typeof(sequence) != TYPE_ARRAY:
202
+ _send_error(client_id, "Sequence must be an array", command_id)
203
+ return
204
+
205
+ if sequence.is_empty():
206
+ _send_error(client_id, "Sequence cannot be empty", command_id)
207
+ return
208
+
209
+ # Calculate timeout based on sequence length and potential wait times
210
+ var total_wait := 0
211
+ for step in sequence:
212
+ if typeof(step) == TYPE_DICTIONARY:
213
+ total_wait += int(step.get("duration_ms", 100))
214
+ var timeout := total_wait + 2000
215
+
216
+ var result := await _send_input_command("input_sequence", [sequence], timeout)
217
+ if result.has("error"):
218
+ _send_error(client_id, result["error"], command_id)
219
+ else:
220
+ _send_success(client_id, result, command_id)
221
+
222
+
223
+ func _handle_get_input_actions(client_id: int, _params: Dictionary, command_id: String) -> void:
224
+ var result := await _send_input_command("get_input_actions", [])
225
+ if result.has("error"):
226
+ _send_error(client_id, result["error"], command_id)
227
+ else:
228
+ _send_success(client_id, result, command_id)
229
+
230
+
231
+ func _send_input_command(action: String, data: Array, timeout_ms: int = DEFAULT_TIMEOUT_MS) -> Dictionary:
232
+ var runtime_bridge := _get_runtime_bridge()
233
+ if runtime_bridge == null:
234
+ return { "error": "Runtime debugger bridge not available. Ensure the project is running." }
235
+
236
+ # Get active session
237
+ var sessions := runtime_bridge.get_sessions()
238
+ var active_session = null
239
+ var session_id := -1
240
+
241
+ for i in range(sessions.size()):
242
+ var session = sessions[i]
243
+ if session and session.has_method("is_active") and session.is_active():
244
+ active_session = session
245
+ session_id = i
246
+ break
247
+
248
+ if active_session == null:
249
+ return { "error": "No active runtime session. Start the project with debugger attached." }
250
+
251
+ # Generate request ID
252
+ var request_id := _next_request_id
253
+ _next_request_id += 1
254
+
255
+ # Prepare payload - prepend request_id to data
256
+ var payload := Array()
257
+ payload.append(request_id)
258
+ for item in data:
259
+ payload.append(item)
260
+
261
+ # Store pending request
262
+ _pending_requests[request_id] = {
263
+ "session_id": session_id,
264
+ "action": action,
265
+ "timestamp": Time.get_ticks_msec()
266
+ }
267
+
268
+ # Send message to runtime
269
+ var message_name := "%s:%s" % [INPUT_CAPTURE_NAME, action]
270
+ active_session.send_message(message_name, payload)
271
+
272
+ # Wait for response with timeout
273
+ var deadline := Time.get_ticks_msec() + timeout_ms
274
+ var result: Dictionary = {}
275
+
276
+ while Time.get_ticks_msec() < deadline:
277
+ # Check if we have a result
278
+ if _has_input_result(session_id, request_id):
279
+ result = _take_input_result(session_id, request_id)
280
+ break
281
+
282
+ # Wait a frame
283
+ if get_tree():
284
+ await get_tree().process_frame
285
+ else:
286
+ break
287
+
288
+ # Clean up pending request
289
+ _pending_requests.erase(request_id)
290
+
291
+ if result.is_empty():
292
+ return { "error": "Input command timed out. Ensure the game has the MCP input handler autoload." }
293
+
294
+ return result
295
+
296
+
297
+ func _has_input_result(session_id: int, request_id: int) -> bool:
298
+ var runtime_bridge := _get_runtime_bridge()
299
+ if runtime_bridge == null:
300
+ return false
301
+
302
+ # Check if runtime bridge has input results
303
+ if runtime_bridge.has_method("has_input_result"):
304
+ return runtime_bridge.has_input_result(session_id, request_id)
305
+
306
+ # Fallback: check _sessions directly if accessible
307
+ var sessions_dict = runtime_bridge.get("_sessions")
308
+ if sessions_dict and sessions_dict.has(session_id):
309
+ var state: Dictionary = sessions_dict[session_id]
310
+ var input_results: Dictionary = state.get("input_results", {})
311
+ return input_results.has(request_id)
312
+
313
+ return false
314
+
315
+
316
+ func _take_input_result(session_id: int, request_id: int) -> Dictionary:
317
+ var runtime_bridge := _get_runtime_bridge()
318
+ if runtime_bridge == null:
319
+ return {}
320
+
321
+ # Check if runtime bridge has take method
322
+ if runtime_bridge.has_method("take_input_result"):
323
+ return runtime_bridge.take_input_result(session_id, request_id)
324
+
325
+ # Fallback: access _sessions directly if accessible
326
+ var sessions_dict = runtime_bridge.get("_sessions")
327
+ if sessions_dict and sessions_dict.has(session_id):
328
+ var state: Dictionary = sessions_dict[session_id]
329
+ var input_results: Dictionary = state.get("input_results", {})
330
+ if input_results.has(request_id):
331
+ var result: Dictionary = input_results[request_id]
332
+ input_results.erase(request_id)
333
+ state["input_results"] = input_results
334
+ sessions_dict[session_id] = state
335
+ return result
336
+
337
+ return {}
@@ -0,0 +1 @@
1
+ uid://n3hflqvxu06j