@satelliteoflove/godot-mcp 3.21.1 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -59
- package/addon/command_router.gd +1 -0
- package/addon/commands/mesh_commands.gd +52 -0
- package/addon/commands/mesh_commands.gd.uid +1 -0
- package/addon/commands/screenshot_commands.gd +9 -2
- package/addon/core/mcp_debugger_plugin.gd +5 -0
- package/addon/game_bridge/mcp_game_bridge.gd +67 -1
- package/addon/game_bridge/mesh_validator.gd +361 -0
- package/addon/game_bridge/mesh_validator.gd.uid +1 -0
- package/addon/plugin.cfg +1 -1
- package/dist/__tests__/tools/docs.test.js +9 -1
- package/dist/__tests__/tools/docs.test.js.map +1 -1
- package/dist/__tests__/tools/editor.test.js +23 -0
- package/dist/__tests__/tools/editor.test.js.map +1 -1
- package/dist/__tests__/tools/validate-meshes.test.d.ts +2 -0
- package/dist/__tests__/tools/validate-meshes.test.d.ts.map +1 -0
- package/dist/__tests__/tools/validate-meshes.test.js +73 -0
- package/dist/__tests__/tools/validate-meshes.test.js.map +1 -0
- package/dist/__tests__/tools/watch-contract.test.d.ts +2 -0
- package/dist/__tests__/tools/watch-contract.test.d.ts.map +1 -0
- package/dist/__tests__/tools/watch-contract.test.js +165 -0
- package/dist/__tests__/tools/watch-contract.test.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/editor.d.ts.map +1 -1
- package/dist/tools/editor.js +19 -1
- package/dist/tools/editor.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/validate-meshes.d.ts +7 -0
- package/dist/tools/validate-meshes.d.ts.map +1 -0
- package/dist/tools/validate-meshes.js +59 -0
- package/dist/tools/validate-meshes.js.map +1 -0
- package/dist/utils/usage-logger.d.ts.map +1 -1
- package/dist/utils/usage-logger.js +2 -0
- package/dist/utils/usage-logger.js.map +1 -1
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -1,20 +1,55 @@
|
|
|
1
1
|
# godot-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@satelliteoflove/godot-mcp)
|
|
4
|
+
[](https://github.com/satelliteoflove/godot-mcp/actions/workflows/ci.yml)
|
|
5
|
+
[](https://godotengine.org)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://github.com/satelliteoflove/godot-mcp/blob/main/LICENSE)
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
Give your AI assistant eyes and hands in the Godot editor — and a running game it can actually playtest.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
<!-- HERO PLACEHOLDER: short demo GIF goes here (agent running the game, stepping time, reading state). Use an absolute raw.githubusercontent.com URL so it also renders on npm. -->
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## Why this one?
|
|
14
|
+
|
|
15
|
+
There are a few Godot MCP servers out there, and most can open a scene and poke at nodes. This one is built around a harder problem: **letting an agent verify its own work.** Run the game, drive it like a player, observe what actually happened, and prove the change did what it claimed — without you ferrying screenshots and error logs back and forth.
|
|
16
|
+
|
|
17
|
+
The pieces that make that possible, and that you won't find elsewhere:
|
|
18
|
+
|
|
19
|
+
- **Deterministic playtesting.** Freeze the game clock, step exact slices of game time (or step *until a condition holds*), with inputs riding inside the window. Observation never races the game.
|
|
20
|
+
- **Cheap observation.** Live entity state — positions, velocities, animation state, your own custom data — as structured JSON. Most "what's happening on screen?" questions get answered without spending vision tokens on a screenshot.
|
|
21
|
+
- **Real input.** Named actions with analog strength, joypad buttons and stick vectors, raw keys with modifier combos, relative mouse-look, text typing. Sequences run with precise timing and can prove they changed game state.
|
|
22
|
+
- **Scenario setup.** Run GDScript inside the running game: grant the weapon, skip to wave 3, spawn a test bot — no debug hooks baked into your game code.
|
|
23
|
+
|
|
24
|
+
Here's what that looks like when an agent tests a boss fight:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
godot_editor run frozen=true # boot with game time frozen at frame 0
|
|
28
|
+
godot_exec GameState.wave = 3 # set up the scenario worth testing
|
|
29
|
+
godot_game_time step_until "tree.get_nodes_in_group('boss').size() >= 1"
|
|
30
|
+
godot_runtime_state digest # exact positions and state — no pixels, no guessing
|
|
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
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Less copy-paste, more creating.
|
|
10
36
|
|
|
11
37
|
## Quick Start
|
|
12
38
|
|
|
13
39
|
### 1. Configure your AI assistant
|
|
14
40
|
|
|
15
|
-
Add godot-mcp to your MCP client. See the [Installation Guide](INSTALL.md) for config examples (Claude Desktop, Claude Code, VSCode/Copilot, and more).
|
|
41
|
+
Add godot-mcp to your MCP client. See the [Installation Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/INSTALL.md) for config examples (Claude Desktop, Claude Code, VSCode/Copilot, and more). The short version:
|
|
16
42
|
|
|
17
|
-
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"godot-mcp": {
|
|
47
|
+
"command": "npx",
|
|
48
|
+
"args": ["-y", "@satelliteoflove/godot-mcp"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
18
53
|
|
|
19
54
|
### 2. Install the Godot addon
|
|
20
55
|
|
|
@@ -22,76 +57,84 @@ While you're at it, add [minimal-godot-mcp](https://github.com/ryanmazzolini/min
|
|
|
22
57
|
npx @satelliteoflove/godot-mcp --install-addon /path/to/your/godot/project
|
|
23
58
|
```
|
|
24
59
|
|
|
25
|
-
Enable in Godot: **Project Settings > Plugins > Godot MCP**
|
|
60
|
+
Enable it in Godot: **Project > Project Settings > Plugins > Godot MCP**
|
|
26
61
|
|
|
27
|
-
### 3.
|
|
62
|
+
### 3. Start building
|
|
28
63
|
|
|
29
|
-
Open your Godot project, restart your AI assistant, and start building.
|
|
64
|
+
Open your Godot project, restart your AI assistant, and start building. If anything refuses to connect, the [Troubleshooting Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/troubleshooting.md) has you covered.
|
|
30
65
|
|
|
31
|
-
## What
|
|
66
|
+
## What's in the box
|
|
32
67
|
|
|
33
|
-
|
|
34
|
-
- **Inspect** nodes, resources, animations, tilemaps, 3D spatial data
|
|
35
|
-
- **Modify** scenes, nodes, scripts, animations, tilemaps directly
|
|
36
|
-
- **Test** by running the game and injecting input
|
|
37
|
-
- **Learn** by fetching Godot docs on demand
|
|
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).
|
|
38
69
|
|
|
39
|
-
|
|
70
|
+
| Tool | What it does |
|
|
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 |
|
|
78
|
+
| `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_docs` | Fetch Godot documentation as clean markdown, version-matched to your editor |
|
|
81
|
+
| `godot_input` | Inject input into the running game: actions, joypad, raw keys, mouse-look, text |
|
|
82
|
+
| `godot_profiler` | Metric snapshots and per-frame time series with spike detection |
|
|
83
|
+
| `godot_runtime_state` | Live game state as JSON: one-shot digests, watch windows, signal timelines |
|
|
84
|
+
| `godot_game_time` | Freeze, step, and step-until on the game clock — deterministic observation |
|
|
85
|
+
| `godot_exec` | Run GDScript inside the running game for test scenario setup |
|
|
40
86
|
|
|
41
|
-
|
|
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.
|
|
42
88
|
|
|
43
|
-
|
|
89
|
+
## Things to ask for
|
|
44
90
|
|
|
45
|
-
|
|
91
|
+
A feel for what an agent can do with this, in plain prompts:
|
|
46
92
|
|
|
47
|
-
|
|
93
|
+
- *"Run the game and screenshot the title screen. Does the layout survive 1280x720?"*
|
|
94
|
+
- *"The player can clip through the east wall. Reproduce it, then check the collision shapes and tell me why."*
|
|
95
|
+
- *"Step the game until the second wave spawns and give me every enemy's position and velocity."*
|
|
96
|
+
- *"Hold the left stick at half deflection for two seconds — does the walk animation blend correctly?"*
|
|
97
|
+
- *"Profile ten seconds of gameplay and find what's causing the frame spikes."*
|
|
98
|
+
- *"Add a hit-flash animation to the Player: modulate to red and back over 0.2 seconds."*
|
|
48
99
|
|
|
49
|
-
|
|
50
|
-
- [Claude Code Setup Guide](../docs/claude-code-setup.md) - CLAUDE.md template for Godot projects
|
|
51
|
-
- [Runtime State Guide](../docs/runtime-state-guide.md) - Expose game state to agents via the `mcp_watch` group and `_mcp_state()`
|
|
52
|
-
- [Tools Reference](../docs/tools/README.md) - All 12 tools with full API docs
|
|
53
|
-
- [Resources Reference](../docs/resources.md) - MCP resources for reading project data
|
|
54
|
-
- [Contributing](../CONTRIBUTING.md) - Dev setup, adding tools, release process
|
|
55
|
-
- [Changelog](CHANGELOG.md) - Release history
|
|
100
|
+
For richer runtime observation, add key nodes to the `mcp_watch` group or give them a `_mcp_state()` method — see the [Runtime State Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/runtime-state-guide.md).
|
|
56
101
|
|
|
57
|
-
##
|
|
102
|
+
## How it works
|
|
58
103
|
|
|
104
|
+
```text
|
|
105
|
+
┌─────────────────┐ stdio ┌─────────────────┐ WebSocket ┌──────────────────┐ debugger ┌──────────────┐
|
|
106
|
+
│ MCP client │ ◄────────► │ godot-mcp │ ◄──────────► │ Bridge addon │ ◄─────────► │ Running game │
|
|
107
|
+
│ (Claude, etc.) │ │ server (Node) │ :6550 │ (Godot editor) │ wire │ (autoload) │
|
|
108
|
+
└─────────────────┘ └─────────────────┘ └──────────────────┘ └──────────────┘
|
|
59
109
|
```
|
|
60
|
-
[Claude/AI Assistant/MCP Client] <--stdio--> [MCP Server] <--WebSocket:6550--> [Godot MCP Bridge Addon]
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
WSL2 is supported (auto-detection, host IP discovery, configurable bind modes). See the [Installation Guide](INSTALL.md#wsl-support) for setup details.
|
|
64
|
-
|
|
65
|
-
## CLI smoke test (paste-ready JSON-RPC)
|
|
66
110
|
|
|
67
|
-
|
|
111
|
+
The server talks to an editor addon over a local WebSocket; the addon reaches into the running game over Godot's own debugger protocol, so the game process needs no extra ports or setup. The addon binds to `127.0.0.1` by default. WSL2 is fully supported (auto-detection, host IP discovery, configurable bind modes) — see the [Installation Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/INSTALL.md#wsl-support).
|
|
68
112
|
|
|
69
|
-
|
|
113
|
+
Curious about connection lifecycles, the single-client policy, or how frozen-time stepping works under the hood? The [Architecture Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/architecture.md) goes deep.
|
|
70
114
|
|
|
71
|
-
|
|
72
|
-
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"cli-test","version":"0"}}}
|
|
73
|
-
```
|
|
115
|
+
## Works well with minimal-godot-mcp
|
|
74
116
|
|
|
75
|
-
|
|
117
|
+
[minimal-godot-mcp](https://github.com/ryanmazzolini/minimal-godot-mcp) by [@ryanmazzolini](https://github.com/ryanmazzolini) covers exactly what this server doesn't: LSP diagnostics for fast static GDScript checking, and the running game's console output over DAP. This server covers everything that needs an editor bridge. No overlap, no conflict — run both, and your agent gets static analysis and the game console alongside full editor and runtime control.
|
|
76
118
|
|
|
77
|
-
|
|
78
|
-
{"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{},"resources":{},"logging":{}},"serverInfo":{"name":"godot-mcp","version":"2.11.0"}},"jsonrpc":"2.0","id":1}
|
|
79
|
-
```
|
|
119
|
+
One caveat: a Godot editor serves a single godot-mcp client at a time. Extra clients (a subagent inheriting your MCP config, say) wait their turn rather than hijacking your session — details in [Troubleshooting](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/troubleshooting.md#one-client-at-a-time).
|
|
80
120
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
```json
|
|
84
|
-
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"godot_editor","arguments":{"action":"get_state"}}}
|
|
85
|
-
```
|
|
121
|
+
## Documentation
|
|
86
122
|
|
|
87
|
-
|
|
123
|
+
- [Installation Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/INSTALL.md) — MCP client configs for Claude Desktop, Claude Code, VSCode/Copilot, and more
|
|
124
|
+
- [Troubleshooting](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/troubleshooting.md) — connection checklist, CLI smoke test, common fixes
|
|
125
|
+
- [Claude Code Setup Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/claude-code-setup.md) — CLAUDE.md template for Godot projects
|
|
126
|
+
- [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
|
|
129
|
+
- [Architecture Guide](https://github.com/satelliteoflove/godot-mcp/blob/main/docs/architecture.md) — how the server, addon, and game bridge fit together
|
|
130
|
+
- [Contributing](https://github.com/satelliteoflove/godot-mcp/blob/main/CONTRIBUTING.md) — dev setup, adding tools, release process
|
|
131
|
+
- [Changelog](https://github.com/satelliteoflove/godot-mcp/blob/main/server/CHANGELOG.md) — release history
|
|
88
132
|
|
|
89
|
-
|
|
90
|
-
{"result":{"content":[{"type":"text","text":"{\n \"current_scene\": null,\n \"godot_version\": \"4.5.1-stable (official)\",\n \"is_playing\": false,\n \"main_screen\": \"unknown\",\n \"open_scenes\": []\n}"}]},"jsonrpc":"2.0","id":2}
|
|
91
|
-
```
|
|
133
|
+
## Requirements
|
|
92
134
|
|
|
93
|
-
|
|
94
|
-
|
|
135
|
+
- **Godot 4.5+** (the addon uses the Logger class introduced in 4.5)
|
|
136
|
+
- **Node.js 20+**
|
|
137
|
+
- Any MCP client that speaks stdio
|
|
95
138
|
|
|
96
139
|
## Development
|
|
97
140
|
|
|
@@ -102,11 +145,8 @@ npm test
|
|
|
102
145
|
npm run generate-docs
|
|
103
146
|
```
|
|
104
147
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
- Node.js 20+
|
|
108
|
-
- Godot 4.5+
|
|
148
|
+
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.
|
|
109
149
|
|
|
110
150
|
## License
|
|
111
151
|
|
|
112
|
-
MIT
|
|
152
|
+
[MIT](https://github.com/satelliteoflove/godot-mcp/blob/main/LICENSE)
|
package/addon/command_router.gd
CHANGED
|
@@ -24,6 +24,7 @@ func setup(plugin: EditorPlugin) -> void:
|
|
|
24
24
|
_register_handler(MCPRuntimeStateCommands.new(), plugin)
|
|
25
25
|
_register_handler(MCPGameTimeCommands.new(), plugin)
|
|
26
26
|
_register_handler(MCPExecCommands.new(), plugin)
|
|
27
|
+
_register_handler(MCPMeshCommands.new(), plugin)
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
func _register_handler(handler: MCPBaseCommand, plugin: EditorPlugin) -> void:
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends MCPBaseCommand
|
|
3
|
+
class_name MCPMeshCommands
|
|
4
|
+
## Relays mesh-integrity commands to the running game's bridge.
|
|
5
|
+
|
|
6
|
+
# Full validation walks every ArrayMesh surface — generous timeout for big scenes.
|
|
7
|
+
const VALIDATE_TIMEOUT := 20.0
|
|
8
|
+
|
|
9
|
+
var _last_error: Dictionary = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
func get_commands() -> Dictionary:
|
|
13
|
+
return {
|
|
14
|
+
"validate_meshes": validate_meshes,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
func validate_meshes(params: Dictionary) -> Dictionary:
|
|
19
|
+
var result = await _send_and_wait("validate_meshes", [params], VALIDATE_TIMEOUT)
|
|
20
|
+
if result == null:
|
|
21
|
+
return _last_error
|
|
22
|
+
if result is Dictionary:
|
|
23
|
+
return _success(result)
|
|
24
|
+
return _success({"data": result})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
func _send_and_wait(msg_type: String, args: Array, timeout: float):
|
|
28
|
+
if not EditorInterface.is_playing_scene():
|
|
29
|
+
_last_error = _error("NOT_RUNNING", "No game is currently running")
|
|
30
|
+
return null
|
|
31
|
+
|
|
32
|
+
var debugger_plugin = _plugin.get_debugger_plugin() if _plugin else null
|
|
33
|
+
if debugger_plugin == null or not debugger_plugin.has_active_session():
|
|
34
|
+
_last_error = _error("NO_SESSION", "No active debug session")
|
|
35
|
+
return null
|
|
36
|
+
|
|
37
|
+
var sent: bool = debugger_plugin.send_game_message(msg_type, args)
|
|
38
|
+
if not sent:
|
|
39
|
+
_last_error = _error("SEND_FAILED", "Failed to send message to game")
|
|
40
|
+
return null
|
|
41
|
+
|
|
42
|
+
var start_time := Time.get_ticks_msec()
|
|
43
|
+
while not debugger_plugin.has_response(msg_type):
|
|
44
|
+
await Engine.get_main_loop().process_frame
|
|
45
|
+
if (Time.get_ticks_msec() - start_time) / 1000.0 > timeout:
|
|
46
|
+
debugger_plugin.clear_response(msg_type)
|
|
47
|
+
_last_error = _error("TIMEOUT", "Timed out waiting for %s response" % msg_type)
|
|
48
|
+
return null
|
|
49
|
+
|
|
50
|
+
var response = debugger_plugin.get_response(msg_type)
|
|
51
|
+
debugger_plugin.clear_response(msg_type)
|
|
52
|
+
return response
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://60lww3uge36h
|
|
@@ -50,11 +50,18 @@ func capture_game_screenshot(params: Dictionary) -> Dictionary:
|
|
|
50
50
|
func _on_screenshot_received(success: bool, image_base64: String, width: int, height: int, error: String) -> void:
|
|
51
51
|
_screenshot_pending = false
|
|
52
52
|
if success:
|
|
53
|
-
|
|
53
|
+
var payload := {
|
|
54
54
|
"image_base64": image_base64,
|
|
55
55
|
"width": width,
|
|
56
56
|
"height": height
|
|
57
|
-
}
|
|
57
|
+
}
|
|
58
|
+
# Mesh-integrity warnings ride the same game message (no extra
|
|
59
|
+
# round-trip, no version-skew timeout); pass them through so the
|
|
60
|
+
# server can attach the advisory to the image.
|
|
61
|
+
var dp = _plugin.get_debugger_plugin() if _plugin else null
|
|
62
|
+
if dp != null and not (dp.last_screenshot_warnings as Array).is_empty():
|
|
63
|
+
payload["mesh_warnings"] = dp.last_screenshot_warnings
|
|
64
|
+
_screenshot_result = _success(payload)
|
|
58
65
|
else:
|
|
59
66
|
_screenshot_result = _error("CAPTURE_FAILED", error)
|
|
60
67
|
|
|
@@ -19,6 +19,10 @@ var _active_session_id: int = -1
|
|
|
19
19
|
# has_active_session() alone is not enough to know input will land (#241).
|
|
20
20
|
var _bridge_ready: bool = false
|
|
21
21
|
var _pending_screenshot: bool = false
|
|
22
|
+
# Mesh-integrity warnings that rode along with the last screenshot result
|
|
23
|
+
# (element 6 of screenshot_result; empty for bridges that predate it). Held
|
|
24
|
+
# here instead of widening the screenshot_received signal signature.
|
|
25
|
+
var last_screenshot_warnings: Array = []
|
|
22
26
|
var _pending_debug_output: bool = false
|
|
23
27
|
var _pending_performance_metrics: bool = false
|
|
24
28
|
var _pending_find_nodes: bool = false
|
|
@@ -151,6 +155,7 @@ func _handle_screenshot_result(data: Array) -> void:
|
|
|
151
155
|
var width: int = data[2]
|
|
152
156
|
var height: int = data[3]
|
|
153
157
|
var error: String = data[4]
|
|
158
|
+
last_screenshot_warnings = data[5] if data.size() > 5 and data[5] is Array else []
|
|
154
159
|
screenshot_received.emit(success, image_base64, width, height, error)
|
|
155
160
|
|
|
156
161
|
|
|
@@ -3,6 +3,7 @@ class_name MCPGameBridge
|
|
|
3
3
|
|
|
4
4
|
const DEFAULT_MAX_WIDTH := 1024
|
|
5
5
|
const Onscreen := preload("onscreen.gd")
|
|
6
|
+
const MeshValidator := preload("mesh_validator.gd")
|
|
6
7
|
|
|
7
8
|
# Cap on frames waited for the main scene to appear before announcing ready
|
|
8
9
|
# anyway. The scene is normally added within a frame or two of the bridge
|
|
@@ -18,6 +19,17 @@ var _sampler: MCPRuntimeStateSampler
|
|
|
18
19
|
# announcement against firing twice and lets the headless test observe it.
|
|
19
20
|
var _ready_announced := false
|
|
20
21
|
|
|
22
|
+
# On-scene-load mesh-integrity sniff. Corrupt procedural meshes render with no
|
|
23
|
+
# error anywhere (inside-out winding, dropped triangles), so the warning must
|
|
24
|
+
# come TO the agent: the server appends these one-liners to screenshot results
|
|
25
|
+
# — the moment the agent looks at the game is the moment a wrong-looking render
|
|
26
|
+
# becomes actionable. Full diagnosis lives in the validate_meshes command.
|
|
27
|
+
const SNIFF_DELAY_FRAMES := 30 # let _ready-time procedural level builds finish
|
|
28
|
+
const SNIFF_MAX_WARNINGS := 8
|
|
29
|
+
var _mesh_warnings: Array[String] = []
|
|
30
|
+
var _sniff_scene_id: int = 0
|
|
31
|
+
var _sniff_countdown: int = -1
|
|
32
|
+
|
|
21
33
|
|
|
22
34
|
func _ready() -> void:
|
|
23
35
|
# The bridge must keep processing while the scene tree is paused. Input
|
|
@@ -95,6 +107,52 @@ func _emit_bridge_ready(scene_path: String) -> void:
|
|
|
95
107
|
func _process(delta: float) -> void:
|
|
96
108
|
_game_time_process(delta)
|
|
97
109
|
_sequence_process(delta)
|
|
110
|
+
_mesh_sniff_process()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
func _mesh_sniff_process() -> void:
|
|
114
|
+
# The autoload ships in exports and _process runs even when _ready bailed
|
|
115
|
+
# out early — without this gate, non-debug runs (including shipped games)
|
|
116
|
+
# would pay full mesh-array copies on every scene load for a result
|
|
117
|
+
# nothing consumes.
|
|
118
|
+
if not EngineDebugger.is_active():
|
|
119
|
+
return
|
|
120
|
+
var tree := get_tree()
|
|
121
|
+
if tree == null or tree.current_scene == null:
|
|
122
|
+
return
|
|
123
|
+
var sid := tree.current_scene.get_instance_id()
|
|
124
|
+
if sid != _sniff_scene_id:
|
|
125
|
+
_sniff_scene_id = sid
|
|
126
|
+
_sniff_countdown = SNIFF_DELAY_FRAMES
|
|
127
|
+
if _sniff_countdown > 0:
|
|
128
|
+
_sniff_countdown -= 1
|
|
129
|
+
if _sniff_countdown == 0:
|
|
130
|
+
_run_mesh_sniff(tree.current_scene)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
func _run_mesh_sniff(scene_root: Node) -> void:
|
|
134
|
+
_mesh_warnings.clear()
|
|
135
|
+
var stack: Array[Node] = [scene_root]
|
|
136
|
+
while not stack.is_empty():
|
|
137
|
+
var n: Node = stack.pop_back()
|
|
138
|
+
for child in n.get_children():
|
|
139
|
+
stack.push_back(child)
|
|
140
|
+
for w in MeshValidator.sniff(n):
|
|
141
|
+
_mesh_warnings.append(w)
|
|
142
|
+
if _mesh_warnings.size() >= SNIFF_MAX_WARNINGS:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
func _handle_validate_meshes(data: Array) -> void:
|
|
147
|
+
var params: Dictionary = data[0] if data.size() > 0 and data[0] is Dictionary else {}
|
|
148
|
+
var max_findings: int = params.get("max_findings", 25)
|
|
149
|
+
var tree := get_tree()
|
|
150
|
+
var result: Dictionary
|
|
151
|
+
if tree == null or tree.current_scene == null:
|
|
152
|
+
result = {"checked_meshes": 0, "checked_surfaces": 0, "total_findings": 0, "findings": [], "note": "no current scene"}
|
|
153
|
+
else:
|
|
154
|
+
result = MeshValidator.validate(tree.current_scene, max_findings)
|
|
155
|
+
EngineDebugger.send_message("godot_mcp:game_response", ["validate_meshes", result])
|
|
98
156
|
|
|
99
157
|
|
|
100
158
|
# Processing is needed by three independent features; only switch it off when
|
|
@@ -415,6 +473,9 @@ func _on_debugger_message(message: String, data: Array) -> bool:
|
|
|
415
473
|
"exec_clear":
|
|
416
474
|
_handle_exec_clear(data)
|
|
417
475
|
return true
|
|
476
|
+
"validate_meshes":
|
|
477
|
+
_handle_validate_meshes(data)
|
|
478
|
+
return true
|
|
418
479
|
return false
|
|
419
480
|
|
|
420
481
|
|
|
@@ -442,12 +503,17 @@ func _capture_and_send_screenshot(max_width: int) -> void:
|
|
|
442
503
|
image.resize(max_width, new_height, Image.INTERPOLATE_LANCZOS)
|
|
443
504
|
var png_buffer := image.save_png_to_buffer()
|
|
444
505
|
var base64 := Marshalls.raw_to_base64(png_buffer)
|
|
506
|
+
# Element 6 piggybacks the scene's cached mesh-integrity warnings: the
|
|
507
|
+
# moment the agent LOOKS at a wrong-looking render is when they're
|
|
508
|
+
# actionable, and riding the same message costs no extra round-trip and
|
|
509
|
+
# cannot time out on version skew (older receivers ignore the element).
|
|
445
510
|
EngineDebugger.send_message("godot_mcp:screenshot_result", [
|
|
446
511
|
true,
|
|
447
512
|
base64,
|
|
448
513
|
image.get_width(),
|
|
449
514
|
image.get_height(),
|
|
450
|
-
""
|
|
515
|
+
"",
|
|
516
|
+
_mesh_warnings.duplicate()
|
|
451
517
|
])
|
|
452
518
|
|
|
453
519
|
|