
Godot Interactive
- 8 installs
- 52 repo stars
- Updated March 4, 2026
- bfollington/terma
godot-interactive is a Claude Code skill that interacts with a running Godot game via godot-mcp to launch, screenshot, click, inspect, and manipulate it.
About
godot-interactive is a Claude Code skill that drives a running Godot game through the godot-mcp server and a GodotMCPBridge autoload. It can launch and stop the game, capture screenshots, inspect the scene tree, simulate clicks and key presses, and read or write node properties. Developers use it for automated UI testing, debugging visual issues, and verifying game state without manual playtesting.
- Launches, screenshots, clicks, and inspects a running Godot game via MCP
- Playwright-like observe-inspect-interact loop for game testing
- Reads and writes node properties at runtime through a bridge autoload
Godot Interactive by the numbers
- 8 all-time installs (skills.sh)
- Ranked #200 of 247 Game Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
godot-interactive capabilities & compatibility
- Capabilities
- testing · debugging
- Works with
- playwright
- Use cases
- testing · debugging
- Platforms
- macOS · Linux · Windows
- Runs
- Runs locally
What godot-interactive says it does
Interact with a running Godot game via MCP — launch, screenshot, click, inspect scene tree, get/set properties.
Playwright-like interaction loop for Godot games via MCP.
Bridge auto-saves screenshots to `/tmp/godot_screenshot.png` every 2 seconds
npx skills add https://github.com/bfollington/terma --skill godot-interactiveAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 52 |
| Last updated | March 4, 2026 |
| Repository | bfollington/terma ↗ |
What it does
Run automated interactive tests against a live Godot game via MCP.
Who is it for?
Automated UI testing and debugging of a running Godot game.
When should I use this skill?
You want to launch, observe, and interact with a running Godot game to test it.
What you get
Reproducible, LLM-driven interactive tests that launch, observe, and manipulate a running game.
- screenshots
- scene tree inspection
- interactive test runs
By the numbers
- Bridge auto-saves screenshots to /tmp/godot_screenshot.png every 2 seconds
Files
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.gd2. 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.jsonEdit 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 dataNotes:
- 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: 1. Call game_screenshot() to see the game 2. Call game_scene_tree() to find button positions 3. Call game_click() with button's center coordinates 4. 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: trueSupported 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:
1. Launch the game:
run_project({ projectPath: "/Users/ben/code/my-game" })2. Wait for startup (check logs):
get_debug_output()
// Look for "Godot MCP Bridge initialized"3. Observe initial state:
game_screenshot()4. 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}5. Click button center:
game_click({ x: 760, y: 500 }) // Center of button6. Verify result:
game_screenshot() // See visual result
get_debug_output() // Check for print statementsPattern 2: Property Inspection and Validation
Verify game state after actions.
Steps:
1. Launch and perform action:
run_project({ projectPath: "/path/to/game" })
game_click({ x: 640, y: 360 }) // Click play button2. 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}3. Trigger gameplay:
game_action({ name: "select_card" })4. 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:
1. Launch game:
run_project({ projectPath: "/path/to/game" })2. Force victory state:
game_set_property({
node_path: "/root/Main",
property: "state",
value: "VICTORY"
})3. Show victory popup:
game_set_property({
node_path: "/root/Main/UILayer/VictoryPopup",
property: "visible",
value: true
})4. Verify UI appears correctly:
game_screenshot()
game_scene_tree({ path: "/root/Main/UILayer/VictoryPopup" })5. Test OK button:
game_click({ x: 760, y: 500 })Pattern 4: Debugging Visual Issues
Investigate rendering problems.
Steps:
1. Launch game:
run_project({ projectPath: "/path/to/game" })2. Capture screenshot:
game_screenshot()
// Observe: button is not visible3. Inspect button properties:
game_get_property({ node_path: "/root/Main/UILayer/PlayButton", property: "visible" })
// Returns: false // Aha! Button is hidden4. Check parent visibility:
game_get_property({ node_path: "/root/Main/UILayer", property: "visible" })
// Returns: true5. Fix the bug (in code):
# In your script:
$PlayButton.visible = true # Was missing this line6. 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 = CORRECTGotcha 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: 1. MCP tool writes command to /tmp/godot_mcp_command.json 2. Bridge polls the file every frame 3. Bridge executes command and deletes command file 4. Bridge writes response to /tmp/godot_mcp_response.json 5. Bridge prints MCP_RESPONSE:{...} to stdout 6. 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 messages3. 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 expected4. 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 correctlyUse 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 popupTroubleshooting
Game Won't Start
Symptoms: run_project() succeeds but no screenshot appears.
Check: 1. Verify Godot path in .mcp.json is correct 2. Check get_debug_output() for errors 3. Ensure bridge autoload is registered in project.godot
Solution:
get_debug_output()
// Look for "Godot MCP Bridge initialized"
// If missing, bridge isn't loadedClick Doesn't Work
Symptoms: game_click() succeeds but button doesn't respond.
Check: 1. Coordinates are correct (use game_scene_tree) 2. Button is visible (game_get_property) 3. 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 visibleProperty Not Found
Symptoms: game_get_property() returns error "Node not found".
Check: 1. Node path is correct (use game_scene_tree to find path) 2. Node exists at runtime (may be dynamically created)
Solution:
game_scene_tree({ path: "/root/Main", depth: 5 })
// Find the correct node pathScreenshot Is Black/Empty
Symptoms: Screenshot is solid black or shows nothing.
Check: 1. Game has finished rendering (wait 2-3 seconds after launch) 2. Camera is active and positioned correctly 3. 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:
1. Launch games with run_project() and remote debugger 2. Observe state with game_screenshot() and game_scene_tree() 3. Interact with game_click(), game_key(), game_action() 4. Inspect/modify with game_get_property() and game_set_property() 5. 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.
extends Node
## Godot MCP Bridge
## Autoload singleton enabling Claude Code to observe and control the game.
##
## Features:
## - Auto-saves viewport screenshots to /tmp/godot_screenshot.png every 2s
## - Polls /tmp/godot_mcp_command.json for input commands (click, key, action)
## - Writes responses to /tmp/godot_mcp_response.json
##
## Usage from Claude Code:
## Write command JSON to /tmp/godot_mcp_command.json
## Read /tmp/godot_screenshot.png to see result
## Read /tmp/godot_mcp_response.json for command confirmation
const SCREENSHOT_PATH := "/tmp/godot_screenshot.png"
const COMMAND_PATH := "/tmp/godot_mcp_command.json"
const RESPONSE_PATH := "/tmp/godot_mcp_response.json"
const SCREENSHOT_INTERVAL := 2.0
var response_prefix := "MCP_RESPONSE:"
var _screenshot_timer := 0.0
var _frames_since_ready := 0
func _ready():
# Clean up stale command file from previous runs
if FileAccess.file_exists(COMMAND_PATH):
DirAccess.remove_absolute(COMMAND_PATH)
print(response_prefix + '{"type":"ready","bridge_version":"3.0"}')
print("Godot MCP Bridge initialized — screenshots: %s, commands: %s" % [SCREENSHOT_PATH, COMMAND_PATH])
func _process(delta: float):
# Wait a few frames for the scene to render before first screenshot
if _frames_since_ready < 10:
_frames_since_ready += 1
if _frames_since_ready == 10:
_save_screenshot()
return
# Poll for command file
_poll_commands()
# Periodic screenshots
_screenshot_timer += delta
if _screenshot_timer >= SCREENSHOT_INTERVAL:
_screenshot_timer = 0.0
_save_screenshot()
# --- Screenshots ---
func _save_screenshot():
var viewport = get_tree().root.get_viewport()
if not viewport:
return
var img = viewport.get_texture().get_image()
if not img:
return
img.save_png(SCREENSHOT_PATH)
func request_screenshot():
_save_screenshot()
_write_response({"type": "screenshot", "success": true, "path": SCREENSHOT_PATH})
# --- Command polling ---
func _poll_commands():
if not FileAccess.file_exists(COMMAND_PATH):
return
var f = FileAccess.open(COMMAND_PATH, FileAccess.READ)
if not f:
return
var text = f.get_as_text()
f.close()
# Delete the command file immediately so we don't re-process
DirAccess.remove_absolute(COMMAND_PATH)
if text.strip_edges().is_empty():
return
var json = JSON.new()
var err = json.parse(text)
if err != OK:
_write_response({"type": "error", "message": "Invalid JSON: " + json.get_error_message()})
return
var cmd = json.data
if cmd is not Dictionary:
_write_response({"type": "error", "message": "Command must be a JSON object"})
return
_execute_command(cmd)
func _execute_command(cmd: Dictionary):
var action = cmd.get("action", "")
print("MCP command: " + action)
match action:
"click":
_cmd_click(cmd)
"key":
_cmd_key(cmd)
"action_press":
_cmd_action(cmd)
"screenshot":
request_screenshot()
"get_tree":
_cmd_get_tree(cmd)
"get_property":
_cmd_get_property(cmd)
"set_property":
_cmd_set_property(cmd)
_:
_write_response({"type": "error", "message": "Unknown action: " + action})
func _write_response(data: Dictionary):
var f = FileAccess.open(RESPONSE_PATH, FileAccess.WRITE)
if f:
f.store_string(JSON.stringify(data, "\t"))
f.close()
print(response_prefix + JSON.stringify(data))
# --- Input simulation ---
func _cmd_click(cmd: Dictionary):
var x = int(cmd.get("x", 0))
var y = int(cmd.get("y", 0))
var button = int(cmd.get("button", MOUSE_BUTTON_LEFT))
# Press
var press = InputEventMouseButton.new()
press.button_index = button
press.pressed = true
press.position = Vector2(x, y)
press.global_position = Vector2(x, y)
Input.parse_input_event(press)
# Release after a short delay
await get_tree().create_timer(0.05).timeout
var release = InputEventMouseButton.new()
release.button_index = button
release.pressed = false
release.position = Vector2(x, y)
release.global_position = Vector2(x, y)
Input.parse_input_event(release)
# Take a screenshot right after to show the result
await get_tree().create_timer(0.2).timeout
_save_screenshot()
_write_response({"type": "click", "success": true, "x": x, "y": y})
func _cmd_key(cmd: Dictionary):
var keycode_str = cmd.get("key", "")
var keycode = OS.find_keycode_from_string(keycode_str)
if keycode == KEY_NONE:
_write_response({"type": "error", "message": "Unknown key: " + keycode_str})
return
var press = InputEventKey.new()
press.keycode = keycode
press.pressed = true
Input.parse_input_event(press)
await get_tree().create_timer(0.05).timeout
var release = InputEventKey.new()
release.keycode = keycode
release.pressed = false
Input.parse_input_event(release)
await get_tree().create_timer(0.2).timeout
_save_screenshot()
_write_response({"type": "key", "success": true, "key": keycode_str})
func _cmd_action(cmd: Dictionary):
var action_name = cmd.get("name", "")
if not InputMap.has_action(action_name):
_write_response({"type": "error", "message": "Unknown action: " + action_name})
return
Input.action_press(action_name)
await get_tree().create_timer(0.05).timeout
Input.action_release(action_name)
await get_tree().create_timer(0.2).timeout
_save_screenshot()
_write_response({"type": "action_press", "success": true, "name": action_name})
# --- Scene inspection ---
func _cmd_get_tree(cmd: Dictionary):
var root_path = cmd.get("path", "/root")
var max_depth = int(cmd.get("depth", 3))
var node = get_tree().root.get_node_or_null(root_path.replace("/root", "."))
if root_path == "/root":
node = get_tree().root
if not node:
_write_response({"type": "error", "message": "Node not found: " + root_path})
return
var tree = _node_to_dict(node, 0, max_depth)
_write_response({"type": "get_tree", "success": true, "tree": tree})
func _resolve_node(path: String) -> Node:
if path == "/root":
return get_tree().root
var relative = path.replace("/root/", "")
return get_tree().root.get_node_or_null(relative)
func _cmd_get_property(cmd: Dictionary):
var node_path = cmd.get("node_path", "")
var property = cmd.get("property", "")
var node = _resolve_node(node_path)
if not node:
_write_response({"type": "error", "message": "Node not found: " + node_path})
return
var value = node.get(property)
# Convert to JSON-safe types
var json_value = _variant_to_json(value)
_write_response({"type": "get_property", "success": true, "node": node_path, "property": property, "value": json_value})
func _cmd_set_property(cmd: Dictionary):
var node_path = cmd.get("node_path", "")
var property = cmd.get("property", "")
var value = cmd.get("value")
var node = _resolve_node(node_path)
if not node:
_write_response({"type": "error", "message": "Node not found: " + node_path})
return
node.set(property, value)
await get_tree().create_timer(0.1).timeout
_save_screenshot()
_write_response({"type": "set_property", "success": true, "node": node_path, "property": property})
func _variant_to_json(value) -> Variant:
if value is Vector2:
return {"x": value.x, "y": value.y}
elif value is Vector2i:
return {"x": value.x, "y": value.y}
elif value is Vector3:
return {"x": value.x, "y": value.y, "z": value.z}
elif value is Vector3i:
return {"x": value.x, "y": value.y, "z": value.z}
elif value is Color:
return {"r": value.r, "g": value.g, "b": value.b, "a": value.a}
elif value is Transform2D:
return str(value)
elif value is Transform3D:
return str(value)
elif value is Array:
var arr := []
for item in value:
arr.append(_variant_to_json(item))
return arr
elif value is Dictionary:
var d := {}
for key in value:
d[key] = _variant_to_json(value[key])
return d
elif value is NodePath:
return str(value)
elif value is Object:
return "<" + value.get_class() + ">"
else:
return value
func _node_to_dict(node: Node, depth: int, max_depth: int) -> Dictionary:
var d := {
"name": node.name,
"type": node.get_class(),
}
if node is Control:
d["rect"] = {
"x": int(node.global_position.x),
"y": int(node.global_position.y),
"w": int(node.size.x),
"h": int(node.size.y),
}
d["visible"] = node.visible
if node is BaseButton:
d["text"] = node.text if node.has_method("get") and "text" in node else ""
if node is Label:
d["text"] = node.text
if depth < max_depth:
var children := []
for child in node.get_children():
children.append(_node_to_dict(child, depth + 1, max_depth))
if children.size() > 0:
d["children"] = children
return d
{
"mcpServers": {
"godot": {
"command": "node",
"args": ["<PATH_TO_GODOT_MCP>/build/index.js"],
"env": {
"GODOT_PATH": "<PATH_TO_GODOT_BINARY>"
}
}
}
}
Related skills
FAQ
What does it need to run?
It needs the godot-mcp server and a GodotMCPBridge autoload registered in the project.
What can it do to a running game?
It can launch/stop it, screenshot the viewport, inspect the scene tree, simulate input, and get/set node properties.