@satelliteoflove/godot-mcp 2.3.0 → 2.4.1

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 (77) hide show
  1. package/README.md +58 -27
  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 +255 -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 +162 -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 +58 -0
  30. package/addon/core/base_command.gd.uid +1 -0
  31. package/addon/core/mcp_debugger_plugin.gd +149 -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 +129 -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/core/types.d.ts +5 -0
  56. package/dist/core/types.d.ts.map +1 -1
  57. package/dist/index.d.ts +1 -2
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +1 -6
  60. package/dist/index.js.map +1 -1
  61. package/dist/installer/install.d.ts +13 -0
  62. package/dist/installer/install.d.ts.map +1 -0
  63. package/dist/installer/install.js +77 -0
  64. package/dist/installer/install.js.map +1 -0
  65. package/dist/tools/editor.d.ts.map +1 -1
  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/tools/scene3d.d.ts.map +1 -1
  72. package/dist/tools/scene3d.js.map +1 -1
  73. package/dist/version.d.ts +2 -0
  74. package/dist/version.d.ts.map +1 -0
  75. package/dist/version.js +20 -0
  76. package/dist/version.js.map +1 -0
  77. package/package.json +6 -4
@@ -0,0 +1,58 @@
1
+ @tool
2
+ class_name MCPBaseCommand
3
+ extends RefCounted
4
+
5
+ var _plugin: EditorPlugin
6
+
7
+
8
+ func setup(plugin: EditorPlugin) -> void:
9
+ _plugin = plugin
10
+
11
+
12
+ func get_commands() -> Dictionary:
13
+ return {}
14
+
15
+
16
+ func _success(result: Dictionary) -> Dictionary:
17
+ return MCPUtils.success(result)
18
+
19
+
20
+ func _error(code: String, message: String) -> Dictionary:
21
+ return MCPUtils.error(code, message)
22
+
23
+
24
+ func _get_node(path: String) -> Node:
25
+ return MCPUtils.get_node_from_path(path)
26
+
27
+
28
+ func _serialize_value(value: Variant) -> Variant:
29
+ return MCPUtils.serialize_value(value)
30
+
31
+
32
+ func _require_scene_open() -> Dictionary:
33
+ var root := EditorInterface.get_edited_scene_root()
34
+ if not root:
35
+ return _error("NO_SCENE", "No scene is currently open")
36
+ return {}
37
+
38
+
39
+ func _require_typed_node(path: String, type: String, type_error_code: String = "WRONG_TYPE") -> Variant:
40
+ var node := _get_node(path)
41
+ if not node:
42
+ return _error("NODE_NOT_FOUND", "Node not found: %s" % path)
43
+ if not node.is_class(type):
44
+ return _error(type_error_code, "Expected %s, got %s" % [type, node.get_class()])
45
+ return node
46
+
47
+
48
+ func _find_nodes_of_type(root: Node, type: String) -> Array[Dictionary]:
49
+ var result: Array[Dictionary] = []
50
+ _find_nodes_recursive(root, type, result)
51
+ return result
52
+
53
+
54
+ func _find_nodes_recursive(node: Node, type: String, result: Array[Dictionary]) -> void:
55
+ if node.is_class(type):
56
+ result.append({"path": str(node.get_path()), "name": node.name})
57
+ for child in node.get_children():
58
+ _find_nodes_recursive(child, type, result)
@@ -0,0 +1 @@
1
+ uid://nr8o2h4rehka
@@ -0,0 +1,149 @@
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
+ if _active_session_id < 0:
60
+ return false
61
+ if not EditorInterface.is_playing_scene():
62
+ _active_session_id = -1
63
+ return false
64
+ return true
65
+
66
+
67
+ func request_screenshot(max_width: int = 1920) -> void:
68
+ if _active_session_id < 0:
69
+ screenshot_received.emit(false, "", 0, 0, "No active game session")
70
+ return
71
+ _pending_screenshot = true
72
+ var session := get_session(_active_session_id)
73
+ if session:
74
+ session.send_message("godot_mcp:take_screenshot", [max_width])
75
+ else:
76
+ _pending_screenshot = false
77
+ screenshot_received.emit(false, "", 0, 0, "Could not get debugger session")
78
+
79
+
80
+ func _handle_screenshot_result(data: Array) -> void:
81
+ _pending_screenshot = false
82
+ if data.size() < 5:
83
+ screenshot_received.emit(false, "", 0, 0, "Invalid response data")
84
+ return
85
+ var success: bool = data[0]
86
+ var image_base64: String = data[1]
87
+ var width: int = data[2]
88
+ var height: int = data[3]
89
+ var error: String = data[4]
90
+ screenshot_received.emit(success, image_base64, width, height, error)
91
+
92
+
93
+ func request_debug_output(clear: bool = false) -> void:
94
+ if _active_session_id < 0:
95
+ debug_output_received.emit(PackedStringArray())
96
+ return
97
+ _pending_debug_output = true
98
+ var session := get_session(_active_session_id)
99
+ if session:
100
+ session.send_message("godot_mcp:get_debug_output", [clear])
101
+ else:
102
+ _pending_debug_output = false
103
+ debug_output_received.emit(PackedStringArray())
104
+
105
+
106
+ func _handle_debug_output_result(data: Array) -> void:
107
+ _pending_debug_output = false
108
+ var output: PackedStringArray = data[0] if data.size() > 0 else PackedStringArray()
109
+ debug_output_received.emit(output)
110
+
111
+
112
+ func request_performance_metrics() -> void:
113
+ if _active_session_id < 0:
114
+ performance_metrics_received.emit({})
115
+ return
116
+ _pending_performance_metrics = true
117
+ var session := get_session(_active_session_id)
118
+ if session:
119
+ session.send_message("godot_mcp:get_performance_metrics", [])
120
+ else:
121
+ _pending_performance_metrics = false
122
+ performance_metrics_received.emit({})
123
+
124
+
125
+ func _handle_performance_metrics_result(data: Array) -> void:
126
+ _pending_performance_metrics = false
127
+ var metrics: Dictionary = data[0] if data.size() > 0 else {}
128
+ performance_metrics_received.emit(metrics)
129
+
130
+
131
+ func request_find_nodes(name_pattern: String, type_filter: String, root_path: String) -> void:
132
+ if _active_session_id < 0:
133
+ find_nodes_received.emit([], 0, "No active game session")
134
+ return
135
+ _pending_find_nodes = true
136
+ var session := get_session(_active_session_id)
137
+ if session:
138
+ session.send_message("godot_mcp:find_nodes", [name_pattern, type_filter, root_path])
139
+ else:
140
+ _pending_find_nodes = false
141
+ find_nodes_received.emit([], 0, "Could not get debugger session")
142
+
143
+
144
+ func _handle_find_nodes_result(data: Array) -> void:
145
+ _pending_find_nodes = false
146
+ var matches: Array = data[0] if data.size() > 0 else []
147
+ var count: int = data[1] if data.size() > 1 else 0
148
+ var error: String = data[2] if data.size() > 2 else ""
149
+ 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,129 @@
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_VECTOR2I:
51
+ return {"x": value.x, "y": value.y}
52
+ TYPE_VECTOR3:
53
+ return {"x": value.x, "y": value.y, "z": value.z}
54
+ TYPE_VECTOR3I:
55
+ return {"x": value.x, "y": value.y, "z": value.z}
56
+ TYPE_COLOR:
57
+ return {"r": value.r, "g": value.g, "b": value.b, "a": value.a}
58
+ TYPE_OBJECT:
59
+ if value == null:
60
+ return null
61
+ if value is Resource:
62
+ return value.resource_path if value.resource_path else str(value)
63
+ return str(value)
64
+ _:
65
+ return value
66
+
67
+
68
+ static func deserialize_value(value: Variant) -> Variant:
69
+ if value is String and value.begins_with("res://"):
70
+ var resource := load(value)
71
+ if resource:
72
+ return resource
73
+ if value is Dictionary:
74
+ if value.has("_resource"):
75
+ return _create_resource(value)
76
+ if value.has("x") and value.has("y"):
77
+ if value.has("z"):
78
+ return Vector3(value.x, value.y, value.z)
79
+ return Vector2(value.x, value.y)
80
+ if value.has("r") and value.has("g") and value.has("b"):
81
+ return Color(value.r, value.g, value.b, value.get("a", 1.0))
82
+ return value
83
+
84
+
85
+ static func _create_resource(spec: Dictionary) -> Resource:
86
+ var resource_type: String = spec.get("_resource", "")
87
+ if not ClassDB.class_exists(resource_type):
88
+ push_error("MCPUtils: Unknown resource type: %s" % resource_type)
89
+ return null
90
+ if not ClassDB.is_parent_class(resource_type, "Resource"):
91
+ push_error("MCPUtils: Type is not a Resource: %s" % resource_type)
92
+ return null
93
+
94
+ var resource: Resource = ClassDB.instantiate(resource_type)
95
+ if not resource:
96
+ push_error("MCPUtils: Failed to create resource: %s" % resource_type)
97
+ return null
98
+
99
+ for key in spec:
100
+ if key == "_resource":
101
+ continue
102
+ if key in resource:
103
+ resource.set(key, deserialize_value(spec[key]))
104
+
105
+ return resource
106
+
107
+
108
+ static func is_resource_path(path: String) -> bool:
109
+ return path.begins_with("res://")
110
+
111
+
112
+ static func dir_exists(path: String) -> bool:
113
+ if path.is_empty():
114
+ return false
115
+ if is_resource_path(path):
116
+ var dir := DirAccess.open("res://")
117
+ return dir != null and dir.dir_exists(path.trim_prefix("res://"))
118
+ return DirAccess.dir_exists_absolute(path)
119
+
120
+
121
+ static func ensure_dir_exists(path: String) -> Error:
122
+ if dir_exists(path):
123
+ return OK
124
+ if is_resource_path(path):
125
+ var dir := DirAccess.open("res://")
126
+ if not dir:
127
+ return ERR_CANT_OPEN
128
+ return dir.make_dir_recursive(path.trim_prefix("res://"))
129
+ 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.1"
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