@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.
- package/README.md +58 -27
- package/addon/command_router.gd +39 -0
- package/addon/command_router.gd.uid +1 -0
- package/addon/commands/animation_commands.gd +633 -0
- package/addon/commands/animation_commands.gd.uid +1 -0
- package/addon/commands/debug_commands.gd +109 -0
- package/addon/commands/debug_commands.gd.uid +1 -0
- package/addon/commands/file_commands.gd +95 -0
- package/addon/commands/file_commands.gd.uid +1 -0
- package/addon/commands/node_commands.gd +255 -0
- package/addon/commands/node_commands.gd.uid +1 -0
- package/addon/commands/project_commands.gd +114 -0
- package/addon/commands/project_commands.gd.uid +1 -0
- package/addon/commands/resource_commands.gd +293 -0
- package/addon/commands/resource_commands.gd.uid +1 -0
- package/addon/commands/scene3d_commands.gd +162 -0
- package/addon/commands/scene3d_commands.gd.uid +1 -0
- package/addon/commands/scene_commands.gd +131 -0
- package/addon/commands/scene_commands.gd.uid +1 -0
- package/addon/commands/screenshot_commands.gd +130 -0
- package/addon/commands/screenshot_commands.gd.uid +1 -0
- package/addon/commands/script_commands.gd +156 -0
- package/addon/commands/script_commands.gd.uid +1 -0
- package/addon/commands/selection_commands.gd +170 -0
- package/addon/commands/selection_commands.gd.uid +1 -0
- package/addon/commands/system_commands.gd +29 -0
- package/addon/commands/tilemap_commands.gd +657 -0
- package/addon/commands/tilemap_commands.gd.uid +1 -0
- package/addon/core/base_command.gd +58 -0
- package/addon/core/base_command.gd.uid +1 -0
- package/addon/core/mcp_debugger_plugin.gd +149 -0
- package/addon/core/mcp_debugger_plugin.gd.uid +1 -0
- package/addon/core/mcp_logger.gd +40 -0
- package/addon/core/mcp_logger.gd.uid +1 -0
- package/addon/core/mcp_utils.gd +129 -0
- package/addon/core/mcp_utils.gd.uid +1 -0
- package/addon/game_bridge/mcp_game_bridge.gd +195 -0
- package/addon/game_bridge/mcp_game_bridge.gd.uid +1 -0
- package/addon/plugin.cfg +8 -0
- package/addon/plugin.gd +89 -0
- package/addon/plugin.gd.uid +1 -0
- package/addon/ui/status_panel.gd +23 -0
- package/addon/ui/status_panel.gd.uid +1 -0
- package/addon/ui/status_panel.tscn +41 -0
- package/addon/websocket_server.gd +143 -0
- package/addon/websocket_server.gd.uid +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/connection/websocket.d.ts +14 -0
- package/dist/connection/websocket.d.ts.map +1 -1
- package/dist/connection/websocket.js +69 -1
- package/dist/connection/websocket.js.map +1 -1
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/installer/install.d.ts +13 -0
- package/dist/installer/install.d.ts.map +1 -0
- package/dist/installer/install.js +77 -0
- package/dist/installer/install.js.map +1 -0
- package/dist/tools/editor.d.ts.map +1 -1
- package/dist/tools/editor.js.map +1 -1
- package/dist/tools/project.d.ts +3 -3
- package/dist/tools/project.d.ts.map +1 -1
- package/dist/tools/project.js +28 -2
- package/dist/tools/project.js.map +1 -1
- package/dist/tools/scene3d.d.ts.map +1 -1
- package/dist/tools/scene3d.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +20 -0
- package/dist/version.js.map +1 -0
- 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
|
package/addon/plugin.cfg
ADDED
package/addon/plugin.gd
ADDED
|
@@ -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
|