Loading...
Loading...
MCP server for AI-assisted Godot 4 project inspection, editing, validation, and runtime automation via WebSocket bridge
npx skill4agent add aradotso/devtools-skills godot-devtool-mcp-serverSkill by ara.so — Devtools Skills collection.
godot-devtoolMCP Client (Claude Code, Cursor, Cline, etc.)
↓ stdio
Node.js MCP Server (build/index.js)
↓
├─ Native routes (file inspection/editing)
├─ Headless Godot routes (scene/resource ops)
├─ WebSocket bridge (ws://127.0.0.1:8766)
│ ├─ Editor plugin (live scene editing, Inspector, UndoRedo)
│ └─ Runtime autoload (running game inspection, input simulation)
└─ Browser visualizer (local HTTP dashboard)git clone https://github.com/wangdiandao/godot-devtool.git
cd godot-devtool
npm install
npm run build{
"mcpServers": {
"godot-devtool": {
"command": "node",
"args": ["E:/godot-devtool/build/index.js"],
"env": {
"GODOT_PATH": "D:/Program Files/Godot/Godot_v4.x.exe",
"GODOT_DEVTOOL_WS_PORT": "8766"
}
}
}
}[mcp_servers.godot-devtool]
command = "node"
args = ["E:/godot-devtool/build/index.js"]
env = { GODOT_PATH = "D:/Program Files/Godot/Godot_v4.x.exe", GODOT_DEVTOOL_WS_PORT = "8766" }GODOT_PATHgodotGODOT_DEVTOOL_WS_PORT// Call from MCP client
plugin_install({
projectPath: "E:/my-godot-project",
overwrite: true,
websocketPort: 8766
})autoload/DevtoolRuntime = *res://addons/godot_devtool/runtime_bridge.gdprojectPathcontextsessionIdrunIdeditorruntimerun_projectget_capabilities// Lightweight catalog (no schemas)
get_capabilities()
// Focused workflow with schemas
get_capabilities({
toolNames: ["plugin_install", "plugin_status", "scene_tree_inspect"],
includeSchemas: true
})
// Filter by route group
get_capabilities({
routeGroup: "scene",
includeSchemas: true
})
// Filter by transport
get_capabilities({
transport: "editor_ws",
includeSchemas: true
})project_setuplive_editorruntime_testmulti_instancerelease_verifyrun_projectplugin_statusplugin_cleanup_port// Get project metadata
get_project_info({
projectPath: "E:/my-godot-project"
})
// Read project settings
project_get_settings({
projectPath: "E:/my-godot-project",
sections: ["application/config", "display/window"]
})
// Update project settings (dry run first)
project_update_settings({
projectPath: "E:/my-godot-project",
settings: {
"application/config/name": "My Game",
"display/window/size/viewport_width": 1920
},
dryRun: true
})
// Configure input actions
project_input_action({
projectPath: "E:/my-godot-project",
action: "jump",
events: [
{ type: "InputEventKey", keycode: "KEY_SPACE" },
{ type: "InputEventJoypadButton", button_index: 0 }
]
})
// Run project
run_project({
projectPath: "E:/my-godot-project",
scene: "res://levels/level_01.tscn",
debugCollisions: true,
position: [100, 100],
size: [1280, 720]
})
// List running instances
list_run_instances({
projectPath: "E:/my-godot-project"
})
// Stop specific instance
stop_run_instance({
projectPath: "E:/my-godot-project",
runId: "run_12345"
})// Inspect scene tree (native)
scene_tree_inspect({
projectPath: "E:/my-godot-project",
scenePath: "res://player.tscn",
maxDepth: 3
})
// Inspect live editor scene (WebSocket)
editor_ws_scene_tree_inspect({
projectPath: "E:/my-godot-project",
maxDepth: 5
})
// Add node to live scene
editor_ws_node_add({
projectPath: "E:/my-godot-project",
parentPath: "Player/Body",
nodeType: "Sprite2D",
nodeName: "WeaponSprite",
position: 1
})
// Update node property
editor_ws_node_set_property({
projectPath: "E:/my-godot-project",
nodePath: "Player/WeaponSprite",
property: "texture",
value: { type: "Resource", path: "res://sprites/sword.png" }
})
// Delete node with undo
editor_ws_node_delete({
projectPath: "E:/my-godot-project",
nodePath: "Player/OldSprite",
undoable: true
})
// Save scene
editor_ws_scene_save({
projectPath: "E:/my-godot-project"
})// List GDScript files
script_index({
projectPath: "E:/my-godot-project",
includeTests: true
})
// Read script
script_read({
projectPath: "E:/my-godot-project",
scriptPath: "res://player.gd"
})
// Write script
script_write({
projectPath: "E:/my-godot-project",
scriptPath: "res://enemy.gd",
content: `extends CharacterBody2D
var speed = 200.0
func _physics_process(delta):
var direction = Vector2.ZERO
if Input.is_action_pressed("move_right"):
direction.x += 1
velocity = direction * speed
move_and_slide()
`
})
// Check syntax
script_check_syntax({
projectPath: "E:/my-godot-project",
scriptPath: "res://player.gd"
})
// Create and attach script
script_create({
projectPath: "E:/my-godot-project",
scriptPath: "res://powerup.gd",
template: "Node2D",
attachTo: "res://scenes/powerup.tscn"
})// Inspect running game scene tree
runtime_ws_scene_tree_inspect({
projectPath: "E:/my-godot-project",
rootPath: "/root/Game",
maxDepth: 4
})
// Get runtime node property
runtime_ws_node_get_property({
projectPath: "E:/my-godot-project",
nodePath: "/root/Game/Player",
property: "position"
})
// Set runtime property
runtime_ws_node_set_property({
projectPath: "E:/my-godot-project",
nodePath: "/root/Game/Player",
property: "health",
value: { type: "int", value: 100 }
})
// Simulate input action
runtime_ws_input_action({
projectPath: "E:/my-godot-project",
action: "jump",
pressed: true,
strength: 1.0
})
// Capture screenshot
runtime_ws_screenshot({
projectPath: "E:/my-godot-project",
outputPath: "E:/screenshots/test_jump.png"
})
// Wait for node to appear
runtime_ws_wait_for_node({
projectPath: "E:/my-godot-project",
nodePath: "/root/Game/VictoryScreen",
timeout: 5000
})
// Run QA assertion
runtime_ws_qa_assert({
projectPath: "E:/my-godot-project",
nodePath: "/root/Game/Player",
property: "health",
expected: { type: "int", value: 100 },
operator: "greater_than",
message: "Player should have full health at start"
})// List project files
file_list({
projectPath: "E:/my-godot-project",
directory: "res://sprites",
pattern: "*.png",
recursive: true
})
// Search in files
file_search({
projectPath: "E:/my-godot-project",
query: "extends CharacterBody2D",
paths: ["res://scripts"],
filePattern: "*.gd"
})
// Load resource metadata
resource_load({
projectPath: "E:/my-godot-project",
resourcePath: "res://player.tscn",
shallow: true
})
// Build dependency graph
resource_dependency_graph({
projectPath: "E:/my-godot-project",
resourcePath: "res://levels/level_01.tscn",
direction: "forward",
maxDepth: 3
})// Verify Godot installation
get_godot_version()
// Discover available tools
get_capabilities({
routeGroup: "project",
includeSchemas: true
})
// Get project info
get_project_info({ projectPath: "E:/my-game" })
// Check project settings
project_get_settings({
projectPath: "E:/my-game",
sections: ["application", "display", "input"]
})
// Install plugin for live editing
plugin_install({
projectPath: "E:/my-game",
overwrite: true,
websocketPort: 8766
})
// Verify plugin installation
plugin_status({ projectPath: "E:/my-game" })// 1. Ensure editor is open with plugin enabled
// 2. Get current editor selection
editor_ws_selection_get({ projectPath: "E:/my-game" })
// 3. Inspect current scene
editor_ws_scene_tree_inspect({
projectPath: "E:/my-game",
maxDepth: 5
})
// 4. Add a new node
editor_ws_node_add({
projectPath: "E:/my-game",
parentPath: "Player",
nodeType: "Area2D",
nodeName: "HitBox"
})
// 5. Configure the node
editor_ws_node_set_property({
projectPath: "E:/my-game",
nodePath: "Player/HitBox",
property: "collision_layer",
value: { type: "int", value: 2 }
})
// 6. Add collision shape
editor_ws_node_add({
projectPath: "E:/my-game",
parentPath: "Player/HitBox",
nodeType: "CollisionShape2D",
nodeName: "Shape"
})
// 7. Save the scene
editor_ws_scene_save({ projectPath: "E:/my-game" })// 1. Run the project
run_project({
projectPath: "E:/my-game",
scene: "res://test_level.tscn",
debugCollisions: true
})
// 2. Wait for game to load
await sleep(2000)
// 3. Inspect runtime scene tree
runtime_ws_scene_tree_inspect({
projectPath: "E:/my-game",
rootPath: "/root"
})
// 4. Check initial state
runtime_ws_node_get_property({
projectPath: "E:/my-game",
nodePath: "/root/Game/Player",
property: "position"
})
// 5. Simulate input
runtime_ws_input_action({
projectPath: "E:/my-game",
action: "move_right",
pressed: true
})
await sleep(1000)
runtime_ws_input_action({
projectPath: "E:/my-game",
action: "move_right",
pressed: false
})
// 6. Verify movement
runtime_ws_qa_assert({
projectPath: "E:/my-game",
nodePath: "/root/Game/Player",
property: "position.x",
expected: { type: "float", value: 0 },
operator: "greater_than",
message: "Player should move right"
})
// 7. Capture screenshot
runtime_ws_screenshot({
projectPath: "E:/my-game",
outputPath: "E:/test_results/player_moved.png"
})
// 8. Stop the test
stop_project({ projectPath: "E:/my-game" })// List all scenes
const scenes = await file_list({
projectPath: "E:/my-game",
directory: "res://",
pattern: "*.tscn",
recursive: true
})
// Check each scene for common issues
for (const scene of scenes.files) {
// Inspect scene tree
const tree = await scene_tree_inspect({
projectPath: "E:/my-game",
scenePath: scene.path,
maxDepth: 10
})
// Check for orphan nodes (no parent reference)
// Check for missing scripts
// Check for invalid resource paths
// Run syntax check on attached scripts
const scriptsToCheck = tree.nodes
.filter(n => n.script)
.map(n => n.script)
for (const scriptPath of scriptsToCheck) {
await script_check_syntax({
projectPath: "E:/my-game",
scriptPath: scriptPath
})
}
}GODOT_PATHGODOT_DEVTOOL_WS_PORTproject.godot// Add autoload
add_autoload({
projectPath: "E:/my-game",
name: "GameManager",
path: "res://singletons/game_manager.gd",
enabled: true
})
// Configure display settings
project_update_settings({
projectPath: "E:/my-game",
settings: {
"display/window/size/viewport_width": 1920,
"display/window/size/viewport_height": 1080,
"display/window/size/mode": 3, // fullscreen
"display/window/vsync/vsync_mode": 1
}
})project_input_action({
projectPath: "E:/my-game",
action: "attack",
events: [
{
type: "InputEventKey",
keycode: "KEY_CTRL"
},
{
type: "InputEventMouseButton",
button_index: 1 // MOUSE_BUTTON_LEFT
},
{
type: "InputEventJoypadButton",
button_index: 0, // BUTTON_A
device: -1 // any gamepad
}
],
deadzone: 0.5
})// Basic types
{ type: "int", value: 42 }
{ type: "float", value: 3.14 }
{ type: "bool", value: true }
{ type: "String", value: "Hello" }
// Vector types
{ type: "Vector2", value: [10, 20] }
{ type: "Vector3", value: [1, 2, 3] }
{ type: "Color", value: [1.0, 0.5, 0.0, 1.0] } // RGBA
// Resources
{ type: "Resource", path: "res://sprites/player.png" }
// NodePath
{ type: "NodePath", value: "../OtherNode" }
// Arrays
{ type: "Array", value: [1, 2, 3] }
// Dictionaries
{ type: "Dictionary", value: { "key1": "value1", "key2": 42 } }// 1. Check plugin status
plugin_status({ projectPath: "E:/my-game" })
// 2. Check if port is blocked
plugin_cleanup_port({
projectPath: "E:/my-game",
websocketPort: 8766,
kill: false // inspect only
})
// 3. If stale, kill and reinstall
plugin_cleanup_port({
projectPath: "E:/my-game",
websocketPort: 8766,
kill: true
})
plugin_reload({ projectPath: "E:/my-game" })get_autoload({
projectPath: "E:/my-game",
name: "DevtoolRuntime"
})run_projectplugin_status({ projectPath: "E:/my-game" })// List all running instances
list_run_instances({ projectPath: "E:/my-game" })
// Use runId for specific instance operations
runtime_ws_scene_tree_inspect({
projectPath: "E:/my-game",
runId: "run_12345"
})
// Stop specific instance
stop_run_instance({
projectPath: "E:/my-game",
runId: "run_12345"
})get_capabilities// Bad: returns all 234 tool schemas
get_capabilities({ includeSchemas: true }) // REJECTED
// Good: filter first
get_capabilities({
routeGroup: "scene",
transport: "editor_ws",
includeSchemas: true
})
// Or specify exact tools
get_capabilities({
toolNames: ["editor_ws_scene_tree_inspect", "editor_ws_node_add"],
includeSchemas: true
})// Explicit save after edits
editor_ws_scene_save({ projectPath: "E:/my-game" })
// Check if scene is modified
editor_ws_selection_get({ projectPath: "E:/my-game" })
// Returns: { sceneModified: true, ... }// Start visualizer on port 3456
browser_visualizer_start({
projectPath: "E:/my-game",
port: 3456
})
// Open http://127.0.0.1:3456/ in browser
// Shows:
// - Bridge connection status
// - Connected editor/runtime clients
// - Active sessions and run IDs
// - Available screenshot/scene/input routes
// - Pending command queue
// Stop visualizer
browser_visualizer_stop({
projectPath: "E:/my-game"
})// 1. Setup
const projectPath = "E:/my-game"
await plugin_install({
projectPath,
overwrite: true,
websocketPort: 8766
})
// 2. Configure input
await project_input_action({
projectPath,
action: "test_jump",
events: [{ type: "InputEventKey", keycode: "KEY_J" }]
})
// 3. Create test scene
await scene_create({
projectPath,
scenePath: "res://test_scenes/jump_test.tscn",
rootType: "Node2D"
})
// 4. Add player node (assuming editor is open)
await editor_ws_node_add({
projectPath,
parentPath: ".",
nodeType: "CharacterBody2D",
nodeName: "Player"
})
await editor_ws_node_set_property({
projectPath,
nodePath: "Player",
property: "position",
value: { type: "Vector2", value: [100, 100] }
})
await editor_ws_scene_save({ projectPath })
// 5. Run test
const runResult = await run_project({
projectPath,
scene: "res://test_scenes/jump_test.tscn"
})
await sleep(1000)
// 6. Simulate jump
await runtime_ws_input_action({
projectPath,
action: "test_jump",
pressed: true
})
await sleep(100)
await runtime_ws_input_action({
projectPath,
action: "test_jump",
pressed: false
})
// 7. Verify result
const finalPos = await runtime_ws_node_get_property({
projectPath,
nodePath: "/root/Node2D/Player",
property: "position.y"
})
await runtime_ws_qa_assert({
projectPath,
nodePath: "/root/Node2D/Player",
property: "position.y",
expected: { type: "float", value: 100 },
operator: "less_than",
message: "Player should have moved up after jump"
})
// 8. Cleanup
await stop_project({ projectPath })// Run multiple instances for multiplayer testing
const run1 = await run_project({
projectPath: "E:/my-game",
scene: "res://multiplayer_test.tscn",
position: [0, 0]
})
const run2 = await run_project({
projectPath: "E:/my-game",
scene: "res://multiplayer_test.tscn",
position: [800, 0]
})
// Control each instance separately
await runtime_ws_input_action({
projectPath: "E:/my-game",
runId: run1.runId,
action: "move_right",
pressed: true
})
await runtime_ws_input_action({
projectPath: "E:/my-game",
runId: run2.runId,
action: "move_left",
pressed: true
})
// Monitor both
const pos1 = await runtime_ws_node_get_property({
projectPath: "E:/my-game",
runId: run1.runId,
nodePath: "/root/Game/Player",
property: "position"
})
const pos2 = await runtime_ws_node_get_property({
projectPath: "E:/my-game",
runId: run2.runId,
nodePath: "/root/Game/Player",
property: "position"
})
// Cleanup
await stop_run_instance({ projectPath: "E:/my-game", runId: run1.runId })
await stop_run_instance({ projectPath: "E:/my-game", runId: run2.runId })get_capabilitiesdryRun: trueproject_update_settingsget_capabilitiesrouteGrouptransporttoolNamesprojectPathcontextsessionIdrunId