@satelliteoflove/godot-mcp 3.23.1 → 4.0.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 (177) hide show
  1. package/README.md +22 -14
  2. package/addon/command_router.gd +0 -1
  3. package/addon/commands/debug_commands.gd +0 -70
  4. package/addon/commands/exec_commands.gd +1 -1
  5. package/addon/commands/input_commands.gd +1 -1
  6. package/addon/commands/node_commands.gd +1 -142
  7. package/addon/commands/project_commands.gd +1 -1
  8. package/addon/commands/scene_commands.gd +29 -62
  9. package/addon/commands/selection_commands.gd +17 -4
  10. package/addon/core/base_command.gd +0 -24
  11. package/addon/core/mcp_debugger_plugin.gd +0 -29
  12. package/addon/core/mcp_logger.gd +0 -26
  13. package/addon/core/mcp_utils.gd +2 -26
  14. package/addon/game_bridge/mcp_game_bridge.gd +1 -18
  15. package/addon/plugin.cfg +1 -1
  16. package/dist/__tests__/connection/wsl-diagnostic.test.d.ts +2 -0
  17. package/dist/__tests__/connection/wsl-diagnostic.test.d.ts.map +1 -0
  18. package/dist/__tests__/connection/wsl-diagnostic.test.js +36 -0
  19. package/dist/__tests__/connection/wsl-diagnostic.test.js.map +1 -0
  20. package/dist/__tests__/core/annotations.test.js +45 -11
  21. package/dist/__tests__/core/annotations.test.js.map +1 -1
  22. package/dist/__tests__/core/discriminated-union-schema.test.js +19 -22
  23. package/dist/__tests__/core/discriminated-union-schema.test.js.map +1 -1
  24. package/dist/__tests__/core/doc-examples.test.js +2 -1
  25. package/dist/__tests__/core/doc-examples.test.js.map +1 -1
  26. package/dist/__tests__/core/read-only.test.d.ts +2 -0
  27. package/dist/__tests__/core/read-only.test.d.ts.map +1 -0
  28. package/dist/__tests__/core/read-only.test.js +53 -0
  29. package/dist/__tests__/core/read-only.test.js.map +1 -0
  30. package/dist/__tests__/core/schema-fidelity.test.d.ts +2 -0
  31. package/dist/__tests__/core/schema-fidelity.test.d.ts.map +1 -0
  32. package/dist/__tests__/core/schema-fidelity.test.js +95 -0
  33. package/dist/__tests__/core/schema-fidelity.test.js.map +1 -0
  34. package/dist/__tests__/core/startup.test.d.ts +2 -0
  35. package/dist/__tests__/core/startup.test.d.ts.map +1 -0
  36. package/dist/__tests__/core/startup.test.js +58 -0
  37. package/dist/__tests__/core/startup.test.js.map +1 -0
  38. package/dist/__tests__/core/structured-output.test.js +7 -8
  39. package/dist/__tests__/core/structured-output.test.js.map +1 -1
  40. package/dist/__tests__/core/toollist-snapshot.test.d.ts +2 -0
  41. package/dist/__tests__/core/toollist-snapshot.test.d.ts.map +1 -0
  42. package/dist/__tests__/core/toollist-snapshot.test.js +34 -0
  43. package/dist/__tests__/core/toollist-snapshot.test.js.map +1 -0
  44. package/dist/__tests__/evals/score.test.d.ts +2 -0
  45. package/dist/__tests__/evals/score.test.d.ts.map +1 -0
  46. package/dist/__tests__/evals/score.test.js +68 -0
  47. package/dist/__tests__/evals/score.test.js.map +1 -0
  48. package/dist/__tests__/tools/animation.test.js +51 -20
  49. package/dist/__tests__/tools/animation.test.js.map +1 -1
  50. package/dist/__tests__/tools/docs.test.js +171 -12
  51. package/dist/__tests__/tools/docs.test.js.map +1 -1
  52. package/dist/__tests__/tools/editor.test.js +124 -81
  53. package/dist/__tests__/tools/editor.test.js.map +1 -1
  54. package/dist/__tests__/tools/input.test.js +2 -2
  55. package/dist/__tests__/tools/input.test.js.map +1 -1
  56. package/dist/__tests__/tools/node.test.js +104 -119
  57. package/dist/__tests__/tools/node.test.js.map +1 -1
  58. package/dist/__tests__/tools/project.test.js +2 -2
  59. package/dist/__tests__/tools/project.test.js.map +1 -1
  60. package/dist/__tests__/tools/scene.test.js +3 -17
  61. package/dist/__tests__/tools/scene.test.js.map +1 -1
  62. package/dist/__tests__/tools/tilemap.test.js +200 -169
  63. package/dist/__tests__/tools/tilemap.test.js.map +1 -1
  64. package/dist/__tests__/utils/logger.test.js +36 -77
  65. package/dist/__tests__/utils/logger.test.js.map +1 -1
  66. package/dist/cli.js +28 -9
  67. package/dist/cli.js.map +1 -1
  68. package/dist/connection/websocket.d.ts.map +1 -1
  69. package/dist/connection/websocket.js +9 -1
  70. package/dist/connection/websocket.js.map +1 -1
  71. package/dist/core/define-tool.d.ts +0 -1
  72. package/dist/core/define-tool.d.ts.map +1 -1
  73. package/dist/core/define-tool.js.map +1 -1
  74. package/dist/core/doc-examples.d.ts.map +1 -1
  75. package/dist/core/doc-examples.js +6 -1
  76. package/dist/core/doc-examples.js.map +1 -1
  77. package/dist/core/registry.d.ts +1 -15
  78. package/dist/core/registry.d.ts.map +1 -1
  79. package/dist/core/registry.js +5 -36
  80. package/dist/core/registry.js.map +1 -1
  81. package/dist/core/schema.d.ts +3 -1
  82. package/dist/core/schema.d.ts.map +1 -1
  83. package/dist/core/schema.js +164 -25
  84. package/dist/core/schema.js.map +1 -1
  85. package/dist/core/types.d.ts +0 -9
  86. package/dist/core/types.d.ts.map +1 -1
  87. package/dist/evals/score.d.ts +24 -0
  88. package/dist/evals/score.d.ts.map +1 -0
  89. package/dist/evals/score.js +37 -0
  90. package/dist/evals/score.js.map +1 -0
  91. package/dist/index.d.ts +6 -1
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +70 -37
  94. package/dist/index.js.map +1 -1
  95. package/dist/installer/install.d.ts +0 -5
  96. package/dist/installer/install.d.ts.map +1 -1
  97. package/dist/installer/install.js +0 -13
  98. package/dist/installer/install.js.map +1 -1
  99. package/dist/tools/animation.d.ts +5 -4
  100. package/dist/tools/animation.d.ts.map +1 -1
  101. package/dist/tools/animation.js +29 -7
  102. package/dist/tools/animation.js.map +1 -1
  103. package/dist/tools/docs.d.ts +1 -8
  104. package/dist/tools/docs.d.ts.map +1 -1
  105. package/dist/tools/docs.js +6 -4
  106. package/dist/tools/docs.js.map +1 -1
  107. package/dist/tools/editor.d.ts +14 -13
  108. package/dist/tools/editor.d.ts.map +1 -1
  109. package/dist/tools/editor.js +109 -77
  110. package/dist/tools/editor.js.map +1 -1
  111. package/dist/tools/exec.js +5 -5
  112. package/dist/tools/exec.js.map +1 -1
  113. package/dist/tools/game-time.js +4 -4
  114. package/dist/tools/game-time.js.map +1 -1
  115. package/dist/tools/index.d.ts +4 -1
  116. package/dist/tools/index.d.ts.map +1 -1
  117. package/dist/tools/index.js +22 -16
  118. package/dist/tools/index.js.map +1 -1
  119. package/dist/tools/input.d.ts.map +1 -1
  120. package/dist/tools/input.js +10 -8
  121. package/dist/tools/input.js.map +1 -1
  122. package/dist/tools/node.d.ts +8 -26
  123. package/dist/tools/node.d.ts.map +1 -1
  124. package/dist/tools/node.js +64 -88
  125. package/dist/tools/node.js.map +1 -1
  126. package/dist/tools/profiler.js +2 -2
  127. package/dist/tools/profiler.js.map +1 -1
  128. package/dist/tools/project.d.ts.map +1 -1
  129. package/dist/tools/project.js +5 -5
  130. package/dist/tools/project.js.map +1 -1
  131. package/dist/tools/resource.d.ts.map +1 -1
  132. package/dist/tools/resource.js +2 -13
  133. package/dist/tools/resource.js.map +1 -1
  134. package/dist/tools/runtime-state.d.ts +1 -1
  135. package/dist/tools/runtime-state.js +1 -1
  136. package/dist/tools/runtime-state.js.map +1 -1
  137. package/dist/tools/scene.d.ts +0 -5
  138. package/dist/tools/scene.d.ts.map +1 -1
  139. package/dist/tools/scene.js +2 -16
  140. package/dist/tools/scene.js.map +1 -1
  141. package/dist/tools/scene3d.js +2 -2
  142. package/dist/tools/scene3d.js.map +1 -1
  143. package/dist/tools/tilemap.d.ts +6 -4
  144. package/dist/tools/tilemap.d.ts.map +1 -1
  145. package/dist/tools/tilemap.js +101 -65
  146. package/dist/tools/tilemap.js.map +1 -1
  147. package/dist/utils/errors.d.ts +0 -4
  148. package/dist/utils/errors.d.ts.map +1 -1
  149. package/dist/utils/errors.js.map +1 -1
  150. package/dist/utils/logger.d.ts +0 -10
  151. package/dist/utils/logger.d.ts.map +1 -1
  152. package/dist/utils/logger.js +12 -34
  153. package/dist/utils/logger.js.map +1 -1
  154. package/dist/{tools → utils}/project-staleness.d.ts.map +1 -1
  155. package/dist/{tools → utils}/project-staleness.js +1 -1
  156. package/dist/{tools → utils}/project-staleness.js.map +1 -1
  157. package/package.json +11 -5
  158. package/server.json +46 -0
  159. package/addon/commands/script_commands.gd +0 -73
  160. package/addon/commands/script_commands.gd.uid +0 -1
  161. package/dist/core/define-resource.d.ts +0 -9
  162. package/dist/core/define-resource.d.ts.map +0 -1
  163. package/dist/core/define-resource.js +0 -4
  164. package/dist/core/define-resource.js.map +0 -1
  165. package/dist/resources/index.d.ts +0 -4
  166. package/dist/resources/index.d.ts.map +0 -1
  167. package/dist/resources/index.js +0 -10
  168. package/dist/resources/index.js.map +0 -1
  169. package/dist/resources/scene.d.ts +0 -4
  170. package/dist/resources/scene.d.ts.map +0 -1
  171. package/dist/resources/scene.js +0 -32
  172. package/dist/resources/scene.js.map +0 -1
  173. package/dist/resources/script.d.ts +0 -3
  174. package/dist/resources/script.d.ts.map +0 -1
  175. package/dist/resources/script.js +0 -18
  176. package/dist/resources/script.js.map +0 -1
  177. /package/dist/{tools → utils}/project-staleness.d.ts +0 -0
