@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,464 @@
1
+ @tool
2
+ class_name MCPRuntimeDebuggerBridge
3
+ extends EditorDebuggerPlugin
4
+
5
+ signal scene_tree_updated(session_id: int)
6
+ signal runtime_eval_completed(session_id: int, request_id: int)
7
+ signal input_result_received(session_id: int, request_id: int)
8
+
9
+ const CAPTURE_SCENE := "scene"
10
+ const VIEW_HAS_VISIBLE_METHOD := 1 << 1
11
+ const VIEW_VISIBLE := 1 << 2
12
+ const VIEW_VISIBLE_IN_TREE := 1 << 3
13
+ const DEFAULT_TIMEOUT_MS := 800
14
+ const DEFAULT_EVAL_TIMEOUT_MS := 800
15
+ const SCENE_CAPTURE_NAMES := ["scene", "limboai"]
16
+ const EVAL_CAPTURE_NAME := "mcp_eval"
17
+ const INPUT_CAPTURE_NAME := "mcp_input"
18
+
19
+ var _sessions: Dictionary = {}
20
+ var _next_eval_request_id: int = 1
21
+
22
+ func _init() -> void:
23
+ _sessions.clear()
24
+ _next_eval_request_id = 1
25
+
26
+ func _setup_session(session_id: int) -> void:
27
+ _trace("setup_session %s" % session_id)
28
+ _ensure_session(session_id)
29
+ var session := get_session(session_id)
30
+ if session:
31
+ session.started.connect(_on_session_started.bind(session_id), CONNECT_DEFERRED)
32
+ session.stopped.connect(_on_session_stopped.bind(session_id), CONNECT_DEFERRED)
33
+ session.breaked.connect(_on_session_breaked.bind(session_id), CONNECT_DEFERRED)
34
+ if session.is_active():
35
+ var state: Dictionary = _sessions[session_id]
36
+ state["active"] = true
37
+ _sessions[session_id] = state
38
+ _trace("session %s already active" % session_id)
39
+
40
+ func _has_capture(capture: String) -> bool:
41
+ for prefix in SCENE_CAPTURE_NAMES:
42
+ if capture == prefix or capture.begins_with(prefix + ":"):
43
+ return true
44
+ if capture == INPUT_CAPTURE_NAME or capture.begins_with(INPUT_CAPTURE_NAME + ":"):
45
+ return true
46
+ return false
47
+
48
+ func _capture(message: String, data: Array, session_id: int) -> bool:
49
+ _trace("capture %s session=%s payload_len=%s" % [message, session_id, data.size()])
50
+ var normalized := _normalize_capture_name(message)
51
+ if normalized == "scene:scene_tree":
52
+ if data.size() == 0 or data.size() % 6 != 0:
53
+ _trace("discarding malformed scene_tree payload (size=%s)" % data.size())
54
+ return false
55
+ _trace("storing scene tree for session %s (size=%s)" % [session_id, data.size()])
56
+ _store_scene_tree(session_id, data)
57
+ return true
58
+ elif normalized == "%s:result" % EVAL_CAPTURE_NAME:
59
+ _trace("received runtime eval result for session %s" % session_id)
60
+ _store_eval_result(session_id, data)
61
+ return true
62
+ elif normalized == "%s:result" % INPUT_CAPTURE_NAME:
63
+ _trace("received input result for session %s" % session_id)
64
+ _store_input_result(session_id, data)
65
+ return true
66
+ return false
67
+
68
+ func request_runtime_scene_snapshot() -> Dictionary:
69
+ var active_sessions := _get_active_session_ids()
70
+ _trace("active sessions: %s" % active_sessions)
71
+ if active_sessions.is_empty():
72
+ return { "error": "No active runtime session. Start the project or attach the debugger first." }
73
+ var session_id: int = active_sessions[0]
74
+ _ensure_session(session_id)
75
+
76
+ var state: Dictionary = _sessions[session_id]
77
+ var baseline_version: int = state.get("tree_version", 0)
78
+ _request_scene_tree(session_id)
79
+
80
+ return {
81
+ "session_id": session_id,
82
+ "baseline_version": baseline_version
83
+ }
84
+
85
+ func has_new_runtime_snapshot(session_id: int, baseline_version: int) -> bool:
86
+ if not _sessions.has(session_id):
87
+ return false
88
+ var state: Dictionary = _sessions[session_id]
89
+ return state.get("tree_version", 0) > baseline_version and state.get("tree")
90
+
91
+ func build_runtime_snapshot(session_id: int, options: Dictionary = {}) -> Dictionary:
92
+ if not _sessions.has(session_id):
93
+ return {}
94
+ var state: Dictionary = _sessions[session_id]
95
+ if not state.get("tree"):
96
+ return {}
97
+ return _build_response(state["tree"], options)
98
+
99
+ func evaluate_runtime_expression(expression: String, options: Dictionary = {}) -> Dictionary:
100
+ var trimmed := expression.strip_edges()
101
+ if trimmed.is_empty():
102
+ return { "error": "Expression cannot be empty." }
103
+
104
+ var active_sessions := _get_active_session_ids()
105
+ if active_sessions.is_empty():
106
+ return { "error": "No active runtime session. Start the project or attach the debugger first." }
107
+
108
+ var session_id: int = active_sessions[0]
109
+ _ensure_session(session_id)
110
+ var session := get_session(session_id)
111
+ if session == null or not session.is_active():
112
+ return { "error": "Runtime debugger session is not active." }
113
+
114
+ var request_id: int = _next_eval_request_id
115
+ _next_eval_request_id += 1
116
+
117
+ var payload := Array()
118
+ payload.append(request_id)
119
+ payload.append(trimmed)
120
+ if typeof(options) == TYPE_DICTIONARY:
121
+ payload.append(options.duplicate(true))
122
+ else:
123
+ payload.append({})
124
+
125
+ _trace("sending runtime eval request %s to session %s" % [request_id, session_id])
126
+ session.send_message("%s:evaluate" % EVAL_CAPTURE_NAME, payload)
127
+
128
+ return {
129
+ "session_id": session_id,
130
+ "request_id": request_id
131
+ }
132
+
133
+ func has_eval_result(session_id: int, request_id: int) -> bool:
134
+ if not _sessions.has(session_id):
135
+ return false
136
+ var state: Dictionary = _sessions[session_id]
137
+ var results: Dictionary = state.get("eval_results", {})
138
+ return results.has(request_id)
139
+
140
+ func take_eval_result(session_id: int, request_id: int) -> Dictionary:
141
+ if not _sessions.has(session_id):
142
+ return {}
143
+ var state: Dictionary = _sessions[session_id]
144
+ var results: Dictionary = state.get("eval_results", {})
145
+ if not results.has(request_id):
146
+ return {}
147
+ var payload: Variant = results[request_id]
148
+ results.erase(request_id)
149
+ state["eval_results"] = results
150
+ _sessions[session_id] = state
151
+
152
+ var response := {}
153
+ if typeof(payload) == TYPE_DICTIONARY:
154
+ response = payload.duplicate(true)
155
+ if response.has("_received_at"):
156
+ response.erase("_received_at")
157
+ return response
158
+
159
+ func _get_active_session_ids() -> Array:
160
+ var result: Array = []
161
+ var sessions := get_sessions()
162
+ _trace("get_sessions count=%s" % sessions.size())
163
+ for i in range(sessions.size()):
164
+ var session = sessions[i]
165
+ if session and session.has_method("is_active") and session.is_active():
166
+ _ensure_session(i)
167
+ result.append(i)
168
+ return result
169
+
170
+ func _request_scene_tree(session_id: int) -> void:
171
+ var session := get_session(session_id)
172
+ _trace("request_scene_tree session=%s session=%s" % [session_id, session])
173
+ if session and session.is_active():
174
+ var payload := Array()
175
+ payload.push_back("")
176
+ payload.push_back(Array())
177
+ for prefix in SCENE_CAPTURE_NAMES:
178
+ payload[0] = "%s:scene_tree" % prefix
179
+ session.send_message("request_message", payload)
180
+ var state: Dictionary = _sessions.get(session_id, {})
181
+ state["last_request_time"] = Time.get_ticks_msec()
182
+ _sessions[session_id] = state
183
+ else:
184
+ _trace("session inactive, cannot request scene tree")
185
+
186
+ func _store_scene_tree(session_id: int, payload: Array) -> void:
187
+ if payload.is_empty():
188
+ return
189
+ var parsed := _parse_remote_tree(payload)
190
+ if parsed.is_empty():
191
+ return
192
+
193
+ var state: Dictionary = _sessions.get(session_id, {})
194
+ state["tree"] = parsed
195
+ state["tree_version"] = state.get("tree_version", 0) + 1
196
+ state["last_update"] = Time.get_ticks_msec()
197
+ _sessions[session_id] = state
198
+
199
+ scene_tree_updated.emit(session_id)
200
+
201
+ func _store_eval_result(session_id: int, payload: Array) -> void:
202
+ _ensure_session(session_id)
203
+ if payload.is_empty():
204
+ _trace("runtime eval payload empty")
205
+ return
206
+
207
+ var entry: Variant = payload[0]
208
+ if typeof(entry) != TYPE_DICTIONARY:
209
+ _trace("runtime eval payload not dictionary")
210
+ return
211
+
212
+ var result_dict: Dictionary = entry.duplicate(true)
213
+ var request_id := int(result_dict.get("request_id", -1))
214
+ if request_id < 0:
215
+ _trace("runtime eval payload missing request_id")
216
+ return
217
+
218
+ if result_dict.has("output"):
219
+ var raw_output = result_dict["output"]
220
+ var normalized_output: Array = []
221
+ if typeof(raw_output) == TYPE_ARRAY:
222
+ for item in raw_output:
223
+ normalized_output.append(str(item))
224
+ elif typeof(raw_output) == TYPE_PACKED_STRING_ARRAY:
225
+ for item in raw_output:
226
+ normalized_output.append(str(item))
227
+ elif raw_output == null:
228
+ pass
229
+ else:
230
+ normalized_output.append(str(raw_output))
231
+ result_dict["output"] = normalized_output
232
+ else:
233
+ result_dict["output"] = []
234
+
235
+ result_dict["_received_at"] = Time.get_ticks_msec()
236
+
237
+ var state: Dictionary = _sessions.get(session_id, {})
238
+ var results: Dictionary = state.get("eval_results", {})
239
+ results[request_id] = result_dict
240
+ state["eval_results"] = results
241
+ _sessions[session_id] = state
242
+
243
+ runtime_eval_completed.emit(session_id, request_id)
244
+
245
+ func _parse_remote_tree(flat_data: Array) -> Dictionary:
246
+ if flat_data.size() % 6 != 0:
247
+ return {}
248
+
249
+ var nodes: Array = []
250
+ var index := 0
251
+ while index < flat_data.size():
252
+ var node := {
253
+ "child_count": int(flat_data[index]),
254
+ "name": str(flat_data[index + 1]),
255
+ "type": str(flat_data[index + 2]),
256
+ "object_id": int(flat_data[index + 3]),
257
+ "scene_file_path": str(flat_data[index + 4]),
258
+ "view_flags": int(flat_data[index + 5]),
259
+ "children": []
260
+ }
261
+ nodes.append(node)
262
+ index += 6
263
+ if nodes.is_empty():
264
+ return {}
265
+
266
+ var stack: Array = []
267
+ var root: Dictionary = nodes[0]
268
+ root["_remaining"] = root["child_count"]
269
+ stack.append(root)
270
+
271
+ for i in range(1, nodes.size()):
272
+ var current: Dictionary = nodes[i]
273
+ current["_remaining"] = current["child_count"]
274
+ while not stack.is_empty() and stack.back()["_remaining"] <= 0:
275
+ stack.pop_back()
276
+ if stack.is_empty():
277
+ # Malformed stream; abort to avoid inconsistent data.
278
+ return {}
279
+ var parent: Dictionary = stack.back()
280
+ parent["children"].append(current)
281
+ parent["_remaining"] -= 1
282
+ if current["_remaining"] > 0:
283
+ stack.append(current)
284
+
285
+ while not stack.is_empty():
286
+ stack.pop_back()
287
+
288
+ _cleanup_internal_keys(root)
289
+ _assign_paths(root, "")
290
+ return root
291
+
292
+ func _cleanup_internal_keys(node: Dictionary) -> void:
293
+ node.erase("_remaining")
294
+ for child in node["children"]:
295
+ _cleanup_internal_keys(child)
296
+
297
+ func _assign_paths(node: Dictionary, parent_path: String) -> void:
298
+ var name: String = node.get("name", "")
299
+ var current_path := ""
300
+ if parent_path.is_empty():
301
+ current_path = "/root/%s" % name
302
+ else:
303
+ current_path = "%s/%s" % [parent_path, name]
304
+ node["path"] = current_path
305
+
306
+ var view_flags: int = node.get("view_flags", 0)
307
+ node["visibility"] = {
308
+ "has_visible_method": bool(view_flags & VIEW_HAS_VISIBLE_METHOD),
309
+ "visible": bool(view_flags & VIEW_VISIBLE),
310
+ "visible_in_tree": bool(view_flags & VIEW_VISIBLE_IN_TREE)
311
+ }
312
+
313
+ for child in node["children"]:
314
+ _assign_paths(child, current_path)
315
+
316
+ func _build_response(root: Dictionary, options: Dictionary) -> Dictionary:
317
+ var max_depth: int = options.get("max_depth", -1)
318
+ var include_props := options.get("include_properties", false)
319
+ var include_scripts := options.get("include_scripts", false)
320
+
321
+ var response := {
322
+ "scene_path": root.get("scene_file_path", ""),
323
+ "root_node_name": root.get("name", ""),
324
+ "root_node_type": root.get("type", ""),
325
+ "runtime": true,
326
+ "structure": _project_node(root, 0, max_depth)
327
+ }
328
+
329
+ if include_props:
330
+ response["warning_properties"] = "Runtime inspection does not expose live properties yet."
331
+ if include_scripts:
332
+ response["warning_scripts"] = "Runtime inspection does not expose script metadata yet."
333
+
334
+ return response
335
+
336
+ func _project_node(node: Dictionary, depth: int, max_depth: int) -> Dictionary:
337
+ var projected := {
338
+ "name": node.get("name", ""),
339
+ "type": node.get("type", ""),
340
+ "path": node.get("path", ""),
341
+ "object_id": node.get("object_id", 0),
342
+ "scene_file_path": node.get("scene_file_path", ""),
343
+ "visibility": node.get("visibility", {}),
344
+ "children": []
345
+ }
346
+
347
+ if max_depth >= 0 and depth >= max_depth:
348
+ return projected
349
+
350
+ for child in node["children"]:
351
+ projected["children"].append(_project_node(child, depth + 1, max_depth))
352
+
353
+ return projected
354
+
355
+ func _ensure_session(session_id: int) -> void:
356
+ if not _sessions.has(session_id):
357
+ _sessions[session_id] = {
358
+ "tree": null,
359
+ "tree_version": 0,
360
+ "last_update": 0,
361
+ "active": false,
362
+ "eval_results": {},
363
+ "input_results": {}
364
+ }
365
+
366
+ func _on_session_started(session_id: int) -> void:
367
+ _ensure_session(session_id)
368
+ var state: Dictionary = _sessions[session_id]
369
+ state["active"] = true
370
+ _sessions[session_id] = state
371
+ _trace("session %s started" % session_id)
372
+
373
+ func _on_session_stopped(session_id: int) -> void:
374
+ _ensure_session(session_id)
375
+ var state: Dictionary = _sessions[session_id]
376
+ state["active"] = false
377
+ _sessions[session_id] = state
378
+ _trace("session %s stopped" % session_id)
379
+
380
+ func _on_session_breaked(can_debug: bool, session_id: int) -> void:
381
+ _ensure_session(session_id)
382
+ var state: Dictionary = _sessions[session_id]
383
+ state["can_debug"] = can_debug
384
+ _sessions[session_id] = state
385
+ _trace("session %s breaked can_debug=%s" % [session_id, can_debug])
386
+
387
+ func _normalize_capture_name(message: String) -> String:
388
+ for prefix in SCENE_CAPTURE_NAMES:
389
+ var needle := "%s:" % prefix
390
+ if message.begins_with(needle):
391
+ var suffix := message.substr(needle.length())
392
+ if suffix == "scene_tree":
393
+ return "scene:scene_tree"
394
+ return "%s:%s" % [prefix, suffix]
395
+ if message.begins_with("%s:" % EVAL_CAPTURE_NAME):
396
+ var eval_suffix := message.substr(EVAL_CAPTURE_NAME.length() + 1)
397
+ return "%s:%s" % [EVAL_CAPTURE_NAME, eval_suffix]
398
+ if message.begins_with("%s:" % INPUT_CAPTURE_NAME):
399
+ var input_suffix := message.substr(INPUT_CAPTURE_NAME.length() + 1)
400
+ return "%s:%s" % [INPUT_CAPTURE_NAME, input_suffix]
401
+ if message.ends_with(":scene_tree"):
402
+ return "scene:scene_tree"
403
+ return message
404
+
405
+ func _trace(text: String) -> void:
406
+ if OS.is_stdout_verbose():
407
+ print("[RuntimeBridge] %s" % text)
408
+
409
+ # Input simulation result handling
410
+
411
+ func _store_input_result(session_id: int, payload: Array) -> void:
412
+ _ensure_session(session_id)
413
+ if payload.is_empty():
414
+ _trace("input result payload empty")
415
+ return
416
+
417
+ var entry: Variant = payload[0]
418
+ if typeof(entry) != TYPE_DICTIONARY:
419
+ _trace("input result payload not dictionary")
420
+ return
421
+
422
+ var result_dict: Dictionary = entry.duplicate(true)
423
+ var request_id := int(result_dict.get("request_id", -1))
424
+ if request_id < 0:
425
+ _trace("input result payload missing request_id")
426
+ return
427
+
428
+ result_dict["_received_at"] = Time.get_ticks_msec()
429
+
430
+ var state: Dictionary = _sessions.get(session_id, {})
431
+ var results: Dictionary = state.get("input_results", {})
432
+ results[request_id] = result_dict
433
+ state["input_results"] = results
434
+ _sessions[session_id] = state
435
+
436
+ input_result_received.emit(session_id, request_id)
437
+
438
+
439
+ func has_input_result(session_id: int, request_id: int) -> bool:
440
+ if not _sessions.has(session_id):
441
+ return false
442
+ var state: Dictionary = _sessions[session_id]
443
+ var results: Dictionary = state.get("input_results", {})
444
+ return results.has(request_id)
445
+
446
+
447
+ func take_input_result(session_id: int, request_id: int) -> Dictionary:
448
+ if not _sessions.has(session_id):
449
+ return {}
450
+ var state: Dictionary = _sessions[session_id]
451
+ var results: Dictionary = state.get("input_results", {})
452
+ if not results.has(request_id):
453
+ return {}
454
+ var payload: Variant = results[request_id]
455
+ results.erase(request_id)
456
+ state["input_results"] = results
457
+ _sessions[session_id] = state
458
+
459
+ var response := {}
460
+ if typeof(payload) == TYPE_DICTIONARY:
461
+ response = payload.duplicate(true)
462
+ if response.has("_received_at"):
463
+ response.erase("_received_at")
464
+ return response
@@ -0,0 +1 @@
1
+ uid://c4vuvdpmhh01l
@@ -0,0 +1,165 @@
1
+ @tool
2
+ class_name MCPScriptResourceCommands
3
+ extends MCPBaseCommandProcessor
4
+
5
+ func process_command(client_id: int, command_type: String, params: Dictionary, command_id: String) -> bool:
6
+ match command_type:
7
+ "get_script":
8
+ _handle_get_script(client_id, params, command_id)
9
+ return true
10
+ "edit_script":
11
+ _handle_edit_script(client_id, params, command_id)
12
+ return true
13
+ return false # Command not handled
14
+
15
+ func _handle_get_script(client_id: int, params: Dictionary, command_id: String) -> void:
16
+ var path = params.get("path", "")
17
+ var node_path = params.get("node_path", "")
18
+
19
+ # Handle based on which parameter is provided
20
+ var script_path = ""
21
+ var result = {}
22
+
23
+ if not path.is_empty():
24
+ # Direct script path provided
25
+ result = _get_script_by_path(path)
26
+ elif not node_path.is_empty():
27
+ # Node path provided, get attached script
28
+ result = _get_script_by_node(node_path)
29
+ else:
30
+ result = {
31
+ "error": "Either script_path or node_path must be provided",
32
+ "script_found": false
33
+ }
34
+
35
+ _send_success(client_id, result, command_id)
36
+
37
+ func _get_script_by_path(script_path: String) -> Dictionary:
38
+ var normalized_path = _normalize_script_path(script_path)
39
+
40
+ if normalized_path.is_empty():
41
+ return {
42
+ "error": "Script path is required",
43
+ "script_found": false
44
+ }
45
+
46
+ if not FileAccess.file_exists(normalized_path):
47
+ return {
48
+ "error": "Script file not found",
49
+ "script_found": false
50
+ }
51
+
52
+ var file = FileAccess.open(normalized_path, FileAccess.READ)
53
+ if not file:
54
+ return {
55
+ "error": "Failed to open script file",
56
+ "script_found": false
57
+ }
58
+
59
+ var content = file.get_as_text()
60
+ return {
61
+ "script_found": true,
62
+ "script_path": normalized_path,
63
+ "content": content
64
+ }
65
+
66
+ func _get_script_by_node(node_path: String) -> Dictionary:
67
+ # Get editor plugin and interfaces
68
+ var plugin = Engine.get_meta("GodotMCPPlugin")
69
+ if not plugin:
70
+ return {
71
+ "error": "GodotMCPPlugin not found in Engine metadata",
72
+ "script_found": false
73
+ }
74
+
75
+ var editor_interface = plugin.get_editor_interface()
76
+ var edited_scene_root = editor_interface.get_edited_scene_root()
77
+
78
+ if not edited_scene_root:
79
+ return {
80
+ "error": "No scene is currently being edited",
81
+ "script_found": false
82
+ }
83
+
84
+ var node = edited_scene_root.get_node_or_null(node_path)
85
+ if not node:
86
+ return {
87
+ "error": "Node not found",
88
+ "script_found": false
89
+ }
90
+
91
+ var script = node.get_script()
92
+ if not script:
93
+ return {
94
+ "error": "Node has no script attached",
95
+ "script_found": false
96
+ }
97
+
98
+ var script_path = script.resource_path
99
+ return _get_script_by_path(script_path)
100
+
101
+ func _handle_edit_script(client_id: int, params: Dictionary, command_id: String) -> void:
102
+ var script_path = params.get("script_path", "")
103
+
104
+ var result = {}
105
+
106
+ if script_path.is_empty():
107
+ result = {
108
+ "error": "Script path is required",
109
+ "success": false
110
+ }
111
+ elif not params.has("content"):
112
+ result = {
113
+ "error": "Content is required",
114
+ "success": false
115
+ }
116
+ else:
117
+ var content = params.get("content")
118
+ if content == null:
119
+ result = {
120
+ "error": "Content is required",
121
+ "success": false
122
+ }
123
+ else:
124
+ result = _edit_script_content(script_path, str(content))
125
+
126
+ _send_success(client_id, result, command_id)
127
+
128
+ func _edit_script_content(script_path: String, content: String) -> Dictionary:
129
+ var normalized_path = _normalize_script_path(script_path)
130
+
131
+ var file = FileAccess.open(normalized_path, FileAccess.WRITE)
132
+ if not file:
133
+ return {
134
+ "error": "Failed to open script file for writing",
135
+ "success": false
136
+ }
137
+
138
+ file.store_string(content)
139
+
140
+ # Open the script in the editor if possible
141
+ var plugin = Engine.get_meta("GodotMCPPlugin")
142
+ if plugin:
143
+ var editor_interface = plugin.get_editor_interface()
144
+ var script = load(normalized_path)
145
+ if script:
146
+ editor_interface.edit_resource(script)
147
+
148
+ return {
149
+ "success": true,
150
+ "script_path": normalized_path
151
+ }
152
+
153
+ func _normalize_script_path(script_path: String) -> String:
154
+ var normalized_path = script_path
155
+
156
+ if normalized_path.is_empty():
157
+ return normalized_path
158
+
159
+ if not normalized_path.begins_with("res://"):
160
+ normalized_path = "res://" + normalized_path
161
+
162
+ if normalized_path.get_extension().is_empty():
163
+ normalized_path += ".gd"
164
+
165
+ return normalized_path
@@ -0,0 +1 @@
1
+ uid://cpsu8nnmrh2oc