Godot Interactive Testing Skill
Playwright-like interaction loop for Godot games via MCP. Launch, observe, click, inspect, and manipulate a running game through a bridge autoload.
Overview
This skill enables LLM-driven interactive testing of Godot games through the godot-mcp MCP server and a GodotMCPBridge autoload. You can:
- Launch and stop Godot projects via MCP
- Capture viewport screenshots automatically or on-demand
- Inspect the scene tree with Control node positions and text
- Simulate clicks, key presses, and input actions
- Read and write node properties at runtime
- Implement iterative test/debug loops (observe → inspect → interact → observe)
This is particularly useful for:
- Automated UI testing
- Debugging visual issues
- Verifying game state without manual playtesting
- Creating reproducible test scenarios
- Validating UI layouts and interactions
Prerequisites
godot-mcp server:
- Installed at
~/code/godot-mcp(or configurable path) - Built with
npm install && npm run build - Repository: https://github.com/yourusername/godot-mcp
Claude Desktop configuration:
- MCP server configured in
~/.claude/mcp.json(see Setup)
Godot project setup:
- GodotMCPBridge autoload installed
- Remote debugger enabled (automatic when running via
run_project)
Setup for New Projects
1. Copy the Bridge Autoload
Copy the bridge script into your project:
cp assets/templates/godot_mcp_bridge.gd res://scripts/autoload/godot_mcp_bridge.gd
2. Register the Autoload
Add to project.godot:
[autoload]
GodotMCPBridge="*res://scripts/autoload/godot_mcp_bridge.gd"
Or via Godot editor:
- Project → Project Settings → Autoload
- Path:
res://scripts/autoload/godot_mcp_bridge.gd - Name:
GodotMCPBridge - Enable: Yes
3. Create MCP Configuration
Create .mcp.json at project root (use template):
cp assets/templates/mcp.json.template .mcp.json
Edit paths:
{
"mcpServers": {
"godot": {
"command": "node",
"args": ["<PATH_TO_GODOT_MCP>/build/index.js"],
"env": {
"GODOT_PATH": "<PATH_TO_GODOT_BINARY>"
}
}
}
}
Example paths:
- macOS:
/Applications/Godot 4.5.app/Contents/MacOS/Godot - Linux:
/usr/bin/godotor~/godot/bin/godot - Windows:
C:/Godot/godot.exe
Project-local godot-mcp:
If you keep godot-mcp in your project (e.g., .tools/godot-mcp):
"args": [".tools/godot-mcp/build/index.js"]
MCP Tools Reference
All tools are provided by the godot-mcp server and work through the GodotMCPBridge autoload.
run_project(projectPath)
Launch a Godot project with remote debugger enabled.
Parameters:
projectPath(string): Absolute path to project directory containingproject.godot
Returns:
- Success confirmation with process ID
Example:
run_project({ projectPath: "/Users/ben/code/my-game" })
Notes:
- Automatically enables remote debugger on port 6007
- Takes 2-3 seconds for project to fully start
- Bridge begins auto-capturing screenshots every 2 seconds
- Check
get_debug_output()if game doesn't appear to start
stop_project()
Stop the currently running Godot project.
Parameters: None
Returns:
- Success confirmation
Example:
stop_project()
Notes:
- Kills the Godot process gracefully
- Cleans up debugger connection
- Safe to call even if no project is running
get_debug_output()
Retrieve stdout/stderr from the running Godot process.
Parameters: None
Returns:
- Array of log lines (most recent first)
Example:
get_debug_output()
// Returns: ["Frame 1234", "Button pressed", "MCP command: click", ...]
Use cases:
- Check if game started successfully
- Debug print statements from your game
- Verify bridge initialization ("Godot MCP Bridge initialized")
- Inspect error messages
game_screenshot()
Capture the current viewport and return as base64 image.
Parameters: None
Returns:
- Base64-encoded PNG image of the game viewport
Example:
game_screenshot()
// Returns: base64 PNG data
Notes:
- Captures the main viewport at full resolution
- Bridge auto-saves screenshots to
/tmp/godot_screenshot.pngevery 2 seconds - This tool provides immediate capture via MCP response
- Works on stock Godot (unlike
capture_screenshotfrom upstream DAP)
game_scene_tree(path, depth)
Inspect the scene tree with node types, positions, and properties.
Parameters:
path(string, optional): Root path to start inspection, default:/rootdepth(number, optional): Traversal depth, default: 3
Returns:
- JSON tree structure with nodes
Node properties:
name: Node nametype: Node class (Control, Button, Label, Node3D, etc.)rect: For Control nodes:{x, y, w, h}in screen coordinatesvisible: For Control nodes: visibility statetext: For Button/Label nodes: display textchildren: Array of child nodes (up to depth limit)
Example:
game_scene_tree({ path: "/root/Main/UILayer", depth: 2 })
// Returns:
{
"name": "UILayer",
"type": "CanvasLayer",
"children": [
{
"name": "VictoryPopup",
"type": "CenterContainer",
"rect": {"x": 640, "y": 360, "w": 400, "h": 200},
"visible": true,
"children": [
{
"name": "OKButton",
"type": "Button",
"rect": {"x": 720, "y": 480, "w": 80, "h": 40},
"text": "OK",
"visible": true
}
]
}
]
}
Use cases:
- Find clickable UI elements (buttons, controls)
- Verify UI layout and positioning
- Check visibility states
- Locate nodes by name/type for property access
Key gotcha: UI coordinates are full resolution (e.g., 1920x1080), not screenshot pixel coordinates. Always use game_scene_tree to get accurate rect values for clicking.
game_click(x, y, button)
Simulate a mouse click at screen coordinates.
Parameters:
x(number): Screen X coordinatey(number): Screen Y coordinatebutton(number, optional): Mouse button index, default: 1 (left click)- 1: Left button (MOUSE_BUTTON_LEFT)
- 2: Right button (MOUSE_BUTTON_RIGHT)
- 3: Middle button (MOUSE_BUTTON_MIDDLE)
Returns:
- Success confirmation with coordinates
- Automatically captures screenshot after click
Example:
game_click({ x: 720, y: 480, button: 1 })
// Returns: {"type": "click", "success": true, "x": 720, "y": 480}
Notes:
- Simulates press + release with 50ms delay
- Takes a screenshot 200ms after release
- Use
game_scene_treeto find buttonrectcoordinates - Coordinates are in full window resolution, not SubViewport pixels
Workflow:
- Call
game_screenshot()to see the game - Call
game_scene_tree()to find button positions - Call
game_click()with button's center coordinates - Call
game_screenshot()to verify result
game_key(key)
Simulate a key press using Godot key names.
Parameters:
key(string): Godot key name (e.g., "Space", "Escape", "Enter", "A")
Returns:
- Success confirmation
- Automatically captures screenshot after key press
Example:
game_key({ key: "Space" })
game_key({ key: "Escape" })
game_key({ key: "Q" })
Common key names:
- Letters: "A", "B", "C", etc.
- Numbers: "0", "1", "2", etc.
- Special: "Space", "Enter", "Escape", "Tab"
- Arrows: "Up", "Down", "Left", "Right"
- Function: "F1", "F2", etc.
Notes:
- Uses
OS.find_keycode_from_string()- must be valid Godot key name - Simulates press + release with 50ms delay
- Takes screenshot 200ms after release
- Case-sensitive for letters (use uppercase "A", not "a")
game_action(name)
Trigger a Godot input action defined in InputMap.
Parameters:
name(string): Action name from project InputMap
Returns:
- Success confirmation
- Automatically captures screenshot after action
Example:
game_action({ name: "jump" })
game_action({ name: "ui_accept" })
game_action({ name: "shoot" })
Notes:
- Action must be defined in project's InputMap (project.godot)
- Simulates action press + release with 50ms delay
- Takes screenshot 200ms after release
- Works with multi-key bindings (triggers the action, not specific keys)
Use cases:
- Test gameplay actions without knowing exact key bindings
- Trigger complex input combinations (e.g., "dash" = Shift+W)
- Test controller/gamepad actions
game_get_property(node_path, property)
Read a node property at runtime.
Parameters:
node_path(string): Absolute node path from/root(e.g.,/root/Main/Player)property(string): Property name (e.g.,position,health,visible)
Returns:
- Property value (JSON-serialized)
Example:
game_get_property({ node_path: "/root/Main/Player", property: "position" })
// Returns: {"x": 5.2, "y": 0.0, "z": 3.8}
game_get_property({ node_path: "/root/Main/UILayer/HUD", property: "visible" })
// Returns: true
Supported types:
- Primitives: bool, int, float, String
- Vectors: Vector2, Vector2i, Vector3, Vector3i (as
{x, y, z}) - Color:
{r, g, b, a} - Arrays/Dictionaries: recursively converted
- Objects:
"<ClassName>" - NodePath: string representation
Use cases:
- Verify game state (player health, position, score)
- Check UI visibility and text
- Validate object properties after interactions
game_set_property(node_path, property, value)
Write a node property at runtime.
Parameters:
node_path(string): Absolute node path from/rootproperty(string): Property namevalue(any): New value (JSON-serializable)
Returns:
- Success confirmation
- Automatically captures screenshot after change
Example:
game_set_property({
node_path: "/root/Main/Player",
property: "position",
value: { x: 10, y: 0, z: 5 }
})
game_set_property({
node_path: "/root/Main/UILayer/VictoryPopup",
property: "visible",
value: true
})
Notes:
- Takes screenshot 100ms after property change
- Use JSON object syntax for Vector2/Vector3:
{x, y}or{x, y, z} - For testing, you can force game states (show popups, move player, etc.)
Use cases:
- Force UI visibility for testing
- Teleport player to test positions
- Set health/score to test edge cases
- Trigger game states without playing through
Workflow Patterns
Pattern 1: Basic UI Test Loop
Test a button click and verify the result.
Steps:
-
Launch the game:
run_project({ projectPath: "/Users/ben/code/my-game" }) -
Wait for startup (check logs):
get_debug_output() // Look for "Godot MCP Bridge initialized" -
Observe initial state:
game_screenshot() -
Inspect scene tree to find button:
game_scene_tree({ path: "/root/Main/UILayer", depth: 3 }) // Note the button's rect: {x: 720, y: 480, w: 80, h: 40} -
Click button center:
game_click({ x: 760, y: 500 }) // Center of button -
Verify result:
game_screenshot() // See visual result get_debug_output() // Check for print statements
Pattern 2: Property Inspection and Validation
Verify game state after actions.
Steps:
-
Launch and perform action:
run_project({ projectPath: "/path/to/game" }) game_click({ x: 640, y: 360 }) // Click play button -
Read game state:
game_get_property({ node_path: "/root/Main/Player", property: "health" }) // Returns: 100 game_get_property({ node_path: "/root/Main/Ball", property: "position" }) // Returns: {"x": 0, "y": 0, "z": 0} -
Trigger gameplay:
game_action({ name: "select_card" }) -
Verify state change:
game_get_property({ node_path: "/root/Main/Ball", property: "position" }) // Returns: {"x": 5, "y": 0, "z": 3} // Ball moved!
Pattern 3: Forcing Game States for Testing
Set up specific scenarios without manual play.
Steps:
-
Launch game:
run_project({ projectPath: "/path/to/game" }) -
Force victory state:
game_set_property({ node_path: "/root/Main", property: "state", value: "VICTORY" }) -
Show victory popup:
game_set_property({ node_path: "/root/Main/UILayer/VictoryPopup", property: "visible", value: true }) -
Verify UI appears correctly:
game_screenshot() game_scene_tree({ path: "/root/Main/UILayer/VictoryPopup" }) -
Test OK button:
game_click({ x: 760, y: 500 })
Pattern 4: Debugging Visual Issues
Investigate rendering problems.
Steps:
-
Launch game:
run_project({ projectPath: "/path/to/game" }) -
Capture screenshot:
game_screenshot() // Observe: button is not visible -
Inspect button properties:
game_get_property({ node_path: "/root/Main/UILayer/PlayButton", property: "visible" }) // Returns: false // Aha! Button is hidden -
Check parent visibility:
game_get_property({ node_path: "/root/Main/UILayer", property: "visible" }) // Returns: true -
Fix the bug (in code):
# In your script: $PlayButton.visible = true # Was missing this line -
Restart and verify:
stop_project() run_project({ projectPath: "/path/to/game" }) game_screenshot()
Key Gotchas
Gotcha 1: UI Coordinates Are Full Resolution
Problem: Screenshots are 640x360 (SubViewport), but UI coordinates are 1920x1080 (window resolution).
Solution: Always use game_scene_tree to get button rect values. Do not manually calculate click coordinates from screenshot pixels.
Example:
// ❌ WRONG: Clicking screenshot pixel coordinates
game_screenshot() // 640x360 image
game_click({ x: 320, y: 180 }) // Center of screenshot = WRONG
// ✅ CORRECT: Use scene_tree rect
game_scene_tree({ path: "/root/Main/UILayer" })
// Returns: { name: "PlayButton", rect: {x: 860, y: 520, w: 200, h: 80} }
game_click({ x: 960, y: 560 }) // Center of rect = CORRECT
Gotcha 2: Stock Godot DAP Limitation
Problem: The upstream Godot DAP capture_screenshot tool doesn't work on stock Godot builds.
Solution: Use game_screenshot() from this skill instead. It works through the file-based bridge protocol (/tmp/godot_screenshot.png).
Why it matters: Other Godot DAP tools may advertise capture_screenshot, but it requires a custom Godot build with DAP screenshot support. The bridge's game_screenshot() always works.
Gotcha 3: Bridge Initialization Delay
Problem: Calling MCP tools immediately after run_project() may fail if bridge isn't ready.
Solution: Check get_debug_output() for "Godot MCP Bridge initialized" before calling game interaction tools.
Example:
run_project({ projectPath: "/path/to/game" })
// Wait 2-3 seconds or poll debug output
get_debug_output()
// Look for: "Godot MCP Bridge initialized — screenshots: /tmp/godot_screenshot.png..."
Gotcha 4: Scene Tree Depth Limit
Problem: game_scene_tree() defaults to depth 3, may not show deeply nested nodes.
Solution: Increase depth parameter for deeper inspection, or query specific subtrees.
Example:
// Shallow inspection
game_scene_tree({ path: "/root/Main", depth: 2 })
// Deep inspection of UI subtree
game_scene_tree({ path: "/root/Main/UILayer/VictoryPopup", depth: 5 })
Gotcha 5: File-Based Protocol Synchronization
Problem: Bridge uses file-based communication (/tmp/godot_mcp_command.json and /tmp/godot_mcp_response.json), which is polled every frame.
Solution: MCP tools handle synchronization automatically. Response is written after command completes. No manual delays needed.
How it works:
- MCP tool writes command to
/tmp/godot_mcp_command.json - Bridge polls the file every frame
- Bridge executes command and deletes command file
- Bridge writes response to
/tmp/godot_mcp_response.json - Bridge prints
MCP_RESPONSE:{...}to stdout - MCP tool reads response and returns to caller
You don't need to do anything special - just call the tools normally.
Bridge Communication Protocol
The GodotMCPBridge autoload implements a file-based protocol for communication:
Command file: /tmp/godot_mcp_command.json
- MCP server writes JSON commands here
- Bridge polls every frame
- Bridge deletes after reading
Response file: /tmp/godot_mcp_response.json
- Bridge writes JSON responses here
- MCP server reads responses
Screenshot file: /tmp/godot_screenshot.png
- Bridge auto-saves every 2 seconds
- Bridge updates after clicks/keys (200ms delay)
- MCP tools can read this file directly
Debug output:
- Bridge prints
MCP_RESPONSE:{...}to stdout - MCP server captures via
get_debug_output()
Command format:
{
"action": "click",
"x": 640,
"y": 360,
"button": 1
}
Response format:
{
"type": "click",
"success": true,
"x": 640,
"y": 360
}
Best Practices
1. Always Inspect Before Clicking
Use game_scene_tree to find button positions - don't guess coordinates.
// ✅ CORRECT workflow
game_scene_tree({ path: "/root/Main/UILayer" })
// Find button rect
game_click({ x: rect.x + rect.w/2, y: rect.y + rect.h/2 })
// ❌ WRONG workflow
game_screenshot()
// Guess coordinates from image
game_click({ x: 640, y: 360 })
2. Check Debug Output for Errors
After interactions, check get_debug_output() for print statements and errors.
game_click({ x: 640, y: 360 })
get_debug_output()
// Look for "Button pressed" or error messages
3. Use Property Inspection for Validation
Don't just rely on screenshots - verify internal game state.
game_click({ x: 640, y: 360 })
game_get_property({ node_path: "/root/Main/Player", property: "health" })
// Confirm health decreased as expected
4. Force States for Faster Testing
Use game_set_property to skip to specific game states.
// Skip tutorial, go straight to level 5
game_set_property({ node_path: "/root/Main", property: "current_level", value: 5 })
5. Clean Up After Tests
Always stop the project when done to free resources.
stop_project()
Common Use Cases
Use Case 1: Automated UI Regression Testing
Test that buttons still work after code changes.
run_project({ projectPath: "/path/to/game" })
game_screenshot()
game_scene_tree({ path: "/root/Main/UILayer" })
game_click({ x: 640, y: 360 }) // Play button
game_get_property({ node_path: "/root/Main", property: "state" })
// Assert: state is "PLAYING"
stop_project()
Use Case 2: Visual Debugging
Investigate why a UI element doesn't appear.
run_project({ projectPath: "/path/to/game" })
game_screenshot() // See the issue
game_scene_tree({ path: "/root/Main/UILayer" }) // Check hierarchy
game_get_property({ node_path: "/root/Main/UILayer/MissingButton", property: "visible" })
// Debug: visible is false!
Use Case 3: Game State Validation
Verify gameplay logic by checking properties.
run_project({ projectPath: "/path/to/game" })
game_action({ name: "attack" })
game_get_property({ node_path: "/root/Main/Enemy", property: "health" })
// Verify: health decreased correctly
Use Case 4: Screenshot Documentation
Capture screenshots of all UI states for documentation.
run_project({ projectPath: "/path/to/game" })
game_screenshot() // Main menu
game_click({ x: 640, y: 360 }) // Start game
game_screenshot() // Gameplay
game_set_property({ node_path: "/root/Main/Popup", property: "visible", value: true })
game_screenshot() // Settings popup
Troubleshooting
Game Won't Start
Symptoms: run_project() succeeds but no screenshot appears.
Check:
- Verify Godot path in
.mcp.jsonis correct - Check
get_debug_output()for errors - Ensure bridge autoload is registered in
project.godot
Solution:
get_debug_output()
// Look for "Godot MCP Bridge initialized"
// If missing, bridge isn't loaded
Click Doesn't Work
Symptoms: game_click() succeeds but button doesn't respond.
Check:
- Coordinates are correct (use
game_scene_tree) - Button is visible (
game_get_property) - UI is not blocked by another node
Solution:
game_scene_tree({ path: "/root/Main/UILayer" })
// Verify button rect
game_get_property({ node_path: "/root/Main/UILayer/Button", property: "visible" })
// Verify button is visible
Property Not Found
Symptoms: game_get_property() returns error "Node not found".
Check:
- Node path is correct (use
game_scene_treeto find path) - Node exists at runtime (may be dynamically created)
Solution:
game_scene_tree({ path: "/root/Main", depth: 5 })
// Find the correct node path
Screenshot Is Black/Empty
Symptoms: Screenshot is solid black or shows nothing.
Check:
- Game has finished rendering (wait 2-3 seconds after launch)
- Camera is active and positioned correctly
- Scene has visible nodes
Solution:
run_project({ projectPath: "/path/to/game" })
// Wait 3 seconds
get_debug_output()
// Check for "Frame N" output indicating rendering
game_screenshot()
Summary
The godot-interactive skill enables automated testing and debugging of Godot games through MCP tools:
- Launch games with
run_project()and remote debugger - Observe state with
game_screenshot()andgame_scene_tree() - Interact with
game_click(),game_key(),game_action() - Inspect/modify with
game_get_property()andgame_set_property() - Iterate in a test loop until behavior is verified
Key insights:
- File-based protocol is reliable and works on stock Godot
- Always use
game_scene_treefor accurate UI coordinates - Bridge autoload handles all synchronization automatically
- Property inspection validates internal state, not just visuals
This skill transforms Godot development from manual playtesting to automated, reproducible test scenarios.