package/README.md CHANGED
@@ -24,12 +24,12 @@ The pieces that make that possible, and that you won't find elsewhere:
24
24
  Here's what that looks like when an agent tests a boss fight:
25
25
 
26
26
  ```text
27
- godot_editor run frozen=true # boot with game time frozen at frame 0
27
+ godot_editor_edit run frozen=true # boot with game time frozen at frame 0
28
28
  godot_exec GameState.wave = 3 # set up the scenario worth testing
29
29
  godot_game_time step_until "tree.get_nodes_in_group('boss').size() >= 1"
30
30
  godot_runtime_state digest # exact positions and state — no pixels, no guessing
31
31
  godot_game_time step 500ms + dodge input # play the moment that matters
32
- godot_editor screenshot_game # and a screenshot when it's actually worth the tokens
32
+ godot_editor_read screenshot_game # and a screenshot when it's actually worth the tokens
33
33
  ```
34
34
 
35
35
  Less copy-paste, more creating.
@@ -65,26 +65,28 @@ Open your Godot project, restart your AI assistant, and start building. If anyth
65
65
 
66
66
  ## What's in the box
67
67
 
68
- 15 tools, ~90 actions, 3 MCP resources. Full API docs in the [Tools Reference](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/tools/README.md).
68
+ 21 tools, 86 actions. Full API docs in the [Tools Reference](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/tools/README.md).
69
69
 
