@satelliteoflove/godot-mcp 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +3 -4
  2. package/addon/command_router.gd +39 -0
  3. package/addon/command_router.gd.uid +1 -0
  4. package/addon/commands/animation_commands.gd +633 -0
  5. package/addon/commands/animation_commands.gd.uid +1 -0
  6. package/addon/commands/debug_commands.gd +109 -0
  7. package/addon/commands/debug_commands.gd.uid +1 -0
  8. package/addon/commands/file_commands.gd +95 -0
  9. package/addon/commands/file_commands.gd.uid +1 -0
  10. package/addon/commands/node_commands.gd +252 -0
  11. package/addon/commands/node_commands.gd.uid +1 -0
  12. package/addon/commands/project_commands.gd +114 -0
  13. package/addon/commands/project_commands.gd.uid +1 -0
  14. package/addon/commands/resource_commands.gd +293 -0
  15. package/addon/commands/resource_commands.gd.uid +1 -0
  16. package/addon/commands/scene3d_commands.gd +169 -0
  17. package/addon/commands/scene3d_commands.gd.uid +1 -0
  18. package/addon/commands/scene_commands.gd +131 -0
  19. package/addon/commands/scene_commands.gd.uid +1 -0
  20. package/addon/commands/screenshot_commands.gd +130 -0
  21. package/addon/commands/screenshot_commands.gd.uid +1 -0
  22. package/addon/commands/script_commands.gd +156 -0
  23. package/addon/commands/script_commands.gd.uid +1 -0
  24. package/addon/commands/selection_commands.gd +170 -0
  25. package/addon/commands/selection_commands.gd.uid +1 -0
  26. package/addon/commands/system_commands.gd +29 -0
  27. package/addon/commands/tilemap_commands.gd +657 -0
  28. package/addon/commands/tilemap_commands.gd.uid +1 -0
  29. package/addon/core/base_command.gd +29 -0
  30. package/addon/core/base_command.gd.uid +1 -0
  31. package/addon/core/mcp_debugger_plugin.gd +144 -0
  32. package/addon/core/mcp_debugger_plugin.gd.uid +1 -0
  33. package/addon/core/mcp_logger.gd +40 -0
  34. package/addon/core/mcp_logger.gd.uid +1 -0
  35. package/addon/core/mcp_utils.gd +125 -0
  36. package/addon/core/mcp_utils.gd.uid +1 -0
  37. package/addon/game_bridge/mcp_game_bridge.gd +195 -0
  38. package/addon/game_bridge/mcp_game_bridge.gd.uid +1 -0
  39. package/addon/plugin.cfg +8 -0
  40. package/addon/plugin.gd +89 -0
  41. package/addon/plugin.gd.uid +1 -0
  42. package/addon/ui/status_panel.gd +23 -0
  43. package/addon/ui/status_panel.gd.uid +1 -0
  44. package/addon/ui/status_panel.tscn +41 -0
  45. package/addon/websocket_server.gd +143 -0
  46. package/addon/websocket_server.gd.uid +1 -0
  47. package/dist/cli.d.ts +3 -0
  48. package/dist/cli.d.ts.map +1 -0
  49. package/dist/cli.js +60 -0
  50. package/dist/cli.js.map +1 -0
  51. package/dist/connection/websocket.d.ts +14 -0
  52. package/dist/connection/websocket.d.ts.map +1 -1
  53. package/dist/connection/websocket.js +69 -1
  54. package/dist/connection/websocket.js.map +1 -1
  55. package/dist/index.d.ts +1 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1 -6
  58. package/dist/index.js.map +1 -1
  59. package/dist/installer/install.d.ts +13 -0
  60. package/dist/installer/install.d.ts.map +1 -0
  61. package/dist/installer/install.js +77 -0
  62. package/dist/installer/install.js.map +1 -0
  63. package/dist/tools/project.d.ts +3 -3
  64. package/dist/tools/project.d.ts.map +1 -1
  65. package/dist/tools/project.js +28 -2
  66. package/dist/tools/project.js.map +1 -1
  67. package/dist/version.d.ts +2 -0
  68. package/dist/version.d.ts.map +1 -0
  69. package/dist/version.js +20 -0
  70. package/dist/version.js.map +1 -0
  71. package/package.json +6 -4
