@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.
- package/README.md +187 -0
- package/addons/godot_mcp/command_handler.gd +161 -0
- package/addons/godot_mcp/command_handler.gd.uid +1 -0
- package/addons/godot_mcp/commands/base_command_processor.gd +221 -0
- package/addons/godot_mcp/commands/base_command_processor.gd.uid +1 -0
- package/addons/godot_mcp/commands/debugger_commands.gd +221 -0
- package/addons/godot_mcp/commands/debugger_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/editor_commands.gd +237 -0
- package/addons/godot_mcp/commands/editor_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/editor_script_commands.gd +365 -0
- package/addons/godot_mcp/commands/editor_script_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/input_commands.gd +337 -0
- package/addons/godot_mcp/commands/input_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/node_commands.gd +222 -0
- package/addons/godot_mcp/commands/node_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/project_commands.gd +298 -0
- package/addons/godot_mcp/commands/project_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/scene_commands.gd +337 -0
- package/addons/godot_mcp/commands/scene_commands.gd.uid +1 -0
- package/addons/godot_mcp/commands/script_commands.gd +349 -0
- package/addons/godot_mcp/commands/script_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_asset_commands.gd +153 -0
- package/addons/godot_mcp/mcp_asset_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_debug_output_publisher.gd +1669 -0
- package/addons/godot_mcp/mcp_debug_output_publisher.gd.uid +1 -0
- package/addons/godot_mcp/mcp_debugger_bridge.gd +1455 -0
- package/addons/godot_mcp/mcp_debugger_bridge.gd.uid +1 -0
- package/addons/godot_mcp/mcp_enhanced_commands.gd +1083 -0
- package/addons/godot_mcp/mcp_enhanced_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_input_handler.gd +545 -0
- package/addons/godot_mcp/mcp_input_handler.gd.uid +1 -0
- package/addons/godot_mcp/mcp_runtime_debugger_bridge.gd +464 -0
- package/addons/godot_mcp/mcp_runtime_debugger_bridge.gd.uid +1 -0
- package/addons/godot_mcp/mcp_script_resource_commands.gd +165 -0
- package/addons/godot_mcp/mcp_script_resource_commands.gd.uid +1 -0
- package/addons/godot_mcp/mcp_server.gd +260 -0
- package/addons/godot_mcp/mcp_server.gd.uid +1 -0
- package/addons/godot_mcp/plugin.cfg +7 -0
- package/addons/godot_mcp/runtime_debugger.gd +81 -0
- package/addons/godot_mcp/runtime_debugger.gd.uid +1 -0
- package/addons/godot_mcp/ui/mcp_panel.gd +94 -0
- package/addons/godot_mcp/ui/mcp_panel.gd.uid +1 -0
- package/addons/godot_mcp/ui/mcp_panel.tscn +96 -0
- package/addons/godot_mcp/utils/node_utils.gd +82 -0
- package/addons/godot_mcp/utils/node_utils.gd.uid +1 -0
- package/addons/godot_mcp/utils/resource_utils.gd +81 -0
- package/addons/godot_mcp/utils/resource_utils.gd.uid +1 -0
- package/addons/godot_mcp/utils/script_utils.gd +114 -0
- package/addons/godot_mcp/utils/script_utils.gd.uid +1 -0
- package/addons/godot_mcp/websocket_server.gd +197 -0
- package/addons/godot_mcp/websocket_server.gd.uid +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +561 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +156 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/asset_resources.d.ts +29 -0
- package/dist/resources/asset_resources.js +145 -0
- package/dist/resources/asset_resources.js.map +1 -0
- package/dist/resources/debug_resources.d.ts +11 -0
- package/dist/resources/debug_resources.js +106 -0
- package/dist/resources/debug_resources.js.map +1 -0
- package/dist/resources/debugger_resources.d.ts +62 -0
- package/dist/resources/debugger_resources.js +201 -0
- package/dist/resources/debugger_resources.js.map +1 -0
- package/dist/resources/editor_resources.d.ts +47 -0
- package/dist/resources/editor_resources.js +155 -0
- package/dist/resources/editor_resources.js.map +1 -0
- package/dist/resources/project_resources.d.ts +33 -0
- package/dist/resources/project_resources.js +137 -0
- package/dist/resources/project_resources.js.map +1 -0
- package/dist/resources/scene_resources.d.ts +33 -0
- package/dist/resources/scene_resources.js +160 -0
- package/dist/resources/scene_resources.js.map +1 -0
- package/dist/resources/script_resources.d.ts +51 -0
- package/dist/resources/script_resources.js +203 -0
- package/dist/resources/script_resources.js.map +1 -0
- package/dist/tools/asset_tools.d.ts +5 -0
- package/dist/tools/asset_tools.js +125 -0
- package/dist/tools/asset_tools.js.map +1 -0
- package/dist/tools/debugger_tools.d.ts +2 -0
- package/dist/tools/debugger_tools.js +342 -0
- package/dist/tools/debugger_tools.js.map +1 -0
- package/dist/tools/editor_tools.d.ts +2 -0
- package/dist/tools/editor_tools.js +165 -0
- package/dist/tools/editor_tools.js.map +1 -0
- package/dist/tools/enhanced_tools.d.ts +5 -0
- package/dist/tools/enhanced_tools.js +706 -0
- package/dist/tools/enhanced_tools.js.map +1 -0
- package/dist/tools/input_tools.d.ts +2 -0
- package/dist/tools/input_tools.js +408 -0
- package/dist/tools/input_tools.js.map +1 -0
- package/dist/tools/node_tools.d.ts +5 -0
- package/dist/tools/node_tools.js +217 -0
- package/dist/tools/node_tools.js.map +1 -0
- package/dist/tools/project_tools.d.ts +5 -0
- package/dist/tools/project_tools.js +162 -0
- package/dist/tools/project_tools.js.map +1 -0
- package/dist/tools/scene_tools.d.ts +5 -0
- package/dist/tools/scene_tools.js +260 -0
- package/dist/tools/scene_tools.js.map +1 -0
- package/dist/tools/script_resource_tools.d.ts +5 -0
- package/dist/tools/script_resource_tools.js +5 -0
- package/dist/tools/script_resource_tools.js.map +1 -0
- package/dist/tools/script_tools.d.ts +5 -0
- package/dist/tools/script_tools.js +154 -0
- package/dist/tools/script_tools.js.map +1 -0
- package/dist/utils/godot_connection.d.ts +30 -0
- package/dist/utils/godot_connection.js +285 -0
- package/dist/utils/godot_connection.js.map +1 -0
- package/dist/utils/types.d.ts +16 -0
- package/dist/utils/types.js +2 -0
- package/dist/utils/types.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
class_name MCPSceneCommands
|
|
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
|
+
"save_scene":
|
|
8
|
+
_save_scene(client_id, params, command_id)
|
|
9
|
+
return true
|
|
10
|
+
"open_scene":
|
|
11
|
+
_open_scene(client_id, params, command_id)
|
|
12
|
+
return true
|
|
13
|
+
"get_current_scene":
|
|
14
|
+
_get_current_scene(client_id, params, command_id)
|
|
15
|
+
return true
|
|
16
|
+
"get_scene_structure":
|
|
17
|
+
_get_scene_structure(client_id, params, command_id)
|
|
18
|
+
return true
|
|
19
|
+
"create_scene":
|
|
20
|
+
_create_scene(client_id, params, command_id)
|
|
21
|
+
return true
|
|
22
|
+
"delete_scene":
|
|
23
|
+
_delete_scene(client_id, params, command_id)
|
|
24
|
+
return true
|
|
25
|
+
return false # Command not handled
|
|
26
|
+
|
|
27
|
+
func _save_scene(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
28
|
+
var path = params.get("path", "")
|
|
29
|
+
|
|
30
|
+
# Get editor plugin and interfaces
|
|
31
|
+
var plugin = Engine.get_meta("GodotMCPPlugin")
|
|
32
|
+
if not plugin:
|
|
33
|
+
return _send_error(client_id, "GodotMCPPlugin not found in Engine metadata", command_id)
|
|
34
|
+
|
|
35
|
+
var editor_interface = plugin.get_editor_interface()
|
|
36
|
+
var edited_scene_root = editor_interface.get_edited_scene_root()
|
|
37
|
+
|
|
38
|
+
# If no path provided, use the current scene path
|
|
39
|
+
if path.is_empty() and edited_scene_root:
|
|
40
|
+
path = edited_scene_root.scene_file_path
|
|
41
|
+
|
|
42
|
+
# Validation
|
|
43
|
+
if path.is_empty():
|
|
44
|
+
return _send_error(client_id, "Scene path cannot be empty", command_id)
|
|
45
|
+
|
|
46
|
+
# Make sure we have an absolute path
|
|
47
|
+
if not path.begins_with("res://"):
|
|
48
|
+
path = "res://" + path
|
|
49
|
+
|
|
50
|
+
if not path.ends_with(".tscn"):
|
|
51
|
+
path += ".tscn"
|
|
52
|
+
|
|
53
|
+
# Check if we have an edited scene
|
|
54
|
+
if not edited_scene_root:
|
|
55
|
+
return _send_error(client_id, "No scene is currently being edited", command_id)
|
|
56
|
+
|
|
57
|
+
# Save the scene
|
|
58
|
+
var packed_scene = PackedScene.new()
|
|
59
|
+
var result = packed_scene.pack(edited_scene_root)
|
|
60
|
+
if result != OK:
|
|
61
|
+
return _send_error(client_id, "Failed to pack scene: %d" % result, command_id)
|
|
62
|
+
|
|
63
|
+
result = ResourceSaver.save(packed_scene, path)
|
|
64
|
+
if result != OK:
|
|
65
|
+
return _send_error(client_id, "Failed to save scene: %d" % result, command_id)
|
|
66
|
+
|
|
67
|
+
_send_success(client_id, {
|
|
68
|
+
"scene_path": path
|
|
69
|
+
}, command_id)
|
|
70
|
+
|
|
71
|
+
func _open_scene(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
72
|
+
var path = params.get("path", "")
|
|
73
|
+
|
|
74
|
+
# Validation
|
|
75
|
+
if path.is_empty():
|
|
76
|
+
return _send_error(client_id, "Scene path cannot be empty", command_id)
|
|
77
|
+
|
|
78
|
+
# Make sure we have an absolute path
|
|
79
|
+
if not path.begins_with("res://"):
|
|
80
|
+
path = "res://" + path
|
|
81
|
+
|
|
82
|
+
# Check if the file exists
|
|
83
|
+
if not FileAccess.file_exists(path):
|
|
84
|
+
return _send_error(client_id, "Scene file not found: %s" % path, command_id)
|
|
85
|
+
|
|
86
|
+
# Since we can't directly open scenes in tool scripts,
|
|
87
|
+
# we need to defer to the plugin which has access to EditorInterface
|
|
88
|
+
var plugin = Engine.get_meta("GodotMCPPlugin") if Engine.has_meta("GodotMCPPlugin") else null
|
|
89
|
+
|
|
90
|
+
if plugin and plugin.has_method("get_editor_interface"):
|
|
91
|
+
var editor_interface = plugin.get_editor_interface()
|
|
92
|
+
editor_interface.open_scene_from_path(path)
|
|
93
|
+
_send_success(client_id, {
|
|
94
|
+
"scene_path": path
|
|
95
|
+
}, command_id)
|
|
96
|
+
else:
|
|
97
|
+
_send_error(client_id, "Cannot access EditorInterface. Please open the scene manually: %s" % path, command_id)
|
|
98
|
+
|
|
99
|
+
func _get_current_scene(client_id: int, _params: Dictionary, command_id: String) -> void:
|
|
100
|
+
# Get editor plugin and interfaces
|
|
101
|
+
var plugin = Engine.get_meta("GodotMCPPlugin")
|
|
102
|
+
if not plugin:
|
|
103
|
+
return _send_error(client_id, "GodotMCPPlugin not found in Engine metadata", command_id)
|
|
104
|
+
|
|
105
|
+
var editor_interface = plugin.get_editor_interface()
|
|
106
|
+
var edited_scene_root = editor_interface.get_edited_scene_root()
|
|
107
|
+
|
|
108
|
+
if not edited_scene_root:
|
|
109
|
+
print("No scene is currently being edited")
|
|
110
|
+
# Instead of returning an error, return a valid response with empty/default values
|
|
111
|
+
_send_success(client_id, {
|
|
112
|
+
"scene_path": "None",
|
|
113
|
+
"root_node_type": "None",
|
|
114
|
+
"root_node_name": "None"
|
|
115
|
+
}, command_id)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
var scene_path = edited_scene_root.scene_file_path
|
|
119
|
+
if scene_path.is_empty():
|
|
120
|
+
scene_path = "Untitled"
|
|
121
|
+
|
|
122
|
+
print("Current scene path: ", scene_path)
|
|
123
|
+
print("Root node type: ", edited_scene_root.get_class())
|
|
124
|
+
print("Root node name: ", edited_scene_root.name)
|
|
125
|
+
|
|
126
|
+
_send_success(client_id, {
|
|
127
|
+
"scene_path": scene_path,
|
|
128
|
+
"root_node_type": edited_scene_root.get_class(),
|
|
129
|
+
"root_node_name": edited_scene_root.name
|
|
130
|
+
}, command_id)
|
|
131
|
+
|
|
132
|
+
func _get_scene_structure(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
133
|
+
var path = params.get("path", "")
|
|
134
|
+
|
|
135
|
+
# Validation
|
|
136
|
+
if path.is_empty():
|
|
137
|
+
return _send_error(client_id, "Scene path cannot be empty", command_id)
|
|
138
|
+
|
|
139
|
+
if not path.begins_with("res://"):
|
|
140
|
+
path = "res://" + path
|
|
141
|
+
|
|
142
|
+
if not FileAccess.file_exists(path):
|
|
143
|
+
return _send_error(client_id, "Scene file not found: " + path, command_id)
|
|
144
|
+
|
|
145
|
+
# Load the scene to analyze its structure
|
|
146
|
+
var packed_scene = load(path)
|
|
147
|
+
if not packed_scene:
|
|
148
|
+
return _send_error(client_id, "Failed to load scene: " + path, command_id)
|
|
149
|
+
|
|
150
|
+
# Create a temporary instance to analyze
|
|
151
|
+
var scene_instance = packed_scene.instantiate()
|
|
152
|
+
if not scene_instance:
|
|
153
|
+
return _send_error(client_id, "Failed to instantiate scene: " + path, command_id)
|
|
154
|
+
|
|
155
|
+
# Get the scene structure
|
|
156
|
+
var structure = _get_node_structure(scene_instance)
|
|
157
|
+
|
|
158
|
+
# Clean up the temporary instance
|
|
159
|
+
scene_instance.queue_free()
|
|
160
|
+
|
|
161
|
+
# Return the structure
|
|
162
|
+
_send_success(client_id, {
|
|
163
|
+
"path": path,
|
|
164
|
+
"structure": structure
|
|
165
|
+
}, command_id)
|
|
166
|
+
|
|
167
|
+
func _get_node_structure(node: Node) -> Dictionary:
|
|
168
|
+
var structure = {
|
|
169
|
+
"name": node.name,
|
|
170
|
+
"type": node.get_class(),
|
|
171
|
+
"path": node.get_path()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Get script information
|
|
175
|
+
var script = node.get_script()
|
|
176
|
+
if script:
|
|
177
|
+
structure["script"] = script.resource_path
|
|
178
|
+
|
|
179
|
+
# Get important properties
|
|
180
|
+
var properties = {}
|
|
181
|
+
var property_list = node.get_property_list()
|
|
182
|
+
|
|
183
|
+
for prop in property_list:
|
|
184
|
+
var name = prop["name"]
|
|
185
|
+
# Filter to include only the most useful properties
|
|
186
|
+
if not name.begins_with("_") and name not in ["script", "children", "position", "rotation", "scale"]:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
# Skip properties that are default values
|
|
190
|
+
if name == "position" and node.position == Vector2():
|
|
191
|
+
continue
|
|
192
|
+
if name == "rotation" and node.rotation == 0:
|
|
193
|
+
continue
|
|
194
|
+
if name == "scale" and node.scale == Vector2(1, 1):
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
properties[name] = node.get(name)
|
|
198
|
+
|
|
199
|
+
structure["properties"] = properties
|
|
200
|
+
|
|
201
|
+
# Get children
|
|
202
|
+
var children = []
|
|
203
|
+
for child in node.get_children():
|
|
204
|
+
children.append(_get_node_structure(child))
|
|
205
|
+
|
|
206
|
+
structure["children"] = children
|
|
207
|
+
|
|
208
|
+
return structure
|
|
209
|
+
|
|
210
|
+
func _create_scene(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
211
|
+
var path = params.get("path", "")
|
|
212
|
+
var root_node_type = params.get("root_node_type", "Node")
|
|
213
|
+
|
|
214
|
+
# Validation
|
|
215
|
+
if path.is_empty():
|
|
216
|
+
return _send_error(client_id, "Scene path cannot be empty", command_id)
|
|
217
|
+
|
|
218
|
+
# Make sure we have an absolute path
|
|
219
|
+
if not path.begins_with("res://"):
|
|
220
|
+
path = "res://" + path
|
|
221
|
+
|
|
222
|
+
# Ensure path ends with .tscn
|
|
223
|
+
if not path.ends_with(".tscn"):
|
|
224
|
+
path += ".tscn"
|
|
225
|
+
|
|
226
|
+
# Create directory structure if it doesn't exist
|
|
227
|
+
var dir_path = path.get_base_dir()
|
|
228
|
+
if not DirAccess.dir_exists_absolute(dir_path):
|
|
229
|
+
var dir = DirAccess.open("res://")
|
|
230
|
+
if dir:
|
|
231
|
+
dir.make_dir_recursive(dir_path.trim_prefix("res://"))
|
|
232
|
+
|
|
233
|
+
# Check if file already exists
|
|
234
|
+
if FileAccess.file_exists(path):
|
|
235
|
+
return _send_error(client_id, "Scene file already exists: %s" % path, command_id)
|
|
236
|
+
|
|
237
|
+
# Create the root node of the specified type
|
|
238
|
+
var root_node = null
|
|
239
|
+
|
|
240
|
+
match root_node_type:
|
|
241
|
+
"Node":
|
|
242
|
+
root_node = Node.new()
|
|
243
|
+
"Node2D":
|
|
244
|
+
root_node = Node2D.new()
|
|
245
|
+
"Node3D", "Spatial":
|
|
246
|
+
root_node = Node3D.new()
|
|
247
|
+
"Control":
|
|
248
|
+
root_node = Control.new()
|
|
249
|
+
"CanvasLayer":
|
|
250
|
+
root_node = CanvasLayer.new()
|
|
251
|
+
"Panel":
|
|
252
|
+
root_node = Panel.new()
|
|
253
|
+
_:
|
|
254
|
+
# Attempt to create a custom class if built-in type not recognized
|
|
255
|
+
if ClassDB.class_exists(root_node_type):
|
|
256
|
+
root_node = ClassDB.instantiate(root_node_type)
|
|
257
|
+
else:
|
|
258
|
+
return _send_error(client_id, "Invalid root node type: %s" % root_node_type, command_id)
|
|
259
|
+
|
|
260
|
+
# Give the root node a name based on the file name
|
|
261
|
+
var file_name = path.get_file().get_basename()
|
|
262
|
+
root_node.name = file_name
|
|
263
|
+
|
|
264
|
+
# Create a packed scene
|
|
265
|
+
var packed_scene = PackedScene.new()
|
|
266
|
+
var result = packed_scene.pack(root_node)
|
|
267
|
+
if result != OK:
|
|
268
|
+
root_node.free()
|
|
269
|
+
return _send_error(client_id, "Failed to pack scene: %d" % result, command_id)
|
|
270
|
+
|
|
271
|
+
# Save the packed scene to disk
|
|
272
|
+
result = ResourceSaver.save(packed_scene, path)
|
|
273
|
+
if result != OK:
|
|
274
|
+
root_node.free()
|
|
275
|
+
return _send_error(client_id, "Failed to save scene: %d" % result, command_id)
|
|
276
|
+
|
|
277
|
+
# Clean up
|
|
278
|
+
root_node.free()
|
|
279
|
+
|
|
280
|
+
# Try to open the scene in the editor
|
|
281
|
+
var plugin = Engine.get_meta("GodotMCPPlugin") if Engine.has_meta("GodotMCPPlugin") else null
|
|
282
|
+
if plugin and plugin.has_method("get_editor_interface"):
|
|
283
|
+
var editor_interface = plugin.get_editor_interface()
|
|
284
|
+
editor_interface.open_scene_from_path(path)
|
|
285
|
+
|
|
286
|
+
_send_success(client_id, {
|
|
287
|
+
"scene_path": path,
|
|
288
|
+
"root_node_type": root_node_type
|
|
289
|
+
}, command_id)
|
|
290
|
+
|
|
291
|
+
func _delete_scene(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
292
|
+
var path = params.get("path", "")
|
|
293
|
+
|
|
294
|
+
# Validation
|
|
295
|
+
if path.is_empty():
|
|
296
|
+
return _send_error(client_id, "Scene path cannot be empty", command_id)
|
|
297
|
+
|
|
298
|
+
# Make sure we have an absolute path
|
|
299
|
+
if not path.begins_with("res://"):
|
|
300
|
+
path = "res://" + path
|
|
301
|
+
|
|
302
|
+
# Ensure path ends with .tscn
|
|
303
|
+
if not path.ends_with(".tscn"):
|
|
304
|
+
path += ".tscn"
|
|
305
|
+
|
|
306
|
+
# Check if file exists
|
|
307
|
+
if not FileAccess.file_exists(path):
|
|
308
|
+
return _send_error(client_id, "Scene file not found: %s" % path, command_id)
|
|
309
|
+
|
|
310
|
+
# Check if this is the currently open scene
|
|
311
|
+
var plugin = Engine.get_meta("GodotMCPPlugin") if Engine.has_meta("GodotMCPPlugin") else null
|
|
312
|
+
if plugin and plugin.has_method("get_editor_interface"):
|
|
313
|
+
var editor_interface = plugin.get_editor_interface()
|
|
314
|
+
var edited_scene_root = editor_interface.get_edited_scene_root()
|
|
315
|
+
|
|
316
|
+
if edited_scene_root and edited_scene_root.scene_file_path == path:
|
|
317
|
+
return _send_error(client_id, "Cannot delete currently open scene: %s. Close it first." % path, command_id)
|
|
318
|
+
|
|
319
|
+
# Delete the file
|
|
320
|
+
var dir = DirAccess.open("res://")
|
|
321
|
+
if not dir:
|
|
322
|
+
return _send_error(client_id, "Failed to access project directory", command_id)
|
|
323
|
+
|
|
324
|
+
var result = dir.remove(path)
|
|
325
|
+
if result != OK:
|
|
326
|
+
return _send_error(client_id, "Failed to delete scene file: %d" % result, command_id)
|
|
327
|
+
|
|
328
|
+
# Rescan the filesystem to update the editor
|
|
329
|
+
if plugin and plugin.has_method("get_editor_interface"):
|
|
330
|
+
var editor_interface = plugin.get_editor_interface()
|
|
331
|
+
var filesystem = editor_interface.get_resource_filesystem()
|
|
332
|
+
if filesystem:
|
|
333
|
+
filesystem.scan()
|
|
334
|
+
|
|
335
|
+
_send_success(client_id, {
|
|
336
|
+
"scene_path": path
|
|
337
|
+
}, command_id)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://n3ktd1vtbv1p
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
class_name MCPScriptCommands
|
|
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
|
+
"create_script":
|
|
8
|
+
_create_script(client_id, params, command_id)
|
|
9
|
+
return true
|
|
10
|
+
"edit_script":
|
|
11
|
+
_edit_script(client_id, params, command_id)
|
|
12
|
+
return true
|
|
13
|
+
"get_script":
|
|
14
|
+
_get_script(client_id, params, command_id)
|
|
15
|
+
return true
|
|
16
|
+
"get_script_metadata":
|
|
17
|
+
_get_script_metadata(client_id, params, command_id)
|
|
18
|
+
return true
|
|
19
|
+
"get_current_script":
|
|
20
|
+
_get_current_script(client_id, params, command_id)
|
|
21
|
+
return true
|
|
22
|
+
return false # Command not handled
|
|
23
|
+
|
|
24
|
+
# Add this function to help find script files
|
|
25
|
+
func _find_script_file(script_name: String) -> String:
|
|
26
|
+
# First, normalize the path
|
|
27
|
+
var script_path = script_name
|
|
28
|
+
|
|
29
|
+
# Add extension if missing
|
|
30
|
+
if not script_path.ends_with(".gd") and not script_path.ends_with(".cs"):
|
|
31
|
+
script_path += ".gd" # Default to GDScript
|
|
32
|
+
|
|
33
|
+
# If path already contains res://, use it directly
|
|
34
|
+
if script_path.begins_with("res://"):
|
|
35
|
+
if FileAccess.file_exists(script_path):
|
|
36
|
+
return script_path
|
|
37
|
+
else:
|
|
38
|
+
# Add res:// prefix if missing
|
|
39
|
+
script_path = "res://" + script_path
|
|
40
|
+
if FileAccess.file_exists(script_path):
|
|
41
|
+
return script_path
|
|
42
|
+
|
|
43
|
+
# If not found directly, try common script locations
|
|
44
|
+
var file_name = script_path.get_file()
|
|
45
|
+
var common_dirs = [
|
|
46
|
+
"res://scripts/",
|
|
47
|
+
"res://",
|
|
48
|
+
"res://scenes/",
|
|
49
|
+
"res://addons/"
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
for dir in common_dirs:
|
|
53
|
+
var test_path = dir + file_name
|
|
54
|
+
if FileAccess.file_exists(test_path):
|
|
55
|
+
return test_path
|
|
56
|
+
|
|
57
|
+
return "" # Not found
|
|
58
|
+
|
|
59
|
+
func _create_script(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
60
|
+
var script_path = params.get("script_path", "")
|
|
61
|
+
var content = params.get("content", "")
|
|
62
|
+
var node_path = params.get("node_path", "")
|
|
63
|
+
|
|
64
|
+
# Validation
|
|
65
|
+
if script_path.is_empty():
|
|
66
|
+
return _send_error(client_id, "Script path cannot be empty", command_id)
|
|
67
|
+
|
|
68
|
+
# Make sure we have an absolute path
|
|
69
|
+
if not script_path.begins_with("res://"):
|
|
70
|
+
script_path = "res://" + script_path
|
|
71
|
+
|
|
72
|
+
if not script_path.ends_with(".gd"):
|
|
73
|
+
script_path += ".gd"
|
|
74
|
+
|
|
75
|
+
# Get editor plugin and interfaces
|
|
76
|
+
var plugin = Engine.get_meta("GodotMCPPlugin")
|
|
77
|
+
if not plugin:
|
|
78
|
+
return _send_error(client_id, "GodotMCPPlugin not found in Engine metadata", command_id)
|
|
79
|
+
|
|
80
|
+
var editor_interface = plugin.get_editor_interface()
|
|
81
|
+
var script_editor = editor_interface.get_script_editor()
|
|
82
|
+
|
|
83
|
+
# Create the directory if it doesn't exist
|
|
84
|
+
var dir = script_path.get_base_dir()
|
|
85
|
+
if not DirAccess.dir_exists_absolute(dir):
|
|
86
|
+
var err = DirAccess.make_dir_recursive_absolute(dir)
|
|
87
|
+
if err != OK:
|
|
88
|
+
return _send_error(client_id, "Failed to create directory: %s (Error code: %d)" % [dir, err], command_id)
|
|
89
|
+
|
|
90
|
+
# Create the script file
|
|
91
|
+
var file = FileAccess.open(script_path, FileAccess.WRITE)
|
|
92
|
+
if file == null:
|
|
93
|
+
return _send_error(client_id, "Failed to create script file: %s" % script_path, command_id)
|
|
94
|
+
|
|
95
|
+
file.store_string(content)
|
|
96
|
+
file = null # Close the file
|
|
97
|
+
|
|
98
|
+
# Refresh the filesystem
|
|
99
|
+
editor_interface.get_resource_filesystem().scan()
|
|
100
|
+
|
|
101
|
+
# Attach the script to a node if specified
|
|
102
|
+
if not node_path.is_empty():
|
|
103
|
+
var node = _get_editor_node(node_path)
|
|
104
|
+
if not node:
|
|
105
|
+
# Try enhanced node resolution
|
|
106
|
+
node = _get_editor_node_enhanced(node_path)
|
|
107
|
+
if not node:
|
|
108
|
+
return _send_error(client_id, "Node not found: %s" % node_path, command_id)
|
|
109
|
+
|
|
110
|
+
# Wait for script to be recognized in the filesystem
|
|
111
|
+
await get_tree().create_timer(0.5).timeout
|
|
112
|
+
|
|
113
|
+
var script = load(script_path)
|
|
114
|
+
if not script:
|
|
115
|
+
return _send_error(client_id, "Failed to load script: %s" % script_path, command_id)
|
|
116
|
+
|
|
117
|
+
# Use undo/redo for script assignment
|
|
118
|
+
var undo_redo = _get_undo_redo()
|
|
119
|
+
if not undo_redo:
|
|
120
|
+
# Fallback method if we can't get undo/redo
|
|
121
|
+
node.set_script(script)
|
|
122
|
+
_mark_scene_modified()
|
|
123
|
+
else:
|
|
124
|
+
# Use undo/redo for proper editor integration
|
|
125
|
+
undo_redo.create_action("Assign Script")
|
|
126
|
+
undo_redo.add_do_method(node, "set_script", script)
|
|
127
|
+
undo_redo.add_undo_method(node, "set_script", node.get_script())
|
|
128
|
+
undo_redo.commit_action()
|
|
129
|
+
|
|
130
|
+
# Mark the scene as modified
|
|
131
|
+
_mark_scene_modified()
|
|
132
|
+
|
|
133
|
+
# Open the script in the editor
|
|
134
|
+
var script_resource = load(script_path)
|
|
135
|
+
if script_resource:
|
|
136
|
+
editor_interface.edit_script(script_resource)
|
|
137
|
+
|
|
138
|
+
_send_success(client_id, {
|
|
139
|
+
"script_path": script_path,
|
|
140
|
+
"node_path": node_path
|
|
141
|
+
}, command_id)
|
|
142
|
+
|
|
143
|
+
func _edit_script(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
144
|
+
var script_path = params.get("script_path", "")
|
|
145
|
+
var content = params.get("content", "")
|
|
146
|
+
|
|
147
|
+
# Validation
|
|
148
|
+
if script_path.is_empty():
|
|
149
|
+
return _send_error(client_id, "Script path cannot be empty", command_id)
|
|
150
|
+
|
|
151
|
+
if content.is_empty():
|
|
152
|
+
return _send_error(client_id, "Content cannot be empty", command_id)
|
|
153
|
+
|
|
154
|
+
# Make sure we have an absolute path
|
|
155
|
+
if not script_path.begins_with("res://"):
|
|
156
|
+
script_path = "res://" + script_path
|
|
157
|
+
|
|
158
|
+
# Try to find the script if not found directly
|
|
159
|
+
if not FileAccess.file_exists(script_path):
|
|
160
|
+
var found_path = _find_script_file(script_path)
|
|
161
|
+
if not found_path.is_empty():
|
|
162
|
+
script_path = found_path
|
|
163
|
+
else:
|
|
164
|
+
return _send_error(client_id, "Script file not found: %s" % script_path, command_id)
|
|
165
|
+
|
|
166
|
+
# Edit the script file
|
|
167
|
+
var file = FileAccess.open(script_path, FileAccess.WRITE)
|
|
168
|
+
if file == null:
|
|
169
|
+
return _send_error(client_id, "Failed to open script file: %s" % script_path, command_id)
|
|
170
|
+
|
|
171
|
+
file.store_string(content)
|
|
172
|
+
file = null # Close the file
|
|
173
|
+
|
|
174
|
+
_send_success(client_id, {
|
|
175
|
+
"script_path": script_path
|
|
176
|
+
}, command_id)
|
|
177
|
+
|
|
178
|
+
func _get_script(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
179
|
+
var script_path = params.get("script_path", "")
|
|
180
|
+
var node_path = params.get("node_path", "")
|
|
181
|
+
|
|
182
|
+
# Validation - either script_path or node_path must be provided
|
|
183
|
+
if script_path.is_empty() and node_path.is_empty():
|
|
184
|
+
return _send_error(client_id, "Either script_path or node_path must be provided", command_id)
|
|
185
|
+
|
|
186
|
+
# If node_path is provided, get the script from the node
|
|
187
|
+
if not node_path.is_empty():
|
|
188
|
+
var node = _get_editor_node(node_path)
|
|
189
|
+
if not node:
|
|
190
|
+
# Try enhanced node resolution
|
|
191
|
+
node = _get_editor_node_enhanced(node_path)
|
|
192
|
+
if not node:
|
|
193
|
+
return _send_error(client_id, "Node not found: %s" % node_path, command_id)
|
|
194
|
+
|
|
195
|
+
var script = node.get_script()
|
|
196
|
+
if not script:
|
|
197
|
+
return _send_error(client_id, "Node does not have a script: %s" % node_path, command_id)
|
|
198
|
+
|
|
199
|
+
# Handle various script types safely
|
|
200
|
+
if typeof(script) == TYPE_OBJECT and "resource_path" in script:
|
|
201
|
+
script_path = script.resource_path
|
|
202
|
+
elif typeof(script) == TYPE_STRING:
|
|
203
|
+
script_path = script
|
|
204
|
+
else:
|
|
205
|
+
# Try to handle other script types gracefully
|
|
206
|
+
print("Script type is not directly supported: ", typeof(script))
|
|
207
|
+
if script.has_method("get_path"):
|
|
208
|
+
script_path = script.get_path()
|
|
209
|
+
elif script.has_method("get_source_code"):
|
|
210
|
+
# Return the script content directly
|
|
211
|
+
_send_success(client_id, {
|
|
212
|
+
"script_path": node_path + " (embedded script)",
|
|
213
|
+
"content": script.get_source_code()
|
|
214
|
+
}, command_id)
|
|
215
|
+
return
|
|
216
|
+
else:
|
|
217
|
+
return _send_error(client_id, "Cannot extract script path from node: %s" % node_path, command_id)
|
|
218
|
+
|
|
219
|
+
# Try to find the script if it's not found directly
|
|
220
|
+
if not FileAccess.file_exists(script_path):
|
|
221
|
+
var found_path = _find_script_file(script_path)
|
|
222
|
+
if not found_path.is_empty():
|
|
223
|
+
script_path = found_path
|
|
224
|
+
else:
|
|
225
|
+
return _send_error(client_id, "Script file not found: %s" % script_path, command_id)
|
|
226
|
+
|
|
227
|
+
# Read the script file
|
|
228
|
+
var file = FileAccess.open(script_path, FileAccess.READ)
|
|
229
|
+
if file == null:
|
|
230
|
+
return _send_error(client_id, "Failed to open script file: %s" % script_path, command_id)
|
|
231
|
+
|
|
232
|
+
var content = file.get_as_text()
|
|
233
|
+
file = null # Close the file
|
|
234
|
+
|
|
235
|
+
_send_success(client_id, {
|
|
236
|
+
"script_path": script_path,
|
|
237
|
+
"content": content
|
|
238
|
+
}, command_id)
|
|
239
|
+
|
|
240
|
+
func _get_script_metadata(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
241
|
+
var path = params.get("path", "")
|
|
242
|
+
|
|
243
|
+
# Validation
|
|
244
|
+
if path.is_empty():
|
|
245
|
+
return _send_error(client_id, "Script path cannot be empty", command_id)
|
|
246
|
+
|
|
247
|
+
if not path.begins_with("res://"):
|
|
248
|
+
path = "res://" + path
|
|
249
|
+
|
|
250
|
+
# Try to find the script if it's not found directly
|
|
251
|
+
if not FileAccess.file_exists(path):
|
|
252
|
+
var found_path = _find_script_file(path)
|
|
253
|
+
if not found_path.is_empty():
|
|
254
|
+
path = found_path
|
|
255
|
+
else:
|
|
256
|
+
return _send_error(client_id, "Script file not found: " + path, command_id)
|
|
257
|
+
|
|
258
|
+
# Load the script
|
|
259
|
+
var script = load(path)
|
|
260
|
+
if not script:
|
|
261
|
+
return _send_error(client_id, "Failed to load script: " + path, command_id)
|
|
262
|
+
|
|
263
|
+
# Extract script metadata
|
|
264
|
+
var metadata = {
|
|
265
|
+
"path": path,
|
|
266
|
+
"language": "gdscript" if path.ends_with(".gd") else "csharp" if path.ends_with(".cs") else "unknown"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Attempt to get script class info
|
|
270
|
+
var class_name_str = ""
|
|
271
|
+
var extends_class = ""
|
|
272
|
+
|
|
273
|
+
# Read the file to extract class_name and extends info
|
|
274
|
+
var file = FileAccess.open(path, FileAccess.READ)
|
|
275
|
+
if file:
|
|
276
|
+
var content = file.get_as_text()
|
|
277
|
+
|
|
278
|
+
# Extract class_name
|
|
279
|
+
var class_regex = RegEx.new()
|
|
280
|
+
class_regex.compile("class_name\\s+([a-zA-Z_][a-zA-Z0-9_]*)")
|
|
281
|
+
var result = class_regex.search(content)
|
|
282
|
+
if result:
|
|
283
|
+
class_name_str = result.get_string(1)
|
|
284
|
+
|
|
285
|
+
# Extract extends
|
|
286
|
+
var extends_regex = RegEx.new()
|
|
287
|
+
extends_regex.compile("extends\\s+([a-zA-Z_][a-zA-Z0-9_]*)")
|
|
288
|
+
result = extends_regex.search(content)
|
|
289
|
+
if result:
|
|
290
|
+
extends_class = result.get_string(1)
|
|
291
|
+
|
|
292
|
+
# Add to metadata
|
|
293
|
+
metadata["class_name"] = class_name_str
|
|
294
|
+
metadata["extends"] = extends_class
|
|
295
|
+
|
|
296
|
+
# Try to extract methods and signals
|
|
297
|
+
var methods = []
|
|
298
|
+
var signals = []
|
|
299
|
+
|
|
300
|
+
var method_regex = RegEx.new()
|
|
301
|
+
method_regex.compile("func\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(")
|
|
302
|
+
var method_matches = method_regex.search_all(content)
|
|
303
|
+
|
|
304
|
+
for match_result in method_matches:
|
|
305
|
+
methods.append(match_result.get_string(1))
|
|
306
|
+
|
|
307
|
+
var signal_regex = RegEx.new()
|
|
308
|
+
signal_regex.compile("signal\\s+([a-zA-Z_][a-zA-Z0-9_]*)")
|
|
309
|
+
var signal_matches = signal_regex.search_all(content)
|
|
310
|
+
|
|
311
|
+
for match_result in signal_matches:
|
|
312
|
+
signals.append(match_result.get_string(1))
|
|
313
|
+
|
|
314
|
+
metadata["methods"] = methods
|
|
315
|
+
metadata["signals"] = signals
|
|
316
|
+
|
|
317
|
+
_send_success(client_id, metadata, command_id)
|
|
318
|
+
|
|
319
|
+
func _get_current_script(client_id: int, params: Dictionary, command_id: String) -> void:
|
|
320
|
+
# Get editor plugin and interfaces
|
|
321
|
+
var plugin = Engine.get_meta("GodotMCPPlugin")
|
|
322
|
+
if not plugin:
|
|
323
|
+
return _send_error(client_id, "GodotMCPPlugin not found in Engine metadata", command_id)
|
|
324
|
+
|
|
325
|
+
var editor_interface = plugin.get_editor_interface()
|
|
326
|
+
var script_editor = editor_interface.get_script_editor()
|
|
327
|
+
var current_script = script_editor.get_current_script()
|
|
328
|
+
|
|
329
|
+
if not current_script:
|
|
330
|
+
return _send_success(client_id, {
|
|
331
|
+
"script_found": false,
|
|
332
|
+
"message": "No script is currently being edited"
|
|
333
|
+
}, command_id)
|
|
334
|
+
|
|
335
|
+
var script_path = current_script.resource_path
|
|
336
|
+
|
|
337
|
+
# Read the script content
|
|
338
|
+
var file = FileAccess.open(script_path, FileAccess.READ)
|
|
339
|
+
if not file:
|
|
340
|
+
return _send_error(client_id, "Failed to open script file: %s" % script_path, command_id)
|
|
341
|
+
|
|
342
|
+
var content = file.get_as_text()
|
|
343
|
+
file = null # Close the file
|
|
344
|
+
|
|
345
|
+
_send_success(client_id, {
|
|
346
|
+
"script_found": true,
|
|
347
|
+
"script_path": script_path,
|
|
348
|
+
"content": content
|
|
349
|
+
}, command_id)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bbfou75clfpg5
|