@satelliteoflove/godot-mcp 2.2.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 (75) 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/editor.d.ts +20 -5
  64. package/dist/tools/editor.d.ts.map +1 -1
  65. package/dist/tools/editor.js +27 -4
  66. package/dist/tools/editor.js.map +1 -1
  67. package/dist/tools/project.d.ts +3 -3
  68. package/dist/tools/project.d.ts.map +1 -1
  69. package/dist/tools/project.js +28 -2
  70. package/dist/tools/project.js.map +1 -1
  71. package/dist/version.d.ts +2 -0
  72. package/dist/version.d.ts.map +1 -0
  73. package/dist/version.js +20 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +6 -4
@@ -0,0 +1,144 @@
1
+ @tool
2
+ extends EditorDebuggerPlugin
3
+ class_name MCPDebuggerPlugin
4
+
5
+ signal screenshot_received(success: bool, image_base64: String, width: int, height: int, error: String)
6
+ signal debug_output_received(output: PackedStringArray)
7
+ signal performance_metrics_received(metrics: Dictionary)
8
+ signal find_nodes_received(matches: Array, count: int, error: String)
9
+
10
+ var _active_session_id: int = -1
11
+ var _pending_screenshot: bool = false
12
+ var _pending_debug_output: bool = false
13
+ var _pending_performance_metrics: bool = false
14
+ var _pending_find_nodes: bool = false
15
+
16
+
17
+ func _has_capture(prefix: String) -> bool:
18
+ return prefix == "godot_mcp"
19
+
20
+
21
+ func _capture(message: String, data: Array, session_id: int) -> bool:
22
+ match message:
23
+ "godot_mcp:screenshot_result":
24
+ _handle_screenshot_result(data)
25
+ return true
26
+ "godot_mcp:debug_output_result":
27
+ _handle_debug_output_result(data)
28
+ return true
29
+ "godot_mcp:performance_metrics_result":
30
+ _handle_performance_metrics_result(data)
31
+ return true
32
+ "godot_mcp:find_nodes_result":
33
+ _handle_find_nodes_result(data)
34
+ return true
35
+ return false
36
+
37
+
38
+ func _setup_session(session_id: int) -> void:
39
+ _active_session_id = session_id
40
+
41
+
42
+ func _session_stopped() -> void:
43
+ _active_session_id = -1
44
+ if _pending_screenshot:
45
+ _pending_screenshot = false
46
+ screenshot_received.emit(false, "", 0, 0, "Game session ended")
47
+ if _pending_debug_output:
48
+ _pending_debug_output = false
49
+ debug_output_received.emit(PackedStringArray())
50
+ if _pending_performance_metrics:
51
+ _pending_performance_metrics = false
52
+ performance_metrics_received.emit({})
53
+ if _pending_find_nodes:
54
+ _pending_find_nodes = false
55
+ find_nodes_received.emit([], 0, "Game session ended")
56
+
57
+
58
+ func has_active_session() -> bool:
59
+ return _active_session_id >= 0
60
+
61
+
62
+ func request_screenshot(max_width: int = 1920) -> void:
63
+ if _active_session_id < 0:
64
+ screenshot_received.emit(false, "", 0, 0, "No active game session")
65
+ return
66
+ _pending_screenshot = true
67
+ var session := get_session(_active_session_id)
68
+ if session:
69
+ session.send_message("godot_mcp:take_screenshot", [max_width])
70
+ else:
71
+ _pending_screenshot = false
72
+ screenshot_received.emit(false, "", 0, 0, "Could not get debugger session")
73
+
74
+
75
+ func _handle_screenshot_result(data: Array) -> void:
76
+ _pending_screenshot = false
77
+ if data.size() < 5:
78
+ screenshot_received.emit(false, "", 0, 0, "Invalid response data")
79
+ return
80
+ var success: bool = data[0]
81
+ var image_base64: String = data[1]
82
+ var width: int = data[2]
83
+ var height: int = data[3]
84
+ var error: String = data[4]
85
+ screenshot_received.emit(success, image_base64, width, height, error)
86
+
87
+
88
+ func request_debug_output(clear: bool = false) -> void:
89
+ if _active_session_id < 0:
90
+ debug_output_received.emit(PackedStringArray())
91
+ return
92
+ _pending_debug_output = true
93
+ var session := get_session(_active_session_id)
94
+ if session:
95
+ session.send_message("godot_mcp:get_debug_output", [clear])
96
+ else:
97
+ _pending_debug_output = false
98
+ debug_output_received.emit(PackedStringArray())
99
+
100
+
101
+ func _handle_debug_output_result(data: Array) -> void:
102
+ _pending_debug_output = false
103
+ var output: PackedStringArray = data[0] if data.size() > 0 else PackedStringArray()
104
+ debug_output_received.emit(output)
105
+
106
+
107
+ func request_performance_metrics() -> void:
108
+ if _active_session_id < 0:
109
+ performance_metrics_received.emit({})
110
+ return
111
+ _pending_performance_metrics = true
112
+ var session := get_session(_active_session_id)
113
+ if session:
114
+ session.send_message("godot_mcp:get_performance_metrics", [])
115
+ else:
116
+ _pending_performance_metrics = false
117
+ performance_metrics_received.emit({})
118
+
119
+
120
+ func _handle_performance_metrics_result(data: Array) -> void:
121
+ _pending_performance_metrics = false
122
+ var metrics: Dictionary = data[0] if data.size() > 0 else {}
123
+ performance_metrics_received.emit(metrics)
124
+
125
+
126
+ func request_find_nodes(name_pattern: String, type_filter: String, root_path: String) -> void:
127
+ if _active_session_id < 0:
128
+ find_nodes_received.emit([], 0, "No active game session")
129
+ return
130
+ _pending_find_nodes = true
131
+ var session := get_session(_active_session_id)
132
+ if session:
133
+ session.send_message("godot_mcp:find_nodes", [name_pattern, type_filter, root_path])
134
+ else:
135
+ _pending_find_nodes = false
136
+ find_nodes_received.emit([], 0, "Could not get debugger session")
137
+
138
+
139
+ func _handle_find_nodes_result(data: Array) -> void:
140
+ _pending_find_nodes = false
141
+ var matches: Array = data[0] if data.size() > 0 else []
142
+ var count: int = data[1] if data.size() > 1 else 0
143
+ var error: String = data[2] if data.size() > 2 else ""
144
+ find_nodes_received.emit(matches, count, error)
@@ -0,0 +1 @@
1
+ uid://cc71xjkq3cjfs
@@ -0,0 +1,40 @@
1
+ @tool
2
+ class_name MCPLogger extends Logger
3
+
4
+ static var _output: PackedStringArray = []
5
+ static var _max_lines := 1000
6
+ static var _mutex := Mutex.new()
7
+
8
+
9
+ static func _static_init() -> void:
10
+ OS.add_logger(MCPLogger.new())
11
+
12
+
13
+ func _log_message(message: String, error: bool) -> void:
14
+ _mutex.lock()
15
+ var prefix := "[ERROR] " if error else ""
16
+ _output.append(prefix + message)
17
+ if _output.size() > _max_lines:
18
+ _output.remove_at(0)
19
+ _mutex.unlock()
20
+
21
+
22
+ func _log_error(function: String, file: String, line: int, code: String,
23
+ rationale: String, editor_notify: bool, error_type: int,
24
+ script_backtraces: Array[ScriptBacktrace]) -> void:
25
+ _mutex.lock()
26
+ var msg := "[%s:%d] %s: %s" % [file.get_file(), line, code, rationale]
27
+ _output.append("[ERROR] " + msg)
28
+ if _output.size() > _max_lines:
29
+ _output.remove_at(0)
30
+ _mutex.unlock()
31
+
32
+
33
+ static func get_output() -> PackedStringArray:
34
+ return _output
35
+
36
+
37
+ static func clear() -> void:
38
+ _mutex.lock()
39
+ _output.clear()
40
+ _mutex.unlock()
@@ -0,0 +1 @@
1
+ uid://cljx8hhn7rshd
@@ -0,0 +1,125 @@
1
+ @tool
2
+ class_name MCPUtils
3
+ extends RefCounted
4
+
5
+
6
+ static func success(result: Dictionary) -> Dictionary:
7
+ return {
8
+ "status": "success",
9
+ "result": result
10
+ }
11
+
12
+
13
+ static func error(code: String, message: String) -> Dictionary:
14
+ return {
15
+ "status": "error",
16
+ "error": {
17
+ "code": code,
18
+ "message": message
19
+ }
20
+ }
21
+
22
+
23
+ static func get_node_from_path(path: String) -> Node:
24
+ var root := EditorInterface.get_edited_scene_root()
25
+ if not root:
26
+ return null
27
+
28
+ if path == "/root" or path == "/" or path == str(root.get_path()):
29
+ return root
30
+
31
+ if path.begins_with("/root/"):
32
+ var parts := path.split("/")
33
+ if parts.size() >= 3:
34
+ if parts[2] == root.name:
35
+ var relative_path := "/".join(parts.slice(3))
36
+ if relative_path.is_empty():
37
+ return root
38
+ return root.get_node_or_null(relative_path)
39
+
40
+ if path.begins_with("/"):
41
+ path = path.substr(1)
42
+
43
+ return root.get_node_or_null(path)
44
+
45
+
46
+ static func serialize_value(value: Variant) -> Variant:
47
+ match typeof(value):
48
+ TYPE_VECTOR2:
49
+ return {"x": value.x, "y": value.y}
50
+ TYPE_VECTOR3:
51
+ return {"x": value.x, "y": value.y, "z": value.z}
52
+ TYPE_COLOR:
53
+ return {"r": value.r, "g": value.g, "b": value.b, "a": value.a}
54
+ TYPE_OBJECT:
55
+ if value == null:
56
+ return null
57
+ if value is Resource:
58
+ return value.resource_path if value.resource_path else str(value)
59
+ return str(value)
60
+ _:
61
+ return value
62
+
63
+
64
+ static func deserialize_value(value: Variant) -> Variant:
65
+ if value is String and value.begins_with("res://"):
66
+ var resource := load(value)
67
+ if resource:
68
+ return resource
69
+ if value is Dictionary:
70
+ if value.has("_resource"):
71
+ return _create_resource(value)
72
+ if value.has("x") and value.has("y"):
73
+ if value.has("z"):
74
+ return Vector3(value.x, value.y, value.z)
75
+ return Vector2(value.x, value.y)
76
+ if value.has("r") and value.has("g") and value.has("b"):
77
+ return Color(value.r, value.g, value.b, value.get("a", 1.0))
78
+ return value
79
+
80
+
81
+ static func _create_resource(spec: Dictionary) -> Resource:
82
+ var resource_type: String = spec.get("_resource", "")
83
+ if not ClassDB.class_exists(resource_type):
84
+ push_error("MCPUtils: Unknown resource type: %s" % resource_type)
85
+ return null
86
+ if not ClassDB.is_parent_class(resource_type, "Resource"):
87
+ push_error("MCPUtils: Type is not a Resource: %s" % resource_type)
88
+ return null
89
+
90
+ var resource: Resource = ClassDB.instantiate(resource_type)
91
+ if not resource:
92
+ push_error("MCPUtils: Failed to create resource: %s" % resource_type)
93
+ return null
94
+
95
+ for key in spec:
96
+ if key == "_resource":
97
+ continue
98
+ if key in resource:
99
+ resource.set(key, deserialize_value(spec[key]))
100
+
101
+ return resource
102
+
103
+
104
+ static func is_resource_path(path: String) -> bool:
105
+ return path.begins_with("res://")
106
+
107
+
108
+ static func dir_exists(path: String) -> bool:
109
+ if path.is_empty():
110
+ return false
111
+ if is_resource_path(path):
112
+ var dir := DirAccess.open("res://")
113
+ return dir != null and dir.dir_exists(path.trim_prefix("res://"))
114
+ return DirAccess.dir_exists_absolute(path)
115
+
116
+
117
+ static func ensure_dir_exists(path: String) -> Error:
118
+ if dir_exists(path):
119
+ return OK
120
+ if is_resource_path(path):
121
+ var dir := DirAccess.open("res://")
122
+ if not dir:
123
+ return ERR_CANT_OPEN
124
+ return dir.make_dir_recursive(path.trim_prefix("res://"))
125
+ return DirAccess.make_dir_recursive_absolute(path)
@@ -0,0 +1 @@
1
+ uid://dscyuw14ojs2v
@@ -0,0 +1,195 @@
1
+ extends Node
2
+ class_name MCPGameBridge
3
+
4
+ const DEFAULT_MAX_WIDTH := 1920
5
+
6
+ var _logger: _MCPGameLogger
7
+
8
+
9
+ func _ready() -> void:
10
+ if not EngineDebugger.is_active():
11
+ return
12
+ _logger = _MCPGameLogger.new()
13
+ OS.add_logger(_logger)
14
+ EngineDebugger.register_message_capture("godot_mcp", _on_debugger_message)
15
+ print("[MCP Game Bridge] Initialized")
16
+
17
+
18
+ func _exit_tree() -> void:
19
+ if EngineDebugger.is_active():
20
+ EngineDebugger.unregister_message_capture("godot_mcp")
21
+
22
+
23
+ func _on_debugger_message(message: String, data: Array) -> bool:
24
+ match message:
25
+ "take_screenshot":
26
+ _take_screenshot_deferred.call_deferred(data)
27
+ return true
28
+ "get_debug_output":
29
+ _handle_get_debug_output(data)
30
+ return true
31
+ "get_performance_metrics":
32
+ _handle_get_performance_metrics()
33
+ return true
34
+ "find_nodes":
35
+ _handle_find_nodes(data)
36
+ return true
37
+ return false
38
+
39
+
40
+ func _take_screenshot_deferred(data: Array) -> void:
41
+ var max_width: int = data[0] if data.size() > 0 else DEFAULT_MAX_WIDTH
42
+ await RenderingServer.frame_post_draw
43
+ _capture_and_send_screenshot(max_width)
44
+
45
+
46
+ func _capture_and_send_screenshot(max_width: int) -> void:
47
+ var viewport := get_viewport()
48
+ if viewport == null:
49
+ _send_screenshot_error("NO_VIEWPORT", "Could not get game viewport")
50
+ return
51
+ var image := viewport.get_texture().get_image()
52
+ if image == null:
53
+ _send_screenshot_error("CAPTURE_FAILED", "Failed to capture image from viewport")
54
+ return
55
+ if max_width > 0 and image.get_width() > max_width:
56
+ var scale_factor := float(max_width) / float(image.get_width())
57
+ var new_height := int(image.get_height() * scale_factor)
58
+ image.resize(max_width, new_height, Image.INTERPOLATE_LANCZOS)
59
+ var png_buffer := image.save_png_to_buffer()
60
+ var base64 := Marshalls.raw_to_base64(png_buffer)
61
+ EngineDebugger.send_message("godot_mcp:screenshot_result", [
62
+ true,
63
+ base64,
64
+ image.get_width(),
65
+ image.get_height(),
66
+ ""
67
+ ])
68
+
69
+
70
+ func _send_screenshot_error(code: String, message: String) -> void:
71
+ EngineDebugger.send_message("godot_mcp:screenshot_result", [
72
+ false,
73
+ "",
74
+ 0,
75
+ 0,
76
+ "%s: %s" % [code, message]
77
+ ])
78
+
79
+
80
+ func _handle_get_debug_output(data: Array) -> void:
81
+ var clear: bool = data[0] if data.size() > 0 else false
82
+ var output := _logger.get_output() if _logger else PackedStringArray()
83
+ if clear and _logger:
84
+ _logger.clear()
85
+ EngineDebugger.send_message("godot_mcp:debug_output_result", [output])
86
+
87
+
88
+ func _handle_find_nodes(data: Array) -> void:
89
+ var name_pattern: String = data[0] if data.size() > 0 else ""
90
+ var type_filter: String = data[1] if data.size() > 1 else ""
91
+ var root_path: String = data[2] if data.size() > 2 else ""
92
+
93
+ var tree := get_tree()
94
+ var scene_root := tree.current_scene if tree else null
95
+ if not scene_root:
96
+ EngineDebugger.send_message("godot_mcp:find_nodes_result", [[], 0, "No scene running"])
97
+ return
98
+
99
+ var search_root: Node = scene_root
100
+ if not root_path.is_empty():
101
+ search_root = _get_node_from_path(root_path, scene_root)
102
+ if not search_root:
103
+ EngineDebugger.send_message("godot_mcp:find_nodes_result", [[], 0, "Root not found: " + root_path])
104
+ return
105
+
106
+ var matches: Array = []
107
+ _find_recursive(search_root, scene_root, name_pattern, type_filter, matches)
108
+ EngineDebugger.send_message("godot_mcp:find_nodes_result", [matches, matches.size(), ""])
109
+
110
+
111
+ func _get_node_from_path(path: String, scene_root: Node) -> Node:
112
+ if path == "/" or path.is_empty():
113
+ return scene_root
114
+
115
+ if path.begins_with("/root/"):
116
+ var parts := path.split("/")
117
+ if parts.size() >= 3 and parts[2] == scene_root.name:
118
+ var relative := "/".join(parts.slice(3))
119
+ if relative.is_empty():
120
+ return scene_root
121
+ return scene_root.get_node_or_null(relative)
122
+
123
+ if path.begins_with("/"):
124
+ path = path.substr(1)
125
+
126
+ return scene_root.get_node_or_null(path)
127
+
128
+
129
+ func _find_recursive(node: Node, scene_root: Node, name_pattern: String, type_filter: String, results: Array) -> void:
130
+ var name_matches := name_pattern.is_empty() or node.name.matchn(name_pattern)
131
+ var type_matches := type_filter.is_empty() or node.is_class(type_filter)
132
+
133
+ if name_matches and type_matches:
134
+ var path := "/root/" + scene_root.name
135
+ var relative := scene_root.get_path_to(node)
136
+ if relative != NodePath("."):
137
+ path += "/" + str(relative)
138
+ results.append({"path": path, "type": node.get_class()})
139
+
140
+ for child in node.get_children():
141
+ _find_recursive(child, scene_root, name_pattern, type_filter, results)
142
+
143
+
144
+ func _handle_get_performance_metrics() -> void:
145
+ var metrics := {
146
+ "fps": Performance.get_monitor(Performance.TIME_FPS),
147
+ "frame_time_ms": Performance.get_monitor(Performance.TIME_PROCESS) * 1000.0,
148
+ "physics_time_ms": Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS) * 1000.0,
149
+ "navigation_time_ms": Performance.get_monitor(Performance.TIME_NAVIGATION_PROCESS) * 1000.0,
150
+ "render_objects": int(Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)),
151
+ "render_draw_calls": int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)),
152
+ "render_primitives": int(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME)),
153
+ "physics_2d_active_objects": int(Performance.get_monitor(Performance.PHYSICS_2D_ACTIVE_OBJECTS)),
154
+ "physics_2d_collision_pairs": int(Performance.get_monitor(Performance.PHYSICS_2D_COLLISION_PAIRS)),
155
+ "physics_2d_island_count": int(Performance.get_monitor(Performance.PHYSICS_2D_ISLAND_COUNT)),
156
+ "object_count": int(Performance.get_monitor(Performance.OBJECT_COUNT)),
157
+ "object_resource_count": int(Performance.get_monitor(Performance.OBJECT_RESOURCE_COUNT)),
158
+ "object_node_count": int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT)),
159
+ "object_orphan_node_count": int(Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)),
160
+ "memory_static": int(Performance.get_monitor(Performance.MEMORY_STATIC)),
161
+ "memory_static_max": int(Performance.get_monitor(Performance.MEMORY_STATIC_MAX)),
162
+ }
163
+ EngineDebugger.send_message("godot_mcp:performance_metrics_result", [metrics])
164
+
165
+
166
+ class _MCPGameLogger extends Logger:
167
+ var _output: PackedStringArray = []
168
+ var _max_lines := 1000
169
+ var _mutex := Mutex.new()
170
+
171
+ func _log_message(message: String, error: bool) -> void:
172
+ _mutex.lock()
173
+ var prefix := "[ERROR] " if error else ""
174
+ _output.append(prefix + message)
175
+ if _output.size() > _max_lines:
176
+ _output.remove_at(0)
177
+ _mutex.unlock()
178
+
179
+ func _log_error(function: String, file: String, line: int, code: String,
180
+ rationale: String, editor_notify: bool, error_type: int,
181
+ script_backtraces: Array[ScriptBacktrace]) -> void:
182
+ _mutex.lock()
183
+ var msg := "[%s:%d] %s: %s" % [file.get_file(), line, code, rationale]
184
+ _output.append("[ERROR] " + msg)
185
+ if _output.size() > _max_lines:
186
+ _output.remove_at(0)
187
+ _mutex.unlock()
188
+
189
+ func get_output() -> PackedStringArray:
190
+ return _output
191
+
192
+ func clear() -> void:
193
+ _mutex.lock()
194
+ _output.clear()
195
+ _mutex.unlock()
@@ -0,0 +1 @@
1
+ uid://hwfido041o7s
@@ -0,0 +1,8 @@
1
+ [plugin]
2
+
3
+ name="Godot MCP"
4
+ description="Model Context Protocol server for AI assistant integration"
5
+ author="godot-mcp"
6
+ version="2.4.0"
7
+ script="plugin.gd"
8
+ godot_version_min="4.5"
@@ -0,0 +1,89 @@
1
+ @tool
2
+ extends EditorPlugin
3
+
4
+ const WebSocketServer := preload("res://addons/godot_mcp/websocket_server.gd")
5
+ const CommandRouter := preload("res://addons/godot_mcp/command_router.gd")
6
+ const StatusPanel := preload("res://addons/godot_mcp/ui/status_panel.tscn")
7
+ const MCPDebuggerPlugin := preload("res://addons/godot_mcp/core/mcp_debugger_plugin.gd")
8
+
9
+ const GAME_BRIDGE_AUTOLOAD := "MCPGameBridge"
10
+ const GAME_BRIDGE_PATH := "res://addons/godot_mcp/game_bridge/mcp_game_bridge.gd"
11
+
12
+ var _websocket_server: WebSocketServer
13
+ var _command_router: CommandRouter
14
+ var _status_panel: Control
15
+ var _debugger_plugin: MCPDebuggerPlugin
16
+
17
+
18
+ func _enter_tree() -> void:
19
+ _command_router = CommandRouter.new()
20
+ _command_router.setup(self)
21
+
22
+ _websocket_server = WebSocketServer.new()
23
+ _websocket_server.command_received.connect(_on_command_received)
24
+ _websocket_server.client_connected.connect(_on_client_connected)
25
+ _websocket_server.client_disconnected.connect(_on_client_disconnected)
26
+ add_child(_websocket_server)
27
+
28
+ _status_panel = StatusPanel.instantiate()
29
+ add_control_to_bottom_panel(_status_panel, "MCP")
30
+ _update_status("Waiting for connection...")
31
+
32
+ _debugger_plugin = MCPDebuggerPlugin.new()
33
+ add_debugger_plugin(_debugger_plugin)
34
+
35
+ _ensure_game_bridge_autoload()
36
+
37
+ _websocket_server.start_server()
38
+ print("[godot-mcp] Plugin initialized")
39
+
40
+
41
+ func _exit_tree() -> void:
42
+ if _status_panel:
43
+ remove_control_from_bottom_panel(_status_panel)
44
+ _status_panel.queue_free()
45
+
46
+ if _websocket_server:
47
+ _websocket_server.stop_server()
48
+ _websocket_server.queue_free()
49
+
50
+ if _debugger_plugin:
51
+ remove_debugger_plugin(_debugger_plugin)
52
+ _debugger_plugin = null
53
+
54
+ if _command_router:
55
+ _command_router.free()
56
+
57
+ print("[godot-mcp] Plugin disabled")
58
+
59
+
60
+ func _ensure_game_bridge_autoload() -> void:
61
+ if not ProjectSettings.has_setting("autoload/" + GAME_BRIDGE_AUTOLOAD):
62
+ ProjectSettings.set_setting("autoload/" + GAME_BRIDGE_AUTOLOAD, GAME_BRIDGE_PATH)
63
+ ProjectSettings.save()
64
+ print("[godot-mcp] Added MCPGameBridge autoload")
65
+
66
+
67
+ func get_debugger_plugin() -> MCPDebuggerPlugin:
68
+ return _debugger_plugin
69
+
70
+
71
+ func _on_command_received(id: String, command: String, params: Dictionary) -> void:
72
+ var response = await _command_router.handle_command(command, params)
73
+ response["id"] = id
74
+ _websocket_server.send_response(response)
75
+
76
+
77
+ func _on_client_connected() -> void:
78
+ _update_status("Connected")
79
+ print("[godot-mcp] Client connected")
80
+
81
+
82
+ func _on_client_disconnected() -> void:
83
+ _update_status("Disconnected")
84
+ print("[godot-mcp] Client disconnected")
85
+
86
+
87
+ func _update_status(status: String) -> void:
88
+ if _status_panel and _status_panel.has_method("set_status"):
89
+ _status_panel.set_status(status)
@@ -0,0 +1 @@
1
+ uid://cyy2x6bpmk5gx
@@ -0,0 +1,23 @@
1
+ @tool
2
+ extends Control
3
+
4
+ @onready var status_label: Label = $MarginContainer/HBoxContainer/StatusLabel
5
+ @onready var status_icon: ColorRect = $MarginContainer/HBoxContainer/StatusIcon
6
+
7
+
8
+ func _ready() -> void:
9
+ set_status("Initializing...")
10
+
11
+
12
+ func set_status(status: String) -> void:
13
+ if status_label:
14
+ status_label.text = status
15
+
16
+ if status_icon:
17
+ match status:
18
+ "Connected":
19
+ status_icon.color = Color.GREEN
20
+ "Disconnected", "Waiting for connection...":
21
+ status_icon.color = Color.ORANGE
22
+ _:
23
+ status_icon.color = Color.GRAY
@@ -0,0 +1 @@
1
+ uid://28ial83uta0a
@@ -0,0 +1,41 @@
1
+ [gd_scene load_steps=2 format=3]
2
+
3
+ [ext_resource type="Script" path="res://addons/godot_mcp/ui/status_panel.gd" id="1"]
4
+
5
+ [node name="StatusPanel" type="Control"]
6
+ anchors_preset = 15
7
+ anchor_right = 1.0
8
+ anchor_bottom = 1.0
9
+ grow_horizontal = 2
10
+ grow_vertical = 2
11
+ script = ExtResource("1")
12
+
13
+ [node name="MarginContainer" type="MarginContainer" parent="."]
14
+ layout_mode = 1
15
+ anchors_preset = 15
16
+ anchor_right = 1.0
17
+ anchor_bottom = 1.0
18
+ grow_horizontal = 2
19
+ grow_vertical = 2
20
+ theme_override_constants/margin_left = 8
21
+ theme_override_constants/margin_top = 4
22
+ theme_override_constants/margin_right = 8
23
+ theme_override_constants/margin_bottom = 4
24
+
25
+ [node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
26
+ layout_mode = 2
27
+ theme_override_constants/separation = 8
28
+
29
+ [node name="Label" type="Label" parent="MarginContainer/HBoxContainer"]
30
+ layout_mode = 2
31
+ text = "MCP Server:"
32
+
33
+ [node name="StatusIcon" type="ColorRect" parent="MarginContainer/HBoxContainer"]
34
+ custom_minimum_size = Vector2(12, 12)
35
+ layout_mode = 2
36
+ size_flags_vertical = 4
37
+ color = Color(0.5, 0.5, 0.5, 1)
38
+
39
+ [node name="StatusLabel" type="Label" parent="MarginContainer/HBoxContainer"]
40
+ layout_mode = 2
41
+ text = "Initializing..."