@@ -0,0 +1,130 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPScreenshotCommands
4
+
5
+ const DEFAULT_MAX_WIDTH := 1920
6
+ const SCREENSHOT_TIMEOUT := 5.0
7
+
8
+ var _screenshot_result: Dictionary = {}
9
+ var _screenshot_pending: bool = false
10
+
11
+
12
+ func get_commands() -> Dictionary:
13
+ return {
14
+ "capture_game_screenshot": capture_game_screenshot,
15
+ "capture_editor_screenshot": capture_editor_screenshot
16
+ }
17
+
18
+
19
+ func capture_game_screenshot(params: Dictionary) -> Dictionary:
20
+ if not EditorInterface.is_playing_scene():
21
+ return _error("NOT_RUNNING", "No game is currently running. Use run_project first.")
22
+
23
+ var max_width: int = params.get("max_width", DEFAULT_MAX_WIDTH)
24
+
25
+ var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
26
+ if debugger_plugin == null:
27
+ return _error("NO_DEBUGGER", "Debugger plugin not available")
28
+
29
+ if not debugger_plugin.has_active_session():
30
+ return _error("NO_SESSION", "No active debug session. Game may not have MCPGameBridge autoload.")
31
+
32
+ _screenshot_pending = true
33
+ _screenshot_result = {}
34
+
35
+ debugger_plugin.screenshot_received.connect(_on_screenshot_received, CONNECT_ONE_SHOT)
36
+ debugger_plugin.request_screenshot(max_width)
37
+
38
+ var start_time := Time.get_ticks_msec()
39
+ while _screenshot_pending:
40
+ await Engine.get_main_loop().process_frame
41
+ if (Time.get_ticks_msec() - start_time) / 1000.0 > SCREENSHOT_TIMEOUT:
42
+ _screenshot_pending = false
43
+ if debugger_plugin.screenshot_received.is_connected(_on_screenshot_received):
44
+ debugger_plugin.screenshot_received.disconnect(_on_screenshot_received)
45
+ return _error("TIMEOUT", "Screenshot request timed out")
46
+
47
+ return _screenshot_result
48
+
49
+
50
+ func _on_screenshot_received(success: bool, image_base64: String, width: int, height: int, error: String) -> void:
51
+ _screenshot_pending = false
52
+ if success:
53
+ _screenshot_result = _success({
54
+ "image_base64": image_base64,
55
+ "width": width,
56
+ "height": height
57
+ })
58
+ else:
59
+ _screenshot_result = _error("CAPTURE_FAILED", error)
60
+
61
+
62
+ func capture_editor_screenshot(params: Dictionary) -> Dictionary:
63
+ var viewport_type: String = params.get("viewport", "")
64
+ var max_width: int = params.get("max_width", DEFAULT_MAX_WIDTH)
65
+
66
+ var viewport: SubViewport = null
67
+
68
+ if viewport_type == "2d":
69
+ viewport = _find_2d_viewport()
70
+ elif viewport_type == "3d":
71
+ viewport = _find_3d_viewport()
72
+ else:
73
+ viewport = _find_active_viewport()
74
+
75
+ if viewport == null:
76
+ return _error("NO_VIEWPORT", "Could not find editor viewport")
77
+
78
+ var image := viewport.get_texture().get_image()
79
+ return _process_and_encode_image(image, max_width)
80
+
81
+
82
+ func _process_and_encode_image(image: Image, max_width: int) -> Dictionary:
83
+ if image == null:
84
+ return _error("CAPTURE_FAILED", "Failed to capture image from viewport")
85
+
86
+ if max_width > 0 and image.get_width() > max_width:
87
+ var scale_factor := float(max_width) / float(image.get_width())
88
+ var new_height := int(image.get_height() * scale_factor)
89
+ image.resize(max_width, new_height, Image.INTERPOLATE_LANCZOS)
90
+
91
+ var png_buffer := image.save_png_to_buffer()
92
+ var base64 := Marshalls.raw_to_base64(png_buffer)
93
+
94
+ return _success({
95
+ "image_base64": base64,
96
+ "width": image.get_width(),
97
+ "height": image.get_height()
98
+ })
99
+
100
+
101
+ func _find_active_viewport() -> SubViewport:
102
+ var viewport := _find_3d_viewport()
103
+ if viewport:
104
+ return viewport
105
+ return _find_2d_viewport()
106
+
107
+
108
+ func _find_2d_viewport() -> SubViewport:
109
+ var editor_main := EditorInterface.get_editor_main_screen()
110
+ return _find_viewport_in_tree(editor_main, "2D")
111
+
112
+
113
+ func _find_3d_viewport() -> SubViewport:
114
+ var editor_main := EditorInterface.get_editor_main_screen()
115
+ return _find_viewport_in_tree(editor_main, "3D")
116
+
117
+
118
+ func _find_viewport_in_tree(node: Node, hint: String) -> SubViewport:
119
+ if node is SubViewportContainer:
120
+ var container := node as SubViewportContainer
121
+ for child in container.get_children():
122
+ if child is SubViewport:
123
+ return child as SubViewport
124
+
125
+ for child in node.get_children():
126
+ var result := _find_viewport_in_tree(child, hint)
127
+ if result:
128
+ return result
129
+
130
+ return null
@@ -0,0 +1 @@
1
+ uid://cv5f2i08buyb1
@@ -0,0 +1,156 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPScriptCommands
4
+
5
+
6
+ func get_commands() -> Dictionary:
7
+ return {
8
+ "read_script": read_script,
9
+ "get_current_script": get_current_script,
10
+ "create_script": create_script,
11
+ "edit_script": edit_script,
12
+ "attach_script": attach_script,
13
+ "detach_script": detach_script
14
+ }
15
+
16
+
17
+ func read_script(params: Dictionary) -> Dictionary:
18
+ var script_path: String = params.get("script_path", "")
19
+ if script_path.is_empty():
20
+ return _error("INVALID_PARAMS", "script_path is required")
21
+
22
+ if not FileAccess.file_exists(script_path):
23
+ return _error("FILE_NOT_FOUND", "Script file not found: %s" % script_path)
24
+
25
+ var file := FileAccess.open(script_path, FileAccess.READ)
26
+ if not file:
27
+ return _error("READ_FAILED", "Failed to read script: %s" % script_path)
28
+
29
+ var content := file.get_as_text()
30
+ file.close()
31
+
32
+ return _success({"content": content})
33
+
34
+
35
+ func get_current_script(_params: Dictionary) -> Dictionary:
36
+ var script_editor := EditorInterface.get_script_editor()
37
+ if not script_editor:
38
+ return _success({"path": null, "content": null})
39
+
40
+ var current_script := script_editor.get_current_script()
41
+ if not current_script:
42
+ return _success({"path": null, "content": null})
43
+
44
+ return _success({
45
+ "path": current_script.resource_path,
46
+ "content": current_script.source_code
47
+ })
48
+
49
+
50
+ func create_script(params: Dictionary) -> Dictionary:
51
+ var script_path: String = params.get("script_path", "")
52
+ var content: String = params.get("content", "")
53
+ var attach_to: String = params.get("attach_to", "")
54
+
55
+ if script_path.is_empty():
56
+ return _error("INVALID_PARAMS", "script_path is required")
57
+ if content.is_empty():
58
+ return _error("INVALID_PARAMS", "content is required")
59
+
60
+ var dir_path := script_path.get_base_dir()
61
+ if not MCPUtils.dir_exists(dir_path):
62
+ var err := MCPUtils.ensure_dir_exists(dir_path)
63
+ if err != OK:
64
+ return _error("DIR_CREATE_FAILED", "Failed to create directory: %s" % dir_path)
65
+
66
+ var file := FileAccess.open(script_path, FileAccess.WRITE)
67
+ if not file:
68
+ return _error("WRITE_FAILED", "Failed to create script: %s" % script_path)
69
+
70
+ file.store_string(content)
71
+ file.close()
72
+
73
+ EditorInterface.get_resource_filesystem().scan()
74
+
75
+ if not attach_to.is_empty():
76
+ var attach_result := attach_script({"node_path": attach_to, "script_path": script_path})
77
+ if attach_result["status"] == "error":
78
+ return attach_result
79
+
80
+ return _success({"path": script_path})
81
+
82
+
83
+ func edit_script(params: Dictionary) -> Dictionary:
84
+ var script_path: String = params.get("script_path", "")
85
+ var content: String = params.get("content", "")
86
+
87
+ if script_path.is_empty():
88
+ return _error("INVALID_PARAMS", "script_path is required")
89
+ if content.is_empty():
90
+ return _error("INVALID_PARAMS", "content is required")
91
+
92
+ if not FileAccess.file_exists(script_path):
93
+ return _error("FILE_NOT_FOUND", "Script file not found: %s" % script_path)
94
+
95
+ var file := FileAccess.open(script_path, FileAccess.WRITE)
96
+ if not file:
97
+ return _error("WRITE_FAILED", "Failed to write script: %s" % script_path)
98
+
99
+ file.store_string(content)
100
+ file.close()
101
+
102
+ EditorInterface.get_resource_filesystem().scan()
103
+
104
+ var script := load(script_path) as Script
105
+ if script:
106
+ script.reload()
107
+
108
+ return _success({"path": script_path})
109
+
110
+
111
+ func attach_script(params: Dictionary) -> Dictionary:
112
+ var node_path: String = params.get("node_path", "")
113
+ var script_path: String = params.get("script_path", "")
114
+
115
+ if node_path.is_empty():
116
+ return _error("INVALID_PARAMS", "node_path is required")
117
+ if script_path.is_empty():
118
+ return _error("INVALID_PARAMS", "script_path is required")
119
+
120
+ var node := _get_node(node_path)
121
+ if not node:
122
+ return _error("NODE_NOT_FOUND", "Node not found: %s" % node_path)
123
+
124
+ if not FileAccess.file_exists(script_path):
125
+ return _error("FILE_NOT_FOUND", "Script file not found: %s" % script_path)
126
+
127
+ var script := load(script_path) as Script
128
+ if not script:
129
+ return _error("LOAD_FAILED", "Failed to load script: %s" % script_path)
130
+
131
+ node.set_script(script)
132
+
133
+ EditorInterface.get_resource_filesystem().scan()
134
+ script.reload()
135
+
136
+ if node.get_script() != script:
137
+ return _error("ATTACH_FAILED", "Script attachment did not persist")
138
+
139
+ return _success({"node_path": str(node.get_path()), "script_path": script_path})
140
+
141
+
142
+ func detach_script(params: Dictionary) -> Dictionary:
143
+ var node_path: String = params.get("node_path", "")
144
+
145
+ if node_path.is_empty():
146
+ return _error("INVALID_PARAMS", "node_path is required")
147
+
148
+ var node := _get_node(node_path)
149
+ if not node:
150
+ return _error("NODE_NOT_FOUND", "Node not found: %s" % node_path)
151
+
152
+ node.set_script(null)
153
+
154
+ return _success({})
155
+
156
+
@@ -0,0 +1 @@
1
+ uid://6t4dbbx8421b
@@ -0,0 +1,170 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPSelectionCommands
4
+
5
+
6
+ func get_commands() -> Dictionary:
7
+ return {
8
+ "get_editor_state": get_editor_state,
9
+ "get_selected_nodes": get_selected_nodes,
10
+ "select_node": select_node,
11
+ "set_2d_viewport": set_2d_viewport
12
+ }
13
+
14
+
15
+ func get_editor_state(_params: Dictionary) -> Dictionary:
16
+ var root := EditorInterface.get_edited_scene_root()
17
+ var open_scenes := EditorInterface.get_open_scenes()
18
+
19
+ var main_screen := _get_current_main_screen()
20
+
21
+ var result := {
22
+ "current_scene": root.scene_file_path if root else null,
23
+ "is_playing": EditorInterface.is_playing_scene(),
24
+ "godot_version": Engine.get_version_info()["string"],
25
+ "open_scenes": Array(open_scenes),
26
+ "main_screen": main_screen
27
+ }
28
+
29
+ if main_screen == "3D":
30
+ var camera_info := _get_editor_camera_info()
31
+ if not camera_info.is_empty():
32
+ result["camera"] = camera_info
33
+ elif main_screen == "2D":
34
+ var viewport_2d_info := _get_editor_2d_viewport_info()
35
+ if not viewport_2d_info.is_empty():
36
+ result["viewport_2d"] = viewport_2d_info
37
+
38
+ return _success(result)
39
+
40
+
41
+ func _get_editor_camera_info() -> Dictionary:
42
+ var viewport := EditorInterface.get_editor_viewport_3d(0)
43
+ if not viewport:
44
+ return {}
45
+
46
+ var camera := viewport.get_camera_3d()
47
+ if not camera:
48
+ return {}
49
+
50
+ var pos: Vector3 = camera.global_position
51
+ var rot: Vector3 = camera.global_rotation
52
+ var forward: Vector3 = -camera.global_transform.basis.z
53
+
54
+ var info := {
55
+ "position": {"x": pos.x, "y": pos.y, "z": pos.z},
56
+ "rotation": {"x": rot.x, "y": rot.y, "z": rot.z},
57
+ "forward": {"x": forward.x, "y": forward.y, "z": forward.z},
58
+ "fov": camera.fov,
59
+ "near": camera.near,
60
+ "far": camera.far,
61
+ "projection": "orthogonal" if camera.projection == Camera3D.PROJECTION_ORTHOGONAL else "perspective",
62
+ }
63
+
64
+ if camera.projection == Camera3D.PROJECTION_ORTHOGONAL:
65
+ info["size"] = camera.size
66
+
67
+ return info
68
+
69
+
70
+ func _get_editor_2d_viewport_info() -> Dictionary:
71
+ var viewport := EditorInterface.get_editor_viewport_2d()
72
+ if not viewport:
73
+ return {}
74
+
75
+ var transform := viewport.global_canvas_transform
76
+ var zoom: float = transform.x.x
77
+ var offset: Vector2 = -transform.origin / zoom
78
+
79
+ var size := viewport.size
80
+
81
+ return {
82
+ "center": {"x": offset.x + size.x / zoom / 2, "y": offset.y + size.y / zoom / 2},
83
+ "zoom": zoom,
84
+ "size": {"width": int(size.x), "height": int(size.y)}
85
+ }
86
+
87
+
88
+ func set_2d_viewport(params: Dictionary) -> Dictionary:
89
+ var viewport := EditorInterface.get_editor_viewport_2d()
90
+ if not viewport:
91
+ return _error("NO_VIEWPORT", "Could not access 2D editor viewport")
92
+
93
+ var center_x: float = params.get("center_x", 0.0)
94
+ var center_y: float = params.get("center_y", 0.0)
95
+ var zoom: float = params.get("zoom", 1.0)
96
+
97
+ if zoom <= 0:
98
+ return _error("INVALID_PARAMS", "zoom must be positive")
99
+
100
+ var size := viewport.size
101
+ var offset := Vector2(center_x - size.x / zoom / 2, center_y - size.y / zoom / 2)
102
+ var origin := -offset * zoom
103
+
104
+ var transform := Transform2D(Vector2(zoom, 0), Vector2(0, zoom), origin)
105
+ viewport.global_canvas_transform = transform
106
+
107
+ return _success({
108
+ "center": {"x": center_x, "y": center_y},
109
+ "zoom": zoom
110
+ })
111
+
112
+
113
+ const MAIN_SCREEN_PATTERNS := {
114
+ "2D": ["CanvasItemEditor", "2D"],
115
+ "3D": ["Node3DEditor", "3D"],
116
+ "Script": ["ScriptEditor", "Script"],
117
+ "AssetLib": ["AssetLib", "Asset"],
118
+ }
119
+
120
+
121
+ func _get_current_main_screen() -> String:
122
+ var main_screen := EditorInterface.get_editor_main_screen()
123
+ if not main_screen:
124
+ return "unknown"
125
+
126
+ for child in main_screen.get_children():
127
+ if child.visible and child is Control:
128
+ var cls := child.get_class()
129
+ var node_name := child.name
130
+
131
+ for screen_name in MAIN_SCREEN_PATTERNS:
132
+ var patterns: Array = MAIN_SCREEN_PATTERNS[screen_name]
133
+ if patterns[0] in cls or patterns[1] in node_name:
134
+ return screen_name
135
+
136
+ return "unknown"
137
+
138
+
139
+ func get_selected_nodes(_params: Dictionary) -> Dictionary:
140
+ var selection := EditorInterface.get_selection()
141
+ var root := EditorInterface.get_edited_scene_root()
142
+ var selected: Array[String] = []
143
+
144
+ for node in selection.get_selected_nodes():
145
+ if root and root.is_ancestor_of(node):
146
+ # Build clean path relative to scene root
147
+ var relative_path := root.get_path_to(node)
148
+ var usable_path := "/root/" + root.name
149
+ if relative_path != NodePath("."):
150
+ usable_path += "/" + str(relative_path)
151
+ selected.append(usable_path)
152
+ elif node == root:
153
+ selected.append("/root/" + root.name)
154
+
155
+ return _success({"selected": selected})
156
+
157
+
158
+ func select_node(params: Dictionary) -> Dictionary:
159
+ var node_path: String = params.get("node_path", "")
160
+ if node_path.is_empty():
161
+ return _error("INVALID_PARAMS", "node_path is required")
162
+
163
+ var node := _get_node(node_path)
164
+ if not node:
165
+ return _error("NODE_NOT_FOUND", "Node not found: %s" % node_path)
166
+
167
+ var selection := EditorInterface.get_selection()
168
+ selection.clear()
169
+ selection.add_node(node)
170
+ return _success({})
@@ -0,0 +1 @@
1
+ uid://20vnciu1es3c
@@ -0,0 +1,29 @@
1
+ @tool
2
+ extends MCPBaseCommand
3
+ class_name MCPSystemCommands
4
+
5
+
6
+ func get_commands() -> Dictionary:
7
+ return {
8
+ "mcp_handshake": mcp_handshake,
9
+ }
10
+
11
+
12
+ func mcp_handshake(params: Dictionary) -> Dictionary:
13
+ var server_version: String = params.get("server_version", "unknown")
14
+
15
+ return _success({
16
+ "addon_version": _get_addon_version(),
17
+ "godot_version": Engine.get_version_info()["string"],
18
+ "project_path": ProjectSettings.globalize_path("res://"),
19
+ "project_name": ProjectSettings.get_setting("application/config/name", ""),
20
+ "server_version_received": server_version
21
+ })
22
+
23
+
24
+ func _get_addon_version() -> String:
25
+ var config := ConfigFile.new()
26
+ var err := config.load("res://addons/godot_mcp/plugin.cfg")
27
+ if err == OK:
28
+ return config.get_value("plugin", "version", "unknown")
29
+ return "unknown"