70
70
  | Tool | What it does |
71
71
  |------|--------------|
72
- | `godot_scene` | Open, save, and create scenes |
73
- | `godot_node` | Create, update, delete, and reparent nodes; attach scripts; connect signals |
74
- | `godot_editor` | Editor state, selection, run/stop/restart, screenshots, editor error log, 2D viewport control |
75
- | `godot_project` | Project info and settings |
76
- | `godot_animation` | Query, play, and edit animations down to individual tracks and keyframes |
77
- | `godot_tilemap` / `godot_gridmap` | Read and edit TileMapLayer and GridMap cells |
72
+ | `godot_scene` | Open and save scenes (create new scenes by writing the `.tscn` directly, then open) |
73
+ | `godot_node_read` / `godot_node_edit` | Inspect effective properties, the full scene tree (including instanced sub-scenes), and find nodes; update properties and reparent |
74
+ | `godot_editor_read` / `godot_editor_edit` | Editor state, selection, screenshots, editor error log; run/stop/restart and 2D viewport control |
75
+ | `godot_project` | Project info, settings, addon version skew, and stale-settings detection |
76
+ | `godot_animation_read` / `godot_animation_edit` | Query animations down to keyframes; author tracks and keyframes with instant editor preview |
77
+ | `godot_tilemap_read` / `godot_tilemap_edit` | Read and edit TileMapLayer cells (base64-encoded in `.tscn` — the bridge is the only way) |
78
+ | `godot_gridmap_read` / `godot_gridmap_edit` | Read and edit GridMap cells, same story |
78
79
  | `godot_resource` | Inspect resources with type-aware output: SpriteFrames, TileSet, Materials, Textures |
79
- | `godot_scene3d` | 3D transforms, bounding boxes, and visibility for spatial reasoning |
80
+ | `godot_scene3d` | Engine-computed 3D transforms, bounding boxes, and visibility for spatial reasoning |
80
81
  | `godot_docs` | Fetch Godot documentation as clean markdown, version-matched to your editor |
81
82
  | `godot_input` | Inject input into the running game: actions, joypad, raw keys, mouse-look, text |
82
83
  | `godot_profiler` | Metric snapshots and per-frame time series with spike detection |
83
84
  | `godot_runtime_state` | Live game state as JSON: one-shot digests, watch windows, signal timelines |
84
85
  | `godot_game_time` | Freeze, step, and step-until on the game clock — deterministic observation |
85
86
  | `godot_exec` | Run GDScript inside the running game for test scenario setup |
87
+ | `godot_validate_meshes` | Detect silently corrupt procedural mesh data that masquerades as lighting bugs |
86
88
 
87
- A note on shape: this kit deliberately stays at 15 tools rather than 90. Related operations live as actions inside one tool, so your agent's context isn't flooded with tool definitions it won't use.
89
+ A note on shape: tools split along the read/write boundary, so every `godot_*_read` tool (and the other read-only tools) can be safely auto-allowed in your client's permission settings while writes stay gated. Related operations still live as actions inside one tool, so your agent's context isn't flooded with definitions it won't use. Anything an agent can do by editing project files directly — creating scenes and nodes, attaching scripts, connecting signals — is deliberately *not* duplicated as a tool; the bridge covers what files can't: editor state, verification, binary-encoded cell data, and the running game. A `--read-only` flag serves the look-but-don't-touch use case.
88
90
 
89
91
  ## Things to ask for
90
92
 
@@ -121,11 +123,11 @@ One caveat: a Godot editor serves a single godot-mcp client at a time. Extra cli
121
123
  ## Documentation
122
124
 
123
125
  - [Installation Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/INSTALL.md) — MCP client configs for Claude Desktop, Claude Code, VSCode/Copilot, and more
126
+ - [Migrating to v4](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/migrating-to-v4.md) — renamed tools, removed actions, allowlist updates
124
127
  - [Troubleshooting](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/troubleshooting.md) — connection checklist, CLI smoke test, common fixes
125
128
  - [Claude Code Setup Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/claude-code-setup.md) — CLAUDE.md template for Godot projects
126
129
  - [Runtime State Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/runtime-state-guide.md) — expose game state to agents via `mcp_watch` and `_mcp_state()`
127
- - [Tools Reference](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/tools/README.md) — all 15 tools with full API docs
128
- - [Resources Reference](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/resources.md) — MCP resources for reading project data
130
+ - [Tools Reference](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/tools/README.md) — all 21 tools with full API docs
129
131
  - [Architecture Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/architecture.md) — how the server, addon, and game bridge fit together
130
132
  - [Contributing](https://github.com/satelliteoflove/godot-mcp/blob/main/CONTRIBUTING.md) — dev setup, adding tools, release process
131
133
  - [Changelog](https://github.com/satelliteoflove/godot-mcp/blob/main/server/CHANGELOG.md) — release history
@@ -141,10 +143,16 @@ One caveat: a Godot editor serves a single godot-mcp client at a time. Extra cli
141
143
  ```bash
142
144
  cd server
143
145
  npm install && npm run build
144
- npm test
146
+ npm test # unit + schema-snapshot tests
147
+ npm run test:protocol # wire-level smoke of the built server
145
148
  npm run generate-docs
146
149
  ```
147
150
 
151
+ There is also an agentic eval harness (`npm run eval`) that runs realistic
152
+ tasks through headless Claude Code against a real project — see
153
+ [server/evals/README.md](https://github.com/satelliteoflove/godot-mcp/blob/main/server/evals/README.md). It never starts Godot; you
154
+ bring the open editor.
155
+
148
156
  Contributions welcome — this project favors tools that solve real, time-wasting problems. Read [CONTRIBUTING.md](https://github.com/satelliteoflove/godot-mcp/blob/main/CONTRIBUTING.md) before building something big, or open an issue and we'll figure out the right shape together.
149
157
 
150
158
  ## License
@@ -10,7 +10,6 @@ func setup(plugin: EditorPlugin) -> void:
10
10
  _register_handler(MCPSystemCommands.new(), plugin)
11
11
  _register_handler(MCPSceneCommands.new(), plugin)
12
12
  _register_handler(MCPNodeCommands.new(), plugin)
13
- _register_handler(MCPScriptCommands.new(), plugin)
14
13
  _register_handler(MCPSelectionCommands.new(), plugin)
15
14
  _register_handler(MCPProjectCommands.new(), plugin)
16
15
  _register_handler(MCPDebugCommands.new(), plugin)
@@ -2,21 +2,15 @@
2
2
  extends MCPBaseCommand
3
3
  class_name MCPDebugCommands
4
4
 
5
- const DEBUG_OUTPUT_TIMEOUT := 5.0
6
5
  # Keep in sync with LAUNCH_FROZEN_ENV in mcp_game_bridge.gd.
7
6
  const LAUNCH_FROZEN_ENV := "GODOT_MCP_LAUNCH_FROZEN"
8
7
 
9
- var _debug_output_result: PackedStringArray = []
10
- var _debug_output_pending: bool = false
11
-
12
8
 
13
9
  func get_commands() -> Dictionary:
14
10
  return {
15
11
  "run_project": run_project,
16
12
  "stop_project": stop_project,
17
- "get_debug_output": get_debug_output,
18
13
  "get_log_messages": get_log_messages,
19
- "get_errors": get_errors,
20
14
  "get_stack_trace": get_stack_trace,
21
15
  }
22
16
 
@@ -25,8 +19,6 @@ func run_project(params: Dictionary) -> Dictionary:
25
19
  var scene_path: String = params.get("scene_path", "")
26
20
  var frozen: bool = params.get("frozen", false)
27
21
 
28
- MCPLogger.clear()
29
-
30
22
  # Launch-frozen: the spawned game inherits the editor's environment, so
31
23
  # setting this before play makes the bridge freeze the tree in _ready —
32
24
  # before the first process frame. Deterministic, unlike sending a freeze
@@ -56,64 +48,6 @@ func stop_project(_params: Dictionary) -> Dictionary:
56
48
  return _success({})
57
49
 
58
50
 
59
- func get_debug_output(params: Dictionary) -> Dictionary:
60
- var clear: bool = params.get("clear", false)
61
- var source: String = params.get("source", "")
62
-
63
- if source == "editor":
64
- var output := "\n".join(MCPLogger.get_output())
65
- if clear:
66
- MCPLogger.clear()
67
- return _success({"output": output, "source": "editor"})
68
-
69
- if source == "game":
70
- if not EditorInterface.is_playing_scene():
71
- return _error("NOT_RUNNING", "No game is currently running. Use source: 'editor' for editor output.")
72
- var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
73
- if debugger_plugin == null or not debugger_plugin.has_active_session():
74
- return _error("NO_SESSION", "No active debug session. Use source: 'editor' for editor output.")
75
- return await _fetch_game_debug_output(debugger_plugin, clear)
76
-
77
- if not EditorInterface.is_playing_scene():
78
- var output := "\n".join(MCPLogger.get_output())
79
- if clear:
80
- MCPLogger.clear()
81
- return _success({"output": output, "source": "editor"})
82
-
83
- var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
84
- if debugger_plugin == null or not debugger_plugin.has_active_session():
85
- var output := "\n".join(MCPLogger.get_output())
86
- if clear:
87
- MCPLogger.clear()
88
- return _success({"output": output, "source": "editor"})
89
-
90
- return await _fetch_game_debug_output(debugger_plugin, clear)
91
-
92
-
93
- func _fetch_game_debug_output(debugger_plugin: MCPDebuggerPlugin, clear: bool) -> Dictionary:
94
- _debug_output_pending = true
95
- _debug_output_result = PackedStringArray()
96
-
97
- debugger_plugin.debug_output_received.connect(_on_debug_output_received, CONNECT_ONE_SHOT)
98
- debugger_plugin.request_debug_output(clear)
99
-
100
- var start_time := Time.get_ticks_msec()
101
- while _debug_output_pending:
102
- await Engine.get_main_loop().process_frame
103
- if (Time.get_ticks_msec() - start_time) / 1000.0 > DEBUG_OUTPUT_TIMEOUT:
104
- _debug_output_pending = false
105
- if debugger_plugin.debug_output_received.is_connected(_on_debug_output_received):
106
- debugger_plugin.debug_output_received.disconnect(_on_debug_output_received)
107
- return _success({"output": "\n".join(MCPLogger.get_output()), "source": "editor"})
108
-
109
- return _success({"output": "\n".join(_debug_output_result), "source": "game"})
110
-
111
-
112
- func _on_debug_output_received(output: PackedStringArray) -> void:
113
- _debug_output_pending = false
114
- _debug_output_result = output
115
-
116
-
117
51
  func get_log_messages(params: Dictionary) -> Dictionary:
118
52
  var clear: bool = params.get("clear", false)
119
53
  var limit: int = int(params.get("limit", 50))
@@ -137,10 +71,6 @@ func get_log_messages(params: Dictionary) -> Dictionary:
137
71
  return _success(result)
138
72
 
139
73
 
140
- func get_errors(params: Dictionary) -> Dictionary:
141
- return get_log_messages(params)
142
-
143
-
144
74
  func get_stack_trace(_params: Dictionary) -> Dictionary:
145
75
  var frames := MCPLogger.get_last_stack_trace()
146
76
  var errors := MCPLogger.get_errors()
@@ -111,7 +111,7 @@ func _send_and_wait(msg_type: String, args: Array, timeout: float, call_id: int)
111
111
  debugger_plugin.clear_response(msg_type)
112
112
  var hint := ""
113
113
  if debugger_plugin.is_session_breaked():
114
- hint = " (the game is paused in the editor debugger and did not resume; press Continue in the editor or run godot_editor stop)"
114
+ hint = " (the game is paused in the editor debugger and did not resume; press Continue in the editor or run godot_editor_edit stop)"
115
115
  _last_error = _error("TIMEOUT", "Timed out waiting for %s response%s" % [msg_type, hint])
116
116
  return null
117
117
  return null # unreachable; satisfies the parser
@@ -104,7 +104,7 @@ func _get_editor_input_map() -> Dictionary:
104
104
  # This map is read from the editor's in-memory InputMap, which is loaded at
105
105
  # startup and goes stale if project.godot's [input] section is edited on disk
106
106
  # (#245). Flag that so the caller knows the map may be incomplete and can
107
- # recover with `godot_editor restart`. The game-running path above reads fresh
107
+ # recover with `godot_editor_edit restart`. The game-running path above reads fresh
108
108
  # from the bridge, so it never carries this.
109
109
  var result := {"actions": actions, "source": "editor"}
110
110
  var staleness := MCPUtils.detect_project_staleness()
@@ -12,11 +12,8 @@ func get_commands() -> Dictionary:
12
12
  return {
13
13
  "get_node_properties": get_node_properties,
14
14
  "find_nodes": find_nodes,
15
- "create_node": create_node,
16
15
  "update_node": update_node,
17
- "delete_node": delete_node,
18
- "reparent_node": reparent_node,
19
- "connect_signal": connect_signal
16
+ "reparent_node": reparent_node
20
17
  }
21
18
 
22
19
 
@@ -120,79 +117,6 @@ func _find_recursive(node: Node, scene_root: Node, name_pattern: String, type_fi
120
117
  _find_recursive(child, scene_root, name_pattern, type_filter, results)
121
118
 
122
119
 
123
- func create_node(params: Dictionary) -> Dictionary:
124
- var scene_check := _require_scene_open()
125
- if not scene_check.is_empty():
126
- return scene_check
127
-
128
- var parent_path: String = params.get("parent_path", "")
129
- var node_type: String = params.get("node_type", "")
130
- var scene_path: String = params.get("scene_path", "")
131
- var node_name: String = params.get("node_name", "")
132
- var properties: Dictionary = params.get("properties", {})
133
-
134
- if parent_path.is_empty():
135
- return _error("INVALID_PARAMS", "parent_path is required")
136
- if node_name.is_empty():
137
- return _error("INVALID_PARAMS", "node_name is required")
138
- if node_type.is_empty() and scene_path.is_empty():
139
- return _error("INVALID_PARAMS", "Either node_type or scene_path is required")
140
- if not node_type.is_empty() and not scene_path.is_empty():
141
- return _error("INVALID_PARAMS", "Provide node_type OR scene_path, not both")
142
-
143
- var parent := _get_node(parent_path)
144
- if not parent:
145
- return _error("NODE_NOT_FOUND", "Parent node not found: %s" % parent_path)
146
-
147
- var node: Node
148
- if not scene_path.is_empty():
149
- if not ResourceLoader.exists(scene_path):
150
- return _error("SCENE_NOT_FOUND", "Scene not found: %s" % scene_path)
151
- var packed_scene: PackedScene = load(scene_path)
152
- if not packed_scene:
153
- return _error("LOAD_FAILED", "Failed to load scene: %s" % scene_path)
154
- node = packed_scene.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
155
- if not node:
156
- return _error("INSTANTIATE_FAILED", "Failed to instantiate: %s" % scene_path)
157
- else:
158
- if not ClassDB.class_exists(node_type):
159
- return _error("INVALID_TYPE", "Unknown node type: %s" % node_type)
160
- node = ClassDB.instantiate(node_type)
161
- if not node:
162
- return _error("CREATE_FAILED", "Failed to create node of type: %s" % node_type)
163
-
164
- node.name = node_name
165
-
166
- for key in properties:
167
- if node.has_method("set") and key in node:
168
- var deserialized := MCPUtils.deserialize_value(properties[key])
169
- node.set(key, deserialized)
170
-
171
- parent.add_child(node)
172
- var scene_root := EditorInterface.get_edited_scene_root()
173
- _set_owner_recursive(node, scene_root)
174
-
175
- # Re-apply spatial transforms after add_child: the editor viewport may
176
- # snap newly added Node3D nodes to the current 3D cursor position,
177
- # overriding properties set before add_child.
178
- if node is Node3D:
179
- var n3d := node as Node3D
180
- if "position" in properties:
181
- n3d.position = MCPUtils.deserialize_value(properties["position"])
182
- if "rotation" in properties:
183
- n3d.rotation = MCPUtils.deserialize_value(properties["rotation"])
184
- if "scale" in properties:
185
- n3d.scale = MCPUtils.deserialize_value(properties["scale"])
186
-
187
- return _success({"node_path": str(scene_root.get_path_to(node))})
188
-
189
-
190
- func _set_owner_recursive(node: Node, owner: Node) -> void:
191
- node.owner = owner
192
- for child in node.get_children():
193
- _set_owner_recursive(child, owner)
194
-
195
-
196
120
  func update_node(params: Dictionary) -> Dictionary:
197
121
  var node_path: String = params.get("node_path", "")
198
122
  var properties: Dictionary = params.get("properties", {})
@@ -214,29 +138,6 @@ func update_node(params: Dictionary) -> Dictionary:
214
138
  return _success({})
215
139
 
216
140
 
217
- func delete_node(params: Dictionary) -> Dictionary:
218
- var scene_check := _require_scene_open()
219
- if not scene_check.is_empty():
220
- return scene_check
221
-
222
- var node_path: String = params.get("node_path", "")
223
- if node_path.is_empty():
224
- return _error("INVALID_PARAMS", "node_path is required")
225
-
226
- var node := _get_node(node_path)
227
- if not node:
228
- return _error("NODE_NOT_FOUND", "Node not found: %s" % node_path)
229
-
230
- var root := EditorInterface.get_edited_scene_root()
231
- if node == root:
232
- return _error("CANNOT_DELETE_ROOT", "Cannot delete the root node")
233
-
234
- node.get_parent().remove_child(node)
235
- node.queue_free()
236
-
237
- return _success({})
238
-
239
-
240
141
  func reparent_node(params: Dictionary) -> Dictionary:
241
142
  var scene_check := _require_scene_open()
242
143
  if not scene_check.is_empty():
@@ -270,45 +171,3 @@ func reparent_node(params: Dictionary) -> Dictionary:
270
171
  return _success({"new_path": str(root.get_path_to(node))})
271
172
 
272
173
 
273
- func connect_signal(params: Dictionary) -> Dictionary:
274
- var scene_check := _require_scene_open()
275
- if not scene_check.is_empty():
276
- return scene_check
277
-
278
- var node_path: String = params.get("node_path", "")
279
- var signal_name: String = params.get("signal_name", "")
280
- var target_path: String = params.get("target_path", "")
281
- var method_name: String = params.get("method_name", "")
282
-
283
- if node_path.is_empty():
284
- return _error("INVALID_PARAMS", "node_path is required")
285
- if signal_name.is_empty():
286
- return _error("INVALID_PARAMS", "signal_name is required")
287
- if target_path.is_empty():
288
- return _error("INVALID_PARAMS", "target_path is required")
289
- if method_name.is_empty():
290
- return _error("INVALID_PARAMS", "method_name is required")
291
-
292
- var source_node := _get_node(node_path)
293
- if not source_node:
294
- return _error("NODE_NOT_FOUND", "Source node not found: %s" % node_path)
295
-
296
- var target_node := _get_node(target_path)
297
- if not target_node:
298
- return _error("NODE_NOT_FOUND", "Target node not found: %s" % target_path)
299
-
300
- if not source_node.has_signal(signal_name):
301
- return _error("SIGNAL_NOT_FOUND", "Signal '%s' not found on node %s" % [signal_name, node_path])
302
-
303
- if source_node.is_connected(signal_name, Callable(target_node, method_name)):
304
- return _error("ALREADY_CONNECTED", "Signal '%s' is already connected to %s.%s()" % [signal_name, target_path, method_name])
305
-
306
- var err := source_node.connect(signal_name, Callable(target_node, method_name), CONNECT_PERSIST)
307
- if err != OK:
308
- return _error("CONNECT_FAILED", "Failed to connect signal: %s" % error_string(err))
309
-
310
- EditorInterface.mark_scene_as_unsaved()
311
-
312
- return _success({})
313
-
314
-
@@ -13,7 +13,7 @@ func get_commands() -> Dictionary:
13
13
 
14
14
  # Detect whether project.godot was edited on disk after the editor loaded it,
15
15
  # leaving the editor's in-memory ProjectSettings / InputMap stale (#245). Always
16
- # returns the full report (stale or not); recovery is `godot_editor restart`.
16
+ # returns the full report (stale or not); recovery is `godot_editor_edit restart`.
17
17
  func get_project_staleness(_params: Dictionary) -> Dictionary:
18
18
  return _success(MCPUtils.detect_project_staleness())
19
19
 
@@ -5,39 +5,25 @@ class_name MCPSceneCommands
5
5
 
6
6
  func get_commands() -> Dictionary:
7
7
  return {
8
- "get_current_scene": get_current_scene,
9
8
  "get_scene_tree": get_scene_tree,
10
9
  "open_scene": open_scene,
11
- "save_scene": save_scene,
12
- "create_scene": create_scene
10
+ "save_scene": save_scene
13
11
  }
14
12
 
15
13
 
16
- func get_current_scene(_params: Dictionary) -> Dictionary:
17
- var root := EditorInterface.get_edited_scene_root()
18
- if not root:
19
- return _success({
20
- "path": null,
21
- "root_name": null,
22
- "root_type": null
23
- })
24
-
25
- return _success({
26
- "path": root.scene_file_path,
27
- "root_name": root.name,
28
- "root_type": root.get_class()
29
- })
30
-
31
-
32
- func get_scene_tree(_params: Dictionary) -> Dictionary:
14
+ func get_scene_tree(params: Dictionary) -> Dictionary:
33
15
  var root := EditorInterface.get_edited_scene_root()
34
16
  if not root:
35
17
  return _error("NO_SCENE", "No scene is currently open")
36
18
 
37
- return _success({"tree": _build_tree(root)})
19
+ # 0 = unlimited for both caps (the default when the param is omitted), so the
20
+ # full tree is unchanged unless a caller opts into trimming it.
21
+ var max_depth: int = int(params.get("max_depth", 0))
22
+ var max_children: int = int(params.get("max_children", 0))
23
+ return _success({"tree": _build_tree(root, 1, max_depth, max_children)})
38
24
 
39
25
 
40
- func _build_tree(node: Node) -> Dictionary:
26
+ func _build_tree(node: Node, depth: int, max_depth: int, max_children: int) -> Dictionary:
41
27
  var result := {
42
28
  "name": node.name,
43
29
  "type": node.get_class(),
@@ -50,12 +36,29 @@ func _build_tree(node: Node) -> Dictionary:
50
36
  var pos: Vector3 = node.position
51
37
  result["position"] = {"x": pos.x, "y": pos.y, "z": pos.z}
52
38
 
39
+ var child_nodes := node.get_children()
40
+ var child_count := child_nodes.size()
41
+ if child_count == 0:
42
+ return result
43
+
44
+ # Depth cap: at the limit, stop recursing and just report how many direct
45
+ # children were cut off.
46
+ if max_depth > 0 and depth >= max_depth:
47
+ result["truncated_children"] = child_count
48
+ return result
49
+
50
+ # Breadth cap: list the first max_children and report the remainder.
51
+ var limit := child_count
52
+ if max_children > 0 and child_count > max_children:
53
+ limit = max_children
54
+
53
55
  var children: Array[Dictionary] = []
54
- for child in node.get_children():
55
- children.append(_build_tree(child))
56
+ for i in range(limit):
57
+ children.append(_build_tree(child_nodes[i], depth + 1, max_depth, max_children))
56
58
 
57
- if not children.is_empty():
58
- result["children"] = children
59
+ result["children"] = children
60
+ if limit < child_count:
61
+ result["truncated_children"] = child_count - limit
59
62
 
60
63
  return result
61
64
 
@@ -95,39 +98,3 @@ func save_scene(params: Dictionary) -> Dictionary:
95
98
 
96
99
  return _success({"path": path})
97
100
 
98
-
99
- func create_scene(params: Dictionary) -> Dictionary:
100
- var root_type: String = params.get("root_type", "")
101
- var root_name: String = params.get("root_name", root_type)
102
- var scene_path: String = params.get("scene_path", "")
103
-
104
- if root_type.is_empty():
105
- return _error("INVALID_PARAMS", "root_type is required")
106
- if scene_path.is_empty():
107
- return _error("INVALID_PARAMS", "scene_path is required")
108
-
109
- if not ClassDB.class_exists(root_type):
110
- return _error("INVALID_TYPE", "Unknown node type: %s" % root_type)
111
-
112
- var root: Node = ClassDB.instantiate(root_type)
113
- if not root:
114
- return _error("CREATE_FAILED", "Failed to create node of type: %s" % root_type)
115
-
116
- root.name = root_name
117
-
118
- var packed_scene := PackedScene.new()
119
- var err := packed_scene.pack(root)
120
- root.free()
121
-
122
- if err != OK:
123
- return _error("PACK_FAILED", "Failed to pack scene: %s" % error_string(err))
124
-
125
- err = ResourceSaver.save(packed_scene, scene_path)
126
- if err != OK:
127
- return _error("SAVE_FAILED", "Failed to save scene: %s" % error_string(err))
128
-
129
- EditorInterface.open_scene_from_path(scene_path)
130
-
131
- var uid := ResourceUID.id_to_text(ResourceLoader.get_resource_uid(scene_path))
132
- return _success({"path": scene_path, "uid": uid})
133
-
@@ -90,14 +90,27 @@ func set_2d_viewport(params: Dictionary) -> Dictionary:
90
90
  if not viewport:
91
91
  return _error("NO_VIEWPORT", "Could not access 2D editor viewport")
92
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)
93
+ var size := viewport.size
94
+
95
+ # Read the current view first so any omitted parameter preserves it instead of
96
+ # snapping to a default (a zoom-only call keeps the current center, not 0,0).
97
+ # This inverts the same transform math applied below — see
98
+ # _get_editor_2d_viewport_info().
99
+ var current := viewport.global_canvas_transform
100
+ var current_zoom: float = current.x.x
101
+ var current_offset: Vector2 = -current.origin / current_zoom
102
+ var current_center := Vector2(
103
+ current_offset.x + size.x / current_zoom / 2,
104
+ current_offset.y + size.y / current_zoom / 2
105
+ )
106
+
107
+ var center_x: float = params.get("center_x", current_center.x)
108
+ var center_y: float = params.get("center_y", current_center.y)
109
+ var zoom: float = params.get("zoom", current_zoom)
96
110
 
97
111
  if zoom <= 0:
98
112
  return _error("INVALID_PARAMS", "zoom must be positive")
99
113
 
100
- var size := viewport.size
101
114
  var offset := Vector2(center_x - size.x / zoom / 2, center_y - size.y / zoom / 2)
102
115
  var origin := -offset * zoom
103
116
 
@@ -34,27 +34,3 @@ func _require_scene_open() -> Dictionary:
34
34
  if not root:
35
35
  return _error("NO_SCENE", "No scene is currently open")
36
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
- var scene_root := EditorInterface.get_edited_scene_root()
51
- if scene_root:
52
- _find_nodes_recursive(root, type, result, scene_root)
53
- return result
54
-
55
-
56
- func _find_nodes_recursive(node: Node, type: String, result: Array[Dictionary], scene_root: Node) -> void:
57
- if node.is_class(type):
58
- result.append({"path": str(scene_root.get_path_to(node)), "name": node.name})
59
- for child in node.get_children():
60
- _find_nodes_recursive(child, type, result, scene_root)
@@ -3,14 +3,12 @@ extends EditorDebuggerPlugin
3
3
  class_name MCPDebuggerPlugin
4
4
 
5
5
  signal screenshot_received(success: bool, image_base64: String, width: int, height: int, error: String)
6
- signal debug_output_received(output: PackedStringArray)
7
6
  signal performance_metrics_received(metrics: Dictionary)
8
7
  signal find_nodes_received(matches: Array, count: int, error: String)
9
8
  signal input_map_received(actions: Array, error: String)
10
9
  signal input_sequence_completed(result: Dictionary)
11
10
  signal sequence_capture_received(requested_ms: int, actual_ms: int, ok: bool, image_base64: String, width: int, height: int, error: String)
12
11
  signal type_text_completed(result: Dictionary)
13
- signal game_response(message_type: String, data: Variant)
14
12
  signal bridge_ready()
15
13
 
16
14
  var _active_session_id: int = -1
@@ -23,7 +21,6 @@ var _pending_screenshot: bool = false
23
21
  # (element 6 of screenshot_result; empty for bridges that predate it). Held
24
22
  # here instead of widening the screenshot_received signal signature.
25
23
  var last_screenshot_warnings: Array = []
26
- var _pending_debug_output: bool = false
27
24
  var _pending_performance_metrics: bool = false
28
25
  var _pending_find_nodes: bool = false
29
26
  var _pending_input_map: bool = false
@@ -42,9 +39,6 @@ func _capture(message: String, data: Array, session_id: int) -> bool:
42
39
  "godot_mcp:screenshot_result":
43
40
  _handle_screenshot_result(data)
44
41
  return true
45
- "godot_mcp:debug_output_result":
46
- _handle_debug_output_result(data)
47
- return true
48
42
  "godot_mcp:performance_metrics_result":
49
43
  _handle_performance_metrics_result(data)
50
44
  return true
@@ -93,9 +87,6 @@ func _session_stopped() -> void:
93
87
  if _pending_screenshot:
94
88
  _pending_screenshot = false
95
89
  screenshot_received.emit(false, "", 0, 0, "Game session ended")
96
- if _pending_debug_output:
97
- _pending_debug_output = false
98
- debug_output_received.emit(PackedStringArray())
99
90
  if _pending_performance_metrics:
100
91
  _pending_performance_metrics = false
101
92
  performance_metrics_received.emit({})
@@ -159,25 +150,6 @@ func _handle_screenshot_result(data: Array) -> void:
159
150
  screenshot_received.emit(success, image_base64, width, height, error)
160
151
 
161
152
 
162
- func request_debug_output(clear: bool = false) -> void:
163
- if _active_session_id < 0:
164
- debug_output_received.emit(PackedStringArray())
165
- return
166
- _pending_debug_output = true
167
- var session := get_session(_active_session_id)
168
- if session:
169
- session.send_message("godot_mcp:get_debug_output", [clear])
170
- else:
171
- _pending_debug_output = false
172
- debug_output_received.emit(PackedStringArray())
173
-
174
-
175
- func _handle_debug_output_result(data: Array) -> void:
176
- _pending_debug_output = false
177
- var output: PackedStringArray = data[0] if data.size() > 0 else PackedStringArray()
178
- debug_output_received.emit(output)
179
-
180
-
181
153
  func request_performance_metrics() -> void:
182
154
  if _active_session_id < 0:
183
155
  performance_metrics_received.emit({})
@@ -322,7 +294,6 @@ func _handle_game_response(data: Array) -> void:
322
294
  var response_data: Variant = data[1]
323
295
  _pending_requests.erase(msg_type)
324
296
  _responses[msg_type] = response_data
325
- game_response.emit(msg_type, response_data)
326
297
 
327
298
 
328
299
  func toggle_frame_profiler(enable: bool) -> void: