refactor: 拆分 claude-dev-stack 为 windows-dev-stack 和 wsl-dev-stack

将原 claude-dev-stack 目录拆分为独立的 Windows 和 WSL 部署栈,便于分别维护和使用。

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-29 01:11:20 +08:00
parent e8693dad2a
commit dd3eb24d0f
488 changed files with 33927 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env node
/**
* godot-cli — Command-line interface for Godot MCP Pro
*
* Connects directly to the Godot editor plugin via WebSocket (JSON-RPC 2.0).
* Designed for LLMs that can use bash/terminal tools but have tight MCP tool limits.
* Progressive disclosure via --help at each command level.
*/
export {};
//# sourceMappingURL=cli.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA;;;;;;GAMG"}

View File

@@ -0,0 +1,705 @@
#!/usr/bin/env node
/**
* godot-cli — Command-line interface for Godot MCP Pro
*
* Connects directly to the Godot editor plugin via WebSocket (JSON-RPC 2.0).
* Designed for LLMs that can use bash/terminal tools but have tight MCP tool limits.
* Progressive disclosure via --help at each command level.
*/
import { WebSocketServer } from "ws";
import { randomUUID } from "crypto";
import { createServer } from "net";
const BASE_PORT = 6510;
const MAX_PORT = 6514;
const CONNECT_TIMEOUT_MS = 10000;
const COMMAND_TIMEOUT_MS = 30000;
const COMMANDS = {
project: {
description: "Project info, files, and settings",
commands: {
info: {
description: "Get project metadata (name, version, viewport, renderer, autoloads)",
method: "get_project_info",
},
files: {
description: "List project file/directory tree",
method: "get_filesystem_tree",
args: {
path: { description: "Root path (default: res://)" },
filter: { description: "Glob filter (e.g. '*.gd', '*.tscn')" },
},
},
search: {
description: "Search for files by name pattern",
method: "search_files",
args: {
query: { description: "Search query (fuzzy match or glob)", required: true },
path: { description: "Directory to search in" },
file_type: { description: "Filter by extension (e.g. 'gd', 'tscn')" },
},
},
grep: {
description: "Search inside file contents",
method: "search_in_files",
args: {
query: { description: "Text/regex pattern", required: true },
path: { description: "Directory to search in" },
file_type: { description: "File extension filter (e.g. 'gd', 'tscn')" },
},
},
"get-setting": {
description: "Get project settings",
method: "get_project_settings",
args: {
category: { description: "Settings category filter" },
},
},
"set-setting": {
description: "Set a project setting",
method: "set_project_setting",
args: {
setting: { description: "Setting path (e.g. display/window/size/viewport_width)", required: true },
value: { description: "Value to set", required: true },
},
mapArgs: (p) => ({ setting: p.setting, value: autoType(p.value) }),
},
},
},
scene: {
description: "Scene tree and scene management",
commands: {
tree: {
description: "Get the current scene tree",
method: "get_scene_tree",
args: {
max_depth: { description: "Maximum depth to display", type: "number" },
},
mapArgs: (p) => (p.max_depth ? { max_depth: parseInt(p.max_depth) } : {}),
},
create: {
description: "Create a new scene with a root node",
method: "create_scene",
args: {
path: { description: "Scene path (e.g. res://scenes/player.tscn)", required: true },
root_type: { description: "Root node type (default: Node2D)" },
root_name: { description: "Root node name" },
},
},
open: {
description: "Open a scene in the editor",
method: "open_scene",
args: {
path: { description: "Scene path to open", required: true },
},
},
save: {
description: "Save the current scene",
method: "save_scene",
args: {
path: { description: "Optional path to save as" },
},
},
play: {
description: "Run the current/specified scene",
method: "play_scene",
args: {
mode: { description: "'main' (default), 'current', or a scene path" },
},
},
stop: {
description: "Stop the running scene",
method: "stop_scene",
},
content: {
description: "Get scene file content (tscn format parsed)",
method: "get_scene_file_content",
args: {
path: { description: "Scene file path", required: true },
},
},
exports: {
description: "Get exported variables of a scene",
method: "get_scene_exports",
args: {
path: { description: "Scene file path", required: true },
},
},
delete: {
description: "Delete a scene file",
method: "delete_scene",
args: {
path: { description: "Scene file path to delete", required: true },
},
},
instance: {
description: "Add a scene instance as child node",
method: "add_scene_instance",
args: {
scene_path: { description: "Path to .tscn file to instance", required: true },
parent_path: { description: "Parent node path (default: selected/root)" },
name: { description: "Instance name" },
},
},
},
},
node: {
description: "Add, modify, and delete scene nodes",
commands: {
add: {
description: "Add a new node to the scene",
method: "add_node",
args: {
type: { description: "Node type (e.g. CharacterBody3D, Sprite2D)", required: true },
name: { description: "Node name" },
parent_path: { description: "Parent node path (default: root '.')" },
},
},
delete: {
description: "Delete a node from the scene",
method: "delete_node",
args: {
node_path: { description: "Node path to delete", required: true },
},
},
get: {
description: "Get all properties of a node",
method: "get_node_properties",
args: {
node_path: { description: "Node path", required: true },
},
},
set: {
description: "Set a property on a node",
method: "update_property",
args: {
node_path: { description: "Node path", required: true },
property: { description: "Property name", required: true },
value: { description: "Value to set", required: true },
},
mapArgs: (p) => ({ node_path: p.node_path, property: p.property, value: autoType(p.value) }),
},
duplicate: {
description: "Duplicate a node",
method: "duplicate_node",
args: {
node_path: { description: "Node path to duplicate", required: true },
name: { description: "Name for the duplicate" },
},
},
move: {
description: "Move/reparent a node",
method: "move_node",
args: {
node_path: { description: "Node path to move", required: true },
new_parent_path: { description: "New parent path", required: true },
},
},
rename: {
description: "Rename a node",
method: "rename_node",
args: {
node_path: { description: "Node path", required: true },
new_name: { description: "New name", required: true },
},
},
connect: {
description: "Connect a signal between nodes",
method: "connect_signal",
args: {
source_path: { description: "Source node path", required: true },
signal_name: { description: "Signal name", required: true },
target_path: { description: "Target node path", required: true },
method_name: { description: "Target method name", required: true },
},
},
groups: {
description: "Get groups a node belongs to",
method: "get_node_groups",
args: {
node_path: { description: "Node path", required: true },
},
},
},
},
script: {
description: "Read, create, and edit GDScript/C# files",
commands: {
read: {
description: "Read a script file",
method: "read_script",
args: {
path: { description: "Script path (e.g. res://player.gd)", required: true },
},
},
create: {
description: "Create a new script file (.gd or .cs only)",
method: "create_script",
args: {
path: { description: "Script path", required: true },
content: { description: "Script content", required: true },
base_type: { description: "Base class (default: Node)" },
force: { description: "Override open-script-editor guard" },
},
mapArgs: (p) => {
const r = { path: p.path, content: p.content };
if (p.base_type)
r.base_type = p.base_type;
if (p.force !== undefined)
r.force = p.force === "true";
return r;
},
},
edit: {
description: "Edit an existing script (full replace or 1-based inclusive line range)",
method: "edit_script",
args: {
path: { description: "Script path", required: true },
content: { description: "New content", required: true },
start_line: { description: "Start line for partial edit (1-based inclusive)", type: "number" },
end_line: { description: "End line for partial edit (1-based inclusive)", type: "number" },
force: { description: "Override open-script-editor guard" },
},
mapArgs: (p) => {
const r = { path: p.path, content: p.content };
if (p.start_line)
r.start_line = parseInt(p.start_line);
if (p.end_line)
r.end_line = parseInt(p.end_line);
if (p.force !== undefined)
r.force = p.force === "true";
return r;
},
},
attach: {
description: "Attach a script to a node",
method: "attach_script",
args: {
node_path: { description: "Node path", required: true },
script_path: { description: "Script path", required: true },
},
},
validate: {
description: "Validate a GDScript for errors",
method: "validate_script",
args: {
path: { description: "Script path to validate", required: true },
},
},
list: {
description: "List all scripts in the project",
method: "list_scripts",
},
},
},
editor: {
description: "Editor state, errors, screenshots, and utilities",
commands: {
errors: {
description: "Get current editor errors/warnings",
method: "get_editor_errors",
},
log: {
description: "Get editor output log",
method: "get_output_log",
args: {
lines: { description: "Number of lines (default: 50)", type: "number" },
},
mapArgs: (p) => (p.lines ? { lines: parseInt(p.lines) } : {}),
},
screenshot: {
description: "Take a screenshot of the running game",
method: "get_game_screenshot",
},
"editor-screenshot": {
description: "Take a screenshot of the editor",
method: "get_editor_screenshot",
},
exec: {
description: "Execute an editor script (GDScript in editor context)",
method: "execute_editor_script",
args: {
code: { description: "GDScript code to execute", required: true },
},
},
signals: {
description: "Get signals of a node type",
method: "get_signals",
args: {
node_path: { description: "Node path", required: true },
},
},
reload: {
description: "Reload the project",
method: "reload_project",
},
},
},
input: {
description: "Simulate keyboard, mouse, and input actions",
commands: {
key: {
description: "Simulate a key press in the running game",
method: "simulate_key",
args: {
key: { description: "Key name (e.g. W, A, S, D, Space)", required: true },
duration: { description: "Hold duration in seconds", type: "number" },
pressed: { description: "true=press, false=release" },
},
mapArgs: (p) => {
const r = { key: p.key };
if (p.duration)
r.duration = parseFloat(p.duration);
if (p.pressed !== undefined)
r.pressed = p.pressed === "true";
return r;
},
},
click: {
description: "Simulate a mouse click in the running game",
method: "simulate_mouse_click",
args: {
x: { description: "X coordinate", required: true, type: "number" },
y: { description: "Y coordinate", required: true, type: "number" },
button: { description: "Mouse button: left, right, or middle (default: left)" },
},
mapArgs: (p) => {
const buttonMap = { left: 1, right: 2, middle: 3 };
const r = { x: parseInt(p.x), y: parseInt(p.y) };
if (p.button)
r.button = buttonMap[p.button.toLowerCase()] ?? (parseInt(p.button) || 1);
return r;
},
},
action: {
description: "Simulate an input action (as defined in Input Map)",
method: "simulate_action",
args: {
action: { description: "Action name (e.g. ui_accept, move_left)", required: true },
pressed: { description: "true=press, false=release" },
duration: { description: "Hold duration in seconds", type: "number" },
},
mapArgs: (p) => {
const r = { action: p.action };
if (p.pressed !== undefined)
r.pressed = p.pressed === "true";
if (p.duration)
r.duration = parseFloat(p.duration);
return r;
},
},
actions: {
description: "List all configured input actions",
method: "get_input_actions",
},
},
},
runtime: {
description: "Inspect and control the running game",
commands: {
tree: {
description: "Get the running game's scene tree",
method: "get_game_scene_tree",
args: {
max_depth: { description: "Max depth (-1 for unlimited)", type: "number" },
},
mapArgs: (p) => {
const r = {};
if (p.max_depth)
r.max_depth = parseInt(p.max_depth);
return r;
},
},
get: {
description: "Get properties of a node in the running game",
method: "get_game_node_properties",
args: {
node_path: { description: "Node path", required: true },
properties: { description: "Comma-separated property names (default: all)" },
},
mapArgs: (p) => {
const r = { node_path: p.node_path };
if (p.properties)
r.properties = p.properties.split(",").map(s => s.trim());
return r;
},
},
set: {
description: "Set a property on a running game node",
method: "set_game_node_property",
args: {
node_path: { description: "Node path", required: true },
property: { description: "Property name", required: true },
value: { description: "Value to set", required: true },
},
mapArgs: (p) => ({ node_path: p.node_path, property: p.property, value: autoType(p.value) }),
},
exec: {
description: "Execute GDScript in the running game",
method: "execute_game_script",
args: {
code: { description: "GDScript code", required: true },
node_path: { description: "Node context (default: /root)" },
},
},
ui: {
description: "Find UI elements (buttons, labels, etc.) in the running game",
method: "find_ui_elements",
args: {
type_filter: { description: "Filter by type (Button, Label, etc.)" },
},
},
},
},
};
// ─── Argument parsing ─────────────────────────────────────────────────
function autoType(value) {
if (value === "true")
return true;
if (value === "false")
return false;
if (value === "null")
return null;
const num = Number(value);
if (!isNaN(num) && value.trim() !== "")
return num;
// Try JSON for arrays/objects
if ((value.startsWith("[") || value.startsWith("{")) && (value.endsWith("]") || value.endsWith("}"))) {
try {
return JSON.parse(value);
}
catch { /* fall through */ }
}
return value;
}
function parseArgs(argv) {
const positional = [];
const flags = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg.startsWith("--")) {
const key = arg.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith("--")) {
flags[key] = next;
i++;
}
else {
flags[key] = "true";
}
}
else {
positional.push(arg);
}
}
return { positional, flags };
}
// ─── Help formatting ──────────────────────────────────────────────────
function showMainHelp() {
console.log(`godot-cli — Control Godot editor from the command line
Usage: godot-cli <group> <command> [options]
Groups:`);
for (const [name, group] of Object.entries(COMMANDS)) {
console.log(` ${name.padEnd(12)} ${group.description}`);
}
console.log(`
Options:
--port <N> Godot WebSocket port (default: auto-detect 6510-6514)
--help Show help for a group or command
Examples:
godot-cli project info
godot-cli scene tree
godot-cli node add --type CharacterBody3D --name Player
godot-cli script read --path res://player.gd
godot-cli scene play
godot-cli input key --key W --duration 0.5`);
}
function showGroupHelp(groupName, group) {
console.log(`godot-cli ${groupName}${group.description}
Commands:`);
for (const [name, cmd] of Object.entries(group.commands)) {
console.log(` ${name.padEnd(18)} ${cmd.description}`);
}
console.log(`\nUse: godot-cli ${groupName} <command> --help for details`);
}
function showCommandHelp(groupName, cmdName, cmd) {
console.log(`godot-cli ${groupName} ${cmdName}${cmd.description}`);
if (cmd.args && Object.keys(cmd.args).length > 0) {
console.log(`\nOptions:`);
for (const [name, arg] of Object.entries(cmd.args)) {
const req = arg.required ? " (required)" : "";
console.log(` --${name.padEnd(16)} ${arg.description}${req}`);
}
}
}
// ─── WebSocket connection ─────────────────────────────────────────────
// The Godot plugin is a WebSocket CLIENT that connects to servers on ports 6505-6514.
// The CLI starts a temporary WebSocket SERVER on an available port and waits for
// the Godot plugin to connect (it polls every 3 seconds).
function isPortFree(port) {
return new Promise((resolve) => {
const server = createServer();
server.once("error", () => resolve(false));
server.once("listening", () => {
server.close(() => resolve(true));
});
server.listen(port, "127.0.0.1");
});
}
async function findFreePort(preferredPort) {
if (preferredPort) {
if (await isPortFree(preferredPort))
return preferredPort;
return null;
}
for (let p = BASE_PORT; p <= MAX_PORT; p++) {
if (await isPortFree(p))
return p;
}
return null;
}
/**
* Start a WebSocket server and wait for the Godot plugin to connect.
* Returns the connected client WebSocket and the server (for cleanup).
*/
function waitForGodot(port) {
return new Promise((resolve, reject) => {
const wss = new WebSocketServer({ port, host: "127.0.0.1" });
const timeout = setTimeout(() => {
wss.close();
reject(new Error(`Godot plugin did not connect within ${CONNECT_TIMEOUT_MS / 1000}s.\n` +
"Make sure the Godot editor is running with the MCP plugin enabled."));
}, CONNECT_TIMEOUT_MS);
wss.on("error", (err) => {
clearTimeout(timeout);
reject(err);
});
wss.on("connection", (ws) => {
clearTimeout(timeout);
resolve({ client: ws, wss });
});
});
}
function sendCommand(ws, method, params) {
return new Promise((resolve, reject) => {
const id = randomUUID();
const timeout = setTimeout(() => {
reject(new Error(`Command '${method}' timed out after ${COMMAND_TIMEOUT_MS}ms`));
}, COMMAND_TIMEOUT_MS);
const handler = (data) => {
let msg;
try {
msg = JSON.parse(data.toString());
}
catch {
return;
}
// Ignore ping/pong
if (msg.method === "pong" || msg.method === "ping")
return;
if (msg.id !== id)
return;
clearTimeout(timeout);
ws.off("message", handler);
if (msg.error) {
reject(new Error(`Godot error: ${msg.error.message || JSON.stringify(msg.error)}`));
}
else {
resolve(msg.result);
}
};
ws.on("message", handler);
ws.send(JSON.stringify({ jsonrpc: "2.0", method, params, id }));
});
}
// ─── Main ─────────────────────────────────────────────────────────────
async function main() {
const userArgs = process.argv.slice(2);
const { positional, flags } = parseArgs(userArgs);
// Global --help
if (positional.length === 0 || flags.help === "true" && positional.length === 0) {
showMainHelp();
process.exit(0);
}
const groupName = positional[0];
const group = COMMANDS[groupName];
if (!group) {
console.error(`Unknown group: ${groupName}`);
showMainHelp();
process.exit(1);
}
// Group-level --help
if (positional.length === 1 || (flags.help === "true" && positional.length === 1)) {
showGroupHelp(groupName, group);
process.exit(0);
}
const cmdName = positional[1];
const cmd = group.commands[cmdName];
if (!cmd) {
console.error(`Unknown command: ${groupName} ${cmdName}`);
showGroupHelp(groupName, group);
process.exit(1);
}
// Command-level --help
if (flags.help === "true") {
showCommandHelp(groupName, cmdName, cmd);
process.exit(0);
}
// Validate required args
if (cmd.args) {
for (const [name, arg] of Object.entries(cmd.args)) {
if (arg.required && !flags[name]) {
console.error(`Missing required option: --${name}`);
showCommandHelp(groupName, cmdName, cmd);
process.exit(1);
}
}
}
// Build params
const params = cmd.mapArgs ? cmd.mapArgs(flags) : { ...flags };
// Remove internal flags
delete params.port;
delete params.help;
// Connect and execute
const preferredPort = flags.port ? parseInt(flags.port) : undefined;
const port = await findFreePort(preferredPort);
if (!port) {
console.error(`No free ports in range ${BASE_PORT}-${MAX_PORT}.\n` +
"All ports are occupied by MCP server instances.");
process.exit(1);
}
let client;
let wss;
try {
process.stderr.write(`Waiting for Godot on port ${port}...`);
({ client, wss } = await waitForGodot(port));
process.stderr.write(" connected!\n");
}
catch (err) {
process.stderr.write("\n");
console.error(err.message);
process.exit(1);
}
try {
const result = await sendCommand(client, cmd.method, params);
if (result !== undefined && result !== null) {
console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
}
}
catch (err) {
console.error(err.message);
process.exit(1);
}
finally {
client.close();
wss.close();
}
}
main().catch((err) => {
console.error("Fatal:", err.message);
process.exit(1);
});
//# sourceMappingURL=cli.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
export declare class GodotConnection {
private wss;
private client;
private port;
private fixedPort;
private basePort;
private maxPort;
private pendingRequests;
private heartbeatTimer;
private lastPongAt;
constructor(port?: number, fixedPort?: boolean, options?: {
basePort?: number;
maxPort?: number;
});
/** Start WebSocket server, retrying on the next port if the first bind races. */
connect(): Promise<void>;
/** Try to bind a single WebSocketServer. Resolves once 'listening' fires, rejects on bind error. */
private bindWebSocketServer;
private attachConnectionHandler;
disconnect(): void;
isConnected(): boolean;
getPort(): number;
sendCommand(method: string, params?: Record<string, unknown>): Promise<unknown>;
private handleMessage;
private rejectAllPending;
private startHeartbeat;
private stopHeartbeat;
}
//# sourceMappingURL=godot-connection.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"godot-connection.d.ts","sourceRoot":"","sources":["../src/godot-connection.ts"],"names":[],"mappings":"AAoBA,qBAAa,eAAe;IAC1B,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,SAAS,CAAU;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,eAAe,CAA0C;IACjE,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,UAAU,CAAa;gBAG7B,IAAI,GAAE,MAAkB,EACxB,SAAS,GAAE,OAAe,EAC1B,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO;IAQvD,iFAAiF;IAC3E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA2C9B,oGAAoG;IACpG,OAAO,CAAC,mBAAmB;IAwB3B,OAAO,CAAC,uBAAuB;IAsC/B,UAAU,IAAI,IAAI;IAalB,WAAW,IAAI,OAAO;IAItB,OAAO,IAAI,MAAM;IAIX,WAAW,CACf,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,OAAO,CAAC;IA8BnB,OAAO,CAAC,aAAa;IA6CrB,OAAO,CAAC,gBAAgB;IAQxB,OAAO,CAAC,cAAc;IA4BtB,OAAO,CAAC,aAAa;CAMtB"}

View File

@@ -0,0 +1,226 @@
import { WebSocketServer, WebSocket } from "ws";
import { randomUUID } from "crypto";
import { GodotConnectionError, GodotCommandError, TimeoutError, } from "./utils/errors.js";
const BASE_PORT = 6505;
const MAX_PORT = 6509;
const COMMAND_TIMEOUT_MS = 30000;
const HEARTBEAT_INTERVAL_MS = 10000;
const HEARTBEAT_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 3;
const TCP_KEEPALIVE_DELAY_MS = 5000;
export class GodotConnection {
wss = null;
client = null;
port;
fixedPort;
basePort;
maxPort;
pendingRequests = new Map();
heartbeatTimer = null;
lastPongAt = 0;
constructor(port = BASE_PORT, fixedPort = false, options = {}) {
this.port = port;
this.fixedPort = fixedPort;
this.basePort = options.basePort ?? BASE_PORT;
this.maxPort = options.maxPort ?? MAX_PORT;
}
/** Start WebSocket server, retrying on the next port if the first bind races. */
async connect() {
if (this.wss)
return;
const candidates = this.fixedPort
? [this.port]
: Array.from({ length: this.maxPort - this.basePort + 1 }, (_, i) => this.basePort + i);
let lastError = null;
for (const port of candidates) {
try {
const wss = await this.bindWebSocketServer(port);
this.wss = wss;
this.port = port;
this.attachConnectionHandler(wss);
console.error(`[MCP] WebSocket server listening on ws://127.0.0.1:${port}`);
return;
}
catch (err) {
lastError = err;
// EADDRINUSE means another MCP server (likely a parallel Claude session)
// won the bind race. Silently try the next port. Other errors are
// logged so we don't swallow real config problems.
if (err.code !== "EADDRINUSE") {
console.error(`[MCP] Bind failed on port ${port}: ${err.message}`);
}
}
}
const range = this.fixedPort
? String(this.port)
: `${this.basePort}-${this.maxPort}`;
const hint = this.fixedPort
? "Try removing GODOT_MCP_PORT from your client config to enable auto-scanning, or kill the process holding the port."
: "All ports are occupied — likely too many parallel Claude Code sessions or stale node MCP processes.";
throw new GodotConnectionError(`Failed to bind WebSocket server on port range ${range}. ` +
`Last error: ${lastError?.message ?? "unknown"}. ${hint}`);
}
/** Try to bind a single WebSocketServer. Resolves once 'listening' fires, rejects on bind error. */
bindWebSocketServer(port) {
return new Promise((resolve, reject) => {
const wss = new WebSocketServer({ port, host: "127.0.0.1" });
const onError = (err) => {
wss.off("listening", onListening);
wss.close();
reject(err);
};
const onListening = () => {
wss.off("error", onError);
// Re-attach a runtime error handler now that the server is live.
// Pre-bind errors fail the connect attempt; post-bind errors are logged.
wss.on("error", (err) => {
console.error("[MCP] WebSocket server error:", err.message);
});
resolve(wss);
};
wss.once("error", onError);
wss.once("listening", onListening);
});
}
attachConnectionHandler(wss) {
wss.on("connection", (ws) => {
console.error("[MCP] Godot editor connected");
// Enable OS-level TCP keepalive so half-open sockets surface faster
// than the Windows default (~2 hours). Application-level heartbeat
// below is still the primary detection mechanism.
const sock = ws._socket;
sock?.setKeepAlive?.(true, TCP_KEEPALIVE_DELAY_MS);
if (this.client) {
this.client.close(1000, "Replaced by new connection");
}
this.client = ws;
this.lastPongAt = Date.now();
this.startHeartbeat();
ws.on("message", (data) => {
this.handleMessage(data.toString());
});
ws.on("close", () => {
console.error("[MCP] Godot editor disconnected");
if (this.client === ws) {
this.client = null;
this.stopHeartbeat();
this.rejectAllPending(new GodotConnectionError("Godot disconnected"));
}
});
ws.on("error", (err) => {
console.error("[MCP] WebSocket error:", err.message);
});
});
}
disconnect() {
this.stopHeartbeat();
if (this.client) {
this.client.close(1000, "Server shutting down");
this.client = null;
}
if (this.wss) {
this.wss.close();
this.wss = null;
}
this.rejectAllPending(new GodotConnectionError("Server shut down"));
}
isConnected() {
return this.client?.readyState === WebSocket.OPEN;
}
getPort() {
return this.port;
}
async sendCommand(method, params = {}) {
if (!this.isConnected()) {
throw new GodotConnectionError("Godot editor is not connected. Make sure the Godot MCP Pro plugin is enabled and the editor is running.");
}
const id = randomUUID();
const request = {
jsonrpc: "2.0",
method,
params,
id,
};
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new TimeoutError(method, COMMAND_TIMEOUT_MS));
}, COMMAND_TIMEOUT_MS);
this.pendingRequests.set(id, {
resolve: resolve,
reject,
timer,
});
this.client.send(JSON.stringify(request));
});
}
handleMessage(data) {
let msg;
try {
msg = JSON.parse(data);
}
catch {
console.error("[MCP] Failed to parse message from Godot:", data);
return;
}
const method = msg.method;
if (method === "pong") {
this.lastPongAt = Date.now();
return;
}
// Godot may also send unsolicited pings — reply so its inactivity timer resets
if (method === "ping") {
this.lastPongAt = Date.now();
if (this.isConnected()) {
this.client.send(JSON.stringify({ jsonrpc: "2.0", method: "pong", params: {} }));
}
return;
}
if (!msg.id)
return;
const pending = this.pendingRequests.get(msg.id);
if (!pending)
return;
clearTimeout(pending.timer);
this.pendingRequests.delete(msg.id);
if (msg.error) {
pending.reject(new GodotCommandError(msg.error.code, msg.error.message, msg.error.data));
}
else {
pending.resolve(msg.result);
}
}
rejectAllPending(error) {
for (const [, pending] of this.pendingRequests) {
clearTimeout(pending.timer);
pending.reject(error);
}
this.pendingRequests.clear();
}
startHeartbeat() {
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
if (!this.isConnected())
return;
// If Godot has been silent for too long, the socket is likely half-open.
// terminate() forcibly destroys the TCP socket (vs close() which waits
// for a FIN ack that will never arrive on a dead link).
if (Date.now() - this.lastPongAt > HEARTBEAT_TIMEOUT_MS) {
console.error(`[MCP] Heartbeat timeout (no pong for ${HEARTBEAT_TIMEOUT_MS}ms) — terminating dead connection`);
const dead = this.client;
this.client = null;
this.stopHeartbeat();
this.rejectAllPending(new GodotConnectionError("Heartbeat timeout — Godot connection lost"));
dead?.terminate();
return;
}
this.client.send(JSON.stringify({ jsonrpc: "2.0", method: "ping", params: {} }));
}, HEARTBEAT_INTERVAL_MS);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
}
//# sourceMappingURL=godot-connection.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
export {};
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,138 @@
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { GodotConnection } from "./godot-connection.js";
import { registerProjectTools } from "./tools/project-tools.js";
import { registerSceneTools } from "./tools/scene-tools.js";
import { registerNodeTools } from "./tools/node-tools.js";
import { registerScriptTools } from "./tools/script-tools.js";
import { registerEditorTools } from "./tools/editor-tools.js";
import { registerInputTools } from "./tools/input-tools.js";
import { registerRuntimeTools } from "./tools/runtime-tools.js";
import { registerAnimationTools } from "./tools/animation-tools.js";
import { registerTilemapTools } from "./tools/tilemap-tools.js";
import { registerThemeTools } from "./tools/theme-tools.js";
import { registerProfilingTools } from "./tools/profiling-tools.js";
import { registerBatchTools } from "./tools/batch-tools.js";
import { registerShaderTools } from "./tools/shader-tools.js";
import { registerExportTools } from "./tools/export-tools.js";
import { registerResourceTools } from "./tools/resource-tools.js";
import { registerAnimationTreeTools } from "./tools/animation-tree-tools.js";
import { registerPhysicsTools } from "./tools/physics-tools.js";
import { registerScene3DTools } from "./tools/scene-3d-tools.js";
import { registerParticleTools } from "./tools/particle-tools.js";
import { registerNavigationTools } from "./tools/navigation-tools.js";
import { registerAudioTools } from "./tools/audio-tools.js";
import { registerTestTools } from "./tools/test-tools.js";
import { registerAnalysisTools } from "./tools/analysis-tools.js";
import { registerInputMapTools } from "./tools/input-map-tools.js";
import { registerAndroidTools } from "./tools/android-tools.js";
import { MINIMAL_TOOLS, createFilteredServer } from "./utils/tool-filter.js";
import { loadInstructions } from "./utils/load-instructions.js";
const MINIMAL_MODE = process.argv.includes("--minimal");
const THREED_MODE = process.argv.includes("--3d");
const LITE_MODE = process.argv.includes("--lite") || MINIMAL_MODE || THREED_MODE;
const HTTP_MODE = process.argv.includes("--http");
const HTTP_PORT = parseInt(process.argv.find((_, i, a) => a[i - 1] === "--http-port") ||
process.env.GODOT_MCP_HTTP_PORT ||
"8001");
const explicitPort = process.env.GODOT_MCP_PORT;
const godot = new GodotConnection(parseInt(explicitPort || "6505"), !!explicitPort);
const serverName = MINIMAL_MODE
? "godot-mcp-pro-minimal"
: THREED_MODE
? "godot-mcp-pro-3d"
: LITE_MODE
? "godot-mcp-pro-lite"
: "godot-mcp-pro";
const server = new McpServer({
name: serverName,
version: "1.14.1",
}, {
instructions: loadInstructions(),
});
// In minimal mode, wrap the server to filter tool registrations
const toolServer = MINIMAL_MODE ? createFilteredServer(server, MINIMAL_TOOLS) : server;
// Core tools (always registered)
registerProjectTools(toolServer, godot);
registerSceneTools(toolServer, godot);
registerNodeTools(toolServer, godot);
registerScriptTools(toolServer, godot);
registerEditorTools(toolServer, godot);
registerInputTools(toolServer, godot);
registerRuntimeTools(toolServer, godot);
registerInputMapTools(toolServer, godot);
// 3D-critical tools (registered in FULL and --3d modes)
// Core (81) + Physics (6) + AnimationTree (8) + Navigation (5) = exactly 100 tools
if (!LITE_MODE || THREED_MODE) {
registerPhysicsTools(server, godot);
registerAnimationTreeTools(server, godot);
registerNavigationTools(server, godot);
}
// Extended tools (Full mode only)
if (!LITE_MODE) {
registerAnimationTools(server, godot);
registerAudioTools(server, godot);
registerBatchTools(server, godot);
registerExportTools(server, godot);
registerParticleTools(server, godot);
registerProfilingTools(server, godot);
registerResourceTools(server, godot);
registerScene3DTools(server, godot);
registerShaderTools(server, godot);
registerTestTools(server, godot);
registerThemeTools(server, godot);
registerTilemapTools(server, godot);
registerAnalysisTools(server, godot);
registerAndroidTools(server, godot);
}
// Start server
async function main() {
// Attempt initial connection to Godot (non-blocking).
// If this fails (all ports occupied, etc.), tool calls will fail with a
// clear error message from sendCommand until the user restarts the server.
godot.connect().catch((err) => {
console.error(`[MCP] Failed to start WebSocket server: ${err.message}`);
});
if (HTTP_MODE) {
// Streamable HTTP transport — clients connect via http://host:port/mcp
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
await server.connect(transport);
const httpServer = createServer(async (req, res) => {
const url = new URL(req.url || "/", `http://${req.headers.host}`);
if (url.pathname === "/mcp") {
await transport.handleRequest(req, res);
}
else {
res.writeHead(404).end("Not Found");
}
});
httpServer.listen(HTTP_PORT, () => {
const mode = MINIMAL_MODE ? "MINIMAL " : THREED_MODE ? "3D " : LITE_MODE ? "LITE " : "";
console.error(`[MCP] Godot MCP Pro ${mode}started (HTTP transport on http://127.0.0.1:${HTTP_PORT}/mcp)`);
});
}
else {
// Default stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
const modeLabel = MINIMAL_MODE
? "[MCP] Godot MCP Pro MINIMAL started (35 tools, stdio transport)"
: THREED_MODE
? "[MCP] Godot MCP Pro 3D started (100 tools, stdio transport)"
: LITE_MODE
? "[MCP] Godot MCP Pro LITE started (81 tools, stdio transport)"
: "[MCP] Godot MCP Pro started (stdio transport)";
console.error(modeLabel);
}
}
main().catch((err) => {
console.error("[MCP] Fatal error:", err);
process.exit(1);
});
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env node
/**
* godot-mcp-setup — Setup and management CLI for Godot MCP Pro
*
* Commands:
* install Install dependencies and build the server
* check-update Check if a newer version is available on GitHub
* configure Auto-detect AI client and generate MCP config
* doctor Diagnose environment (Node.js, npm, build status)
*/
export {};
//# sourceMappingURL=setup.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":";AAEA;;;;;;;;GAQG"}

View File

@@ -0,0 +1,273 @@
#!/usr/bin/env node
/**
* godot-mcp-setup — Setup and management CLI for Godot MCP Pro
*
* Commands:
* install Install dependencies and build the server
* check-update Check if a newer version is available on GitHub
* configure Auto-detect AI client and generate MCP config
* doctor Diagnose environment (Node.js, npm, build status)
*/
import { execSync } from "child_process";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
import { resolve, dirname, join } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Server root is one level up from build/
const SERVER_DIR = resolve(__dirname, "..");
const PACKAGE_JSON = join(SERVER_DIR, "package.json");
const BUILD_INDEX = join(SERVER_DIR, "build", "index.js");
const GITHUB_REPO = "youichi-uda/godot-mcp-pro";
// ─── Utilities ────────────────────────────────────────────────
function getVersion() {
try {
const pkg = JSON.parse(readFileSync(PACKAGE_JSON, "utf-8"));
return pkg.version || "unknown";
}
catch {
return "unknown";
}
}
function run(cmd, cwd) {
try {
return execSync(cmd, {
cwd: cwd || SERVER_DIR,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}).trim();
}
catch (err) {
return err.stderr?.trim() || err.message || "command failed";
}
}
function check(label, ok, detail) {
const icon = ok ? "✓" : "✗";
const line = detail ? `${label}: ${detail}` : label;
console.log(` ${icon} ${line}`);
}
/** Compare semver strings. Returns >0 if a > b, <0 if a < b, 0 if equal. */
function compareSemver(a, b) {
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
for (let i = 0; i < 3; i++) {
const diff = (pa[i] || 0) - (pb[i] || 0);
if (diff !== 0)
return diff;
}
return 0;
}
// ─── Commands ─────────────────────────────────────────────────
async function cmdInstall() {
console.log("Installing Godot MCP Pro server...\n");
console.log("[1/2] Installing dependencies...");
try {
execSync("npm install", { cwd: SERVER_DIR, stdio: "inherit" });
}
catch {
console.error("\nFailed to install dependencies. Make sure npm is available.");
process.exit(1);
}
console.log("\n[2/2] Building server...");
try {
execSync("npm run build", { cwd: SERVER_DIR, stdio: "inherit" });
}
catch {
console.error("\nBuild failed. Check for TypeScript errors above.");
process.exit(1);
}
console.log(`\nDone! Server built at: ${BUILD_INDEX}`);
console.log(`Version: ${getVersion()}`);
console.log("\nNext step: Run 'node build/setup.js configure' to set up your AI client.");
}
async function cmdCheckUpdate() {
const current = getVersion();
console.log(`Current version: ${current}\n`);
console.log(`Checking GitHub releases for ${GITHUB_REPO}...`);
try {
const res = await fetch(`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`, { headers: { "User-Agent": "godot-mcp-pro-setup" } });
if (!res.ok) {
if (res.status === 404) {
console.log("No releases found on GitHub.");
return;
}
console.error(`GitHub API error: ${res.status} ${res.statusText}`);
return;
}
const data = (await res.json());
const latest = data.tag_name.replace(/^v/, "");
if (compareSemver(latest, current) > 0) {
console.log(`\nUpdate available: v${latest} (current: v${current})`);
console.log(`Download: ${data.html_url}`);
console.log("\nTo update: download the new version, replace server/src/, and run 'node build/setup.js install'");
}
else {
console.log(`\nUp to date! (${current})`);
}
}
catch (err) {
console.error(`Failed to check for updates: ${err.message}`);
}
}
async function cmdConfigure() {
const serverPath = resolve(BUILD_INDEX).replace(/\\/g, "/");
if (!existsSync(BUILD_INDEX)) {
console.error("Server not built yet. Run 'node build/setup.js install' first.");
process.exit(1);
}
console.log("Detecting AI clients...\n");
// Detect available clients by checking config file locations
const home = process.env.HOME || process.env.USERPROFILE || "";
const cwd = process.cwd();
const candidates = [
{
name: "Claude Code (project)",
configPath: join(cwd, ".mcp.json"),
configKey: "godot-mcp-pro",
},
{
name: "Cursor (project)",
configPath: join(cwd, ".cursor", "mcp.json"),
configKey: "godot-mcp-pro",
},
{
name: "Windsurf (project)",
configPath: join(cwd, ".windsurf", "mcp.json"),
configKey: "godot-mcp-pro",
},
{
name: "Claude Desktop",
configPath: join(home, process.platform === "win32"
? "AppData/Roaming/Claude/claude_desktop_config.json"
: process.platform === "darwin"
? "Library/Application Support/Claude/claude_desktop_config.json"
: ".config/claude/claude_desktop_config.json"),
configKey: "godot-mcp-pro",
},
];
// Find existing configs
const existing = candidates.filter((c) => existsSync(c.configPath));
const missing = candidates.filter((c) => !existsSync(c.configPath));
if (existing.length > 0) {
console.log("Found existing configs:");
for (const c of existing) {
console.log(`${c.name}: ${c.configPath}`);
}
}
// Default: create .mcp.json in cwd (Claude Code)
const target = candidates[0]; // Claude Code project-level
// No GODOT_MCP_PORT env: lets the server auto-scan 6505-6509 so multiple
// Claude Code sessions can each grab a free port. Pinning a single port
// here would force every session to collide on 6505.
const entry = {
command: "node",
args: [serverPath],
};
let config;
if (existsSync(target.configPath)) {
try {
config = JSON.parse(readFileSync(target.configPath, "utf-8"));
if (!config.mcpServers)
config.mcpServers = {};
}
catch {
config = { mcpServers: {} };
}
}
else {
config = { mcpServers: {} };
}
if (config.mcpServers[target.configKey]) {
console.log(`\n${target.name} already configured in ${target.configPath}`);
console.log("Updating server path...");
}
config.mcpServers[target.configKey] = entry;
const dir = dirname(target.configPath);
if (!existsSync(dir))
mkdirSync(dir, { recursive: true });
writeFileSync(target.configPath, JSON.stringify(config, null, 2) + "\n");
console.log(`\nWrote config to: ${target.configPath}`);
console.log(`Server path: ${serverPath}`);
console.log("\nYou're all set! Start your AI assistant to begin using Godot MCP Pro.");
}
function cmdDoctor() {
console.log("Godot MCP Pro — Environment Check\n");
// Node.js
const nodeVer = run("node --version");
const nodeOk = nodeVer.startsWith("v") && parseInt(nodeVer.slice(1)) >= 18;
check("Node.js", nodeOk, nodeVer);
// npm
const npmVer = run("npm --version");
const npmOk = !npmVer.includes("not found") && !npmVer.includes("failed");
check("npm", npmOk, npmVer);
// Dependencies installed
const nodeModules = existsSync(join(SERVER_DIR, "node_modules"));
check("Dependencies installed", nodeModules);
// Server built
const built = existsSync(BUILD_INDEX);
check("Server built", built, built ? BUILD_INDEX : "run 'node build/setup.js install'");
// Version
console.log(`\n Version: ${getVersion()}`);
// Overall
const allOk = nodeOk && npmOk && nodeModules && built;
console.log(allOk ? "\nAll good!" : "\nSome issues found. Fix them above.");
if (!allOk)
process.exit(1);
}
// ─── Main ─────────────────────────────────────────────────────
function showHelp() {
console.log(`godot-mcp-setup — Setup and management for Godot MCP Pro
Usage: node build/setup.js <command>
Commands:
install Install dependencies and build the server
check-update Check if a newer version is available on GitHub
configure Auto-detect AI client and generate .mcp.json config
doctor Check Node.js, npm, and build status
Options:
--help Show this help
--version Show current version
Examples:
node build/setup.js install
node build/setup.js doctor
node build/setup.js configure
node build/setup.js check-update`);
}
async function main() {
const args = process.argv.slice(2);
const cmd = args[0];
if (!cmd || cmd === "--help" || cmd === "-h") {
showHelp();
process.exit(0);
}
if (cmd === "--version" || cmd === "-v") {
console.log(getVersion());
process.exit(0);
}
switch (cmd) {
case "install":
await cmdInstall();
break;
case "check-update":
await cmdCheckUpdate();
break;
case "configure":
await cmdConfigure();
break;
case "doctor":
cmdDoctor();
break;
default:
console.error(`Unknown command: ${cmd}`);
showHelp();
process.exit(1);
}
}
main().catch((err) => {
console.error("Fatal:", err.message);
process.exit(1);
});
//# sourceMappingURL=setup.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerAnalysisTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=analysis-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"analysis-tools.d.ts","sourceRoot":"","sources":["../../src/tools/analysis-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CAmGN"}

View File

@@ -0,0 +1,74 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerAnalysisTools(server, godot) {
server.tool("find_unused_resources", "Scan the project for resource files (.tres, .tscn, .png, .wav, .ogg, .ttf, .gdshader, etc.) that are not referenced by any .tscn, .gd, or .tres file. Useful for cleaning up unused assets.", {
path: z.string().optional().describe("Root path to scan (default: res://)"),
include_addons: z.boolean().optional().describe("Include addons/ directory in scan (default: false)"),
}, async (params) => {
try {
const result = await godot.sendCommand("find_unused_resources", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("analyze_signal_flow", "Map all signal connections in the currently edited scene. Returns a graph-like structure showing which nodes emit which signals and which nodes receive them.", {}, async () => {
try {
const result = await godot.sendCommand("analyze_signal_flow");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("analyze_scene_complexity", "Analyze a scene's complexity: total node count, max nesting depth, nodes grouped by type, attached scripts, and potential issues (too many nodes, deep nesting).", {
path: z.string().optional().describe("Scene path to analyze (default: currently edited scene)"),
}, async (params) => {
try {
const result = await godot.sendCommand("analyze_scene_complexity", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("find_script_references", "Find all places where a given script path, class_name, or resource path is referenced across the project. Searches .tscn, .gd, and .tres files.", {
query: z.string().describe("The script path, class_name, or resource path to search for (e.g. 'res://scripts/player.gd', 'PlayerController', 'res://assets/icon.png')"),
path: z.string().optional().describe("Root path to search (default: res://)"),
include_addons: z.boolean().optional().describe("Include addons/ directory in search (default: false)"),
}, async (params) => {
try {
const result = await godot.sendCommand("find_script_references", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("detect_circular_dependencies", "Check for circular scene dependencies where Scene A instances Scene B which instances Scene A (directly or indirectly). Walks all .tscn files and builds a dependency graph.", {
path: z.string().optional().describe("Root path to scan (default: res://)"),
include_addons: z.boolean().optional().describe("Include addons/ directory in scan (default: false)"),
}, async (params) => {
try {
const result = await godot.sendCommand("detect_circular_dependencies", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_project_statistics", "Get overall project statistics: file counts by extension, total script lines, scene count, resource count, autoload list, and enabled plugins.", {
path: z.string().optional().describe("Root path to scan (default: res://)"),
include_addons: z.boolean().optional().describe("Include addons/ directory in statistics (default: false)"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_project_statistics", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=analysis-tools.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"analysis-tools.js","sourceRoot":"","sources":["../../src/tools/analysis-tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,UAAU,qBAAqB,CACnC,MAAiB,EACjB,KAAsB;IAEtB,MAAM,CAAC,IAAI,CACT,uBAAuB,EACvB,6LAA6L,EAC7L;QACE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;QAC3E,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oDAAoD,CAAC;KACtG,EACD,KAAK,EAAE,MAAM,EAAE,EAAE;QACf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAC;YACxE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,qBAAqB,EACrB,+JAA+J,EAC/J,EAAE,EACF,KAAK,IAAI,EAAE;QACT,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,qBAAqB,CAAC,CAAC;YAC9D,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,0BAA0B,EAC1B,kKAAkK,EAClK;QACE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yDAAyD,CAAC;KAChG,EACD,KAAK,EAAE,MAAM,EAAE,EAAE;QACf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,0BAA0B,EAAE,MAAM,CAAC,CAAC;YAC3E,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,wBAAwB,EACxB,iJAAiJ,EACjJ;QACE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,2IAA2I,CAAC;QACvK,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,uCAAuC,CAAC;QAC7E,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,sDAAsD,CAAC;KACxG,EACD,KAAK,EAAE,MAAM,EAAE,EAAE;QACf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,wBAAwB,EAAE,MAAM,CAAC,CAAC;YACzE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,8BAA8B,EAC9B,8KAA8K,EAC9K;QACE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;QAC3E,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oDAAoD,CAAC;KACtG,EACD,KAAK,EAAE,MAAM,EAAE,EAAE;QACf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,8BAA8B,EAAE,MAAM,CAAC,CAAC;YAC/E,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,wBAAwB,EACxB,gJAAgJ,EAChJ;QACE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;QAC3E,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0DAA0D,CAAC;KAC5G,EACD,KAAK,EAAE,MAAM,EAAE,EAAE;QACf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,wBAAwB,EAAE,MAAM,CAAC,CAAC;YACzE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerAndroidTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=android-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"android-tools.d.ts","sourceRoot":"","sources":["../../src/tools/android-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CAoDN"}

View File

@@ -0,0 +1,42 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerAndroidTools(server, godot) {
server.tool("list_android_devices", "List Android devices visible to adb (parses 'adb devices -l'). Uses the path configured in Editor Settings > Export > Android > Adb, falls back to 'adb' on PATH.", {}, async () => {
try {
const result = await godot.sendCommand("list_android_devices");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_android_preset_info", "Read metadata (package name, export path, runnable flag) from an Android export preset in export_presets.cfg. If no preset is specified, returns the first Android preset.", {
preset_name: z.string().optional().describe("Preset name as shown in Project > Export"),
preset_index: z.number().optional().describe("Preset index (alternative to name)"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_android_preset_info", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("deploy_to_android", "Export APK via Godot CLI, install it on a connected Android device via adb, and optionally launch the main activity. Equivalent to Godot's Remote Deploy button. Requires a configured Android export preset and adb on PATH (or set in Editor Settings). This call is synchronous and may take tens of seconds to complete.", {
preset_name: z.string().optional().describe("Android export preset name (defaults to first Android preset)"),
preset_index: z.number().optional().describe("Preset index (alternative to name)"),
device_serial: z.string().optional().describe("adb device serial (omit to use default device)"),
debug: z.boolean().optional().describe("Debug export (default: true)"),
launch: z.boolean().optional().describe("Launch the app after install (default: true)"),
skip_export: z.boolean().optional().describe("Skip the export step and install the existing APK at the preset's export_path (default: false)"),
}, async (params) => {
try {
const result = await godot.sendCommand("deploy_to_android", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=android-tools.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"android-tools.js","sourceRoot":"","sources":["../../src/tools/android-tools.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerAnimationTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=animation-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"animation-tools.d.ts","sourceRoot":"","sources":["../../src/tools/animation-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA8GN"}

View File

@@ -0,0 +1,85 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerAnimationTools(server, godot) {
server.tool("list_animations", "List all animations in an AnimationPlayer node", {
node_path: z.string().describe("Path to the AnimationPlayer node"),
}, async (params) => {
try {
const result = await godot.sendCommand("list_animations", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("create_animation", "Create a new animation in an AnimationPlayer", {
node_path: z.string().describe("Path to the AnimationPlayer node"),
name: z.string().describe("Name for the new animation"),
length: z.number().optional().describe("Animation length in seconds (default: 1.0)"),
loop_mode: z.number().optional().describe("Loop mode: 0=none, 1=linear, 2=pingpong (default: 0)"),
}, async (params) => {
try {
const result = await godot.sendCommand("create_animation", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("add_animation_track", "Add a track to an animation (value, position, rotation, scale, method, bezier)", {
node_path: z.string().describe("Path to the AnimationPlayer node"),
animation: z.string().describe("Animation name"),
track_path: z.string().describe("Node path and property for the track (e.g. 'Sprite2D:position')"),
track_type: z.string().optional().describe("Track type: value, position_2d, rotation_2d, scale_2d, method, bezier, blend_shape (default: value)"),
update_mode: z.string().optional().describe("Update mode for value tracks: continuous, discrete, capture"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_animation_track", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_animation_keyframe", "Insert a keyframe into an animation track", {
node_path: z.string().describe("Path to the AnimationPlayer node"),
animation: z.string().describe("Animation name"),
track_index: z.number().describe("Track index"),
time: z.number().describe("Time position in seconds"),
value: z.union([z.string(), z.number(), z.boolean()]).describe("Keyframe value. Strings auto-parsed for Vector2, Color, etc."),
easing: z.number().optional().describe("Easing/transition value. 1.0=linear, <1.0=ease-in, >1.0=ease-out. Use negative for in-out variants. (default: 1.0)"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_animation_keyframe", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_animation_info", "Get detailed info about an animation including all tracks and keyframes", {
node_path: z.string().describe("Path to the AnimationPlayer node"),
animation: z.string().describe("Animation name"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_animation_info", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("remove_animation", "Remove an animation from an AnimationPlayer", {
node_path: z.string().describe("Path to the AnimationPlayer node"),
name: z.string().describe("Name of the animation to remove"),
}, async (params) => {
try {
const result = await godot.sendCommand("remove_animation", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=animation-tools.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"animation-tools.js","sourceRoot":"","sources":["../../src/tools/animation-tools.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerAnimationTreeTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=animation-tree-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"animation-tree-tools.d.ts","sourceRoot":"","sources":["../../src/tools/animation-tree-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA+JN"}

View File

@@ -0,0 +1,124 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerAnimationTreeTools(server, godot) {
server.tool("create_animation_tree", "Create an AnimationTree node with an AnimationNodeStateMachine as root, optionally linked to an AnimationPlayer", {
node_path: z.string().describe("Path to the parent node where the AnimationTree will be added"),
anim_player: z.string().optional().describe("Relative path from the AnimationTree to the AnimationPlayer (e.g. '../AnimationPlayer')"),
name: z.string().optional().describe("Name for the AnimationTree node (default: 'AnimationTree')"),
}, async (params) => {
try {
const result = await godot.sendCommand("create_animation_tree", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_animation_tree_structure", "Read the full structure of an AnimationTree including all states, transitions, and blend tree nodes", {
node_path: z.string().describe("Path to the AnimationTree node"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_animation_tree_structure", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("add_state_machine_state", "Add a state to an AnimationNodeStateMachine (animation clip, blend tree, or nested state machine)", {
node_path: z.string().describe("Path to the AnimationTree node"),
state_name: z.string().describe("Name for the new state"),
state_type: z.enum(["animation", "blend_tree", "state_machine"]).optional().describe("Type of state: 'animation' (default), 'blend_tree', or 'state_machine'"),
animation: z.string().optional().describe("Animation name to play (only for state_type='animation')"),
state_machine_path: z.string().optional().describe("Slash-separated path to a nested state machine (e.g. 'Run/SubState'). Empty or omit for root."),
position_x: z.number().optional().describe("X position in the graph editor (default: 0)"),
position_y: z.number().optional().describe("Y position in the graph editor (default: 0)"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_state_machine_state", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("remove_state_machine_state", "Remove a state from an AnimationNodeStateMachine (also removes connected transitions)", {
node_path: z.string().describe("Path to the AnimationTree node"),
state_name: z.string().describe("Name of the state to remove"),
state_machine_path: z.string().optional().describe("Slash-separated path to a nested state machine. Empty or omit for root."),
}, async (params) => {
try {
const result = await godot.sendCommand("remove_state_machine_state", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("add_state_machine_transition", "Add a transition between two states in an AnimationNodeStateMachine with configurable switch mode, advance mode, and expression conditions", {
node_path: z.string().describe("Path to the AnimationTree node"),
from_state: z.string().describe("Source state name (use 'Start' for the entry point)"),
to_state: z.string().describe("Destination state name (use 'End' for the exit point)"),
switch_mode: z.enum(["at_end", "immediate", "sync"]).optional().describe("When to switch: 'at_end' (wait for animation), 'immediate' (default), 'sync'"),
advance_mode: z.enum(["disabled", "enabled", "auto"]).optional().describe("How to advance: 'disabled', 'enabled' (default, uses travel), 'auto' (automatic)"),
advance_expression: z.string().optional().describe("GDScript expression that triggers this transition (e.g. 'is_running')"),
xfade_time: z.number().optional().describe("Cross-fade time in seconds"),
state_machine_path: z.string().optional().describe("Slash-separated path to a nested state machine. Empty or omit for root."),
}, async (params) => {
try {
const result = await godot.sendCommand("add_state_machine_transition", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("remove_state_machine_transition", "Remove a transition between two states in an AnimationNodeStateMachine", {
node_path: z.string().describe("Path to the AnimationTree node"),
from_state: z.string().describe("Source state name"),
to_state: z.string().describe("Destination state name"),
state_machine_path: z.string().optional().describe("Slash-separated path to a nested state machine. Empty or omit for root."),
}, async (params) => {
try {
const result = await godot.sendCommand("remove_state_machine_transition", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_blend_tree_node", "Add or replace a node inside an AnimationNodeBlendTree state (Add2, Blend2, TimeScale, Animation, etc.) with optional connection", {
node_path: z.string().describe("Path to the AnimationTree node"),
blend_tree_state: z.string().describe("Name of the BlendTree state in the state machine"),
bt_node_name: z.string().describe("Name for the node inside the BlendTree"),
bt_node_type: z.enum(["Animation", "Add2", "Blend2", "Add3", "Blend3", "TimeScale", "TimeSeek", "Transition", "OneShot", "Sub2"]).describe("Type of BlendTree node to create"),
animation: z.string().optional().describe("Animation name (only for bt_node_type='Animation')"),
connect_to: z.string().optional().describe("Name of another BlendTree node to connect this node's output to"),
connect_port: z.number().optional().describe("Input port index on the target node (default: 0)"),
state_machine_path: z.string().optional().describe("Slash-separated path to a nested state machine. Empty or omit for root."),
position_x: z.number().optional().describe("X position in the graph editor (default: 0)"),
position_y: z.number().optional().describe("Y position in the graph editor (default: 0)"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_blend_tree_node", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_tree_parameter", "Set an AnimationTree parameter value (conditions, blend amounts, time scale, etc.)", {
node_path: z.string().describe("Path to the AnimationTree node"),
parameter: z.string().describe("Parameter path (e.g. 'conditions/is_running', 'Blend2/blend_amount'). 'parameters/' prefix is auto-added if missing."),
value: z.union([z.string(), z.number(), z.boolean()]).describe("Parameter value. Strings are auto-parsed for Vector2, Color, etc."),
}, async (params) => {
try {
const result = await godot.sendCommand("set_tree_parameter", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=animation-tree-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerAudioTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=audio-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"audio-tools.d.ts","sourceRoot":"","sources":["../../src/tools/audio-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CAsHN"}

View File

@@ -0,0 +1,93 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerAudioTools(server, godot) {
server.tool("get_audio_bus_layout", "Get the entire audio bus layout: all buses with volumes, effects, send targets, solo/mute states", {}, async (params) => {
try {
const result = await godot.sendCommand("get_audio_bus_layout", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("add_audio_bus", "Add a new audio bus with name, volume, send target, solo, and mute settings", {
name: z.string().describe("Name for the new audio bus"),
volume_db: z.number().optional().describe("Volume in dB (default: 0)"),
send: z.string().optional().describe("Name of the bus to send output to (e.g. 'Master')"),
solo: z.boolean().optional().describe("Solo this bus (default: false)"),
mute: z.boolean().optional().describe("Mute this bus (default: false)"),
at_position: z.number().optional().describe("Bus index position to insert at (-1 = end)"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_audio_bus", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_audio_bus", "Modify an existing audio bus: volume, solo, mute, bypass_effects, send, or rename", {
name: z.string().describe("Name of the audio bus to modify"),
volume_db: z.number().optional().describe("Volume in dB"),
solo: z.boolean().optional().describe("Solo state"),
mute: z.boolean().optional().describe("Mute state"),
bypass_effects: z.boolean().optional().describe("Bypass all effects on this bus"),
send: z.string().optional().describe("Name of the bus to send output to"),
rename: z.string().optional().describe("New name for the bus"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_audio_bus", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("add_audio_bus_effect", "Add an audio effect to a bus. Types: reverb, chorus, delay, compressor, limiter, phaser, distortion, lowpassfilter, highpassfilter, bandpassfilter, amplify, eq", {
bus: z.string().describe("Name of the audio bus"),
effect_type: z.string().describe("Effect type: reverb, chorus, delay, compressor, limiter, phaser, distortion, lowpassfilter (or lowpass), highpassfilter (or highpass), bandpassfilter (or bandpass), amplify, eq"),
params: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe("Effect-specific parameters. E.g. for reverb: {room_size, damping, wet, dry, spread}; for compressor: {threshold, ratio, attack_us, release_ms}; for filters: {cutoff_hz, resonance}"),
at_position: z.number().optional().describe("Effect index position (-1 = end)"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_audio_bus_effect", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("add_audio_player", "Add an AudioStreamPlayer, AudioStreamPlayer2D, or AudioStreamPlayer3D node to a parent node", {
node_path: z.string().describe("Path to the parent node"),
name: z.string().describe("Name for the new audio player node"),
type: z.string().optional().describe("Player type: AudioStreamPlayer (default), AudioStreamPlayer2D, AudioStreamPlayer3D"),
stream: z.string().optional().describe("Path to audio resource (e.g. 'res://audio/music.ogg')"),
volume_db: z.number().optional().describe("Volume in dB (default: 0)"),
bus: z.string().optional().describe("Audio bus name (default: 'Master')"),
autoplay: z.boolean().optional().describe("Auto-play when scene starts (default: false)"),
max_distance: z.number().optional().describe("Maximum hearing distance (for 2D/3D players)"),
attenuation: z.number().optional().describe("Distance attenuation factor (for 2D players)"),
attenuation_model: z.number().optional().describe("Attenuation model for 3D: 0=inverse_distance, 1=inverse_square, 2=logarithmic"),
unit_size: z.number().optional().describe("Unit size for 3D player volume reference"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_audio_player", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_audio_info", "Get audio setup for a node subtree: finds all AudioStreamPlayer nodes with their settings, streams, and bus assignments", {
node_path: z.string().describe("Path to the root node to search within"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_audio_info", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=audio-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerBatchTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=batch-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"batch-tools.d.ts","sourceRoot":"","sources":["../../src/tools/batch-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA+HN"}

View File

@@ -0,0 +1,97 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerBatchTools(server, godot) {
server.tool("find_nodes_by_type", "Find all nodes of a specific type in the current scene", {
type: z.string().describe("Node type/class to search for (e.g. 'Sprite2D', 'Label', 'CollisionShape2D')"),
recursive: z.boolean().optional().describe("Search recursively through children (default: true)"),
}, async (params) => {
try {
const result = await godot.sendCommand("find_nodes_by_type", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("find_signal_connections", "Find all signal connections in the current scene, optionally filtered by signal name or node", {
signal_name: z.string().optional().describe("Filter by signal name (partial match)"),
node_path: z.string().optional().describe("Filter by node path (partial match)"),
}, async (params) => {
try {
const result = await godot.sendCommand("find_signal_connections", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("batch_set_property", "Set a property on all nodes of a given type in the current scene", {
type: z.string().describe("Node type to target (e.g. 'Label', 'Sprite2D')"),
property: z.string().describe("Property name to set (e.g. 'visible', 'modulate')"),
value: z.union([z.string(), z.number(), z.boolean()]).describe("Value to set. Strings auto-parsed for Vector2, Color, etc."),
}, async (params) => {
try {
const result = await godot.sendCommand("batch_set_property", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("batch_add_nodes", "Add multiple nodes in a single call. Supports building entire node trees at once — nodes added earlier can be referenced as parents by later entries. Much faster than calling add_node repeatedly.", {
nodes: z.array(z.object({
type: z.string().describe("Node type (e.g. 'Sprite2D', 'CharacterBody2D', 'Label')"),
parent_path: z.string().optional().describe("Parent node path (default: root '.'). Can reference nodes created earlier in this batch."),
name: z.string().optional().describe("Node name"),
properties: z.record(z.string(), z.any()).optional().describe("Properties to set (e.g. {\"position\": \"Vector2(100, 200)\"})"),
})).describe("Array of node definitions to add, processed in order"),
}, async (params) => {
try {
const result = await godot.sendCommand("batch_add_nodes", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("find_node_references", "Search through project files (.tscn, .gd, .tres, .gdshader) for a text pattern", {
pattern: z.string().describe("Text pattern to search for"),
}, async (params) => {
try {
const result = await godot.sendCommand("find_node_references", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_scene_dependencies", "Get all resource dependencies of a scene or resource file", {
path: z.string().describe("Path to the scene or resource file (e.g. 'res://scenes/player.tscn')"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_scene_dependencies", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("cross_scene_set_property", "Preview or apply a property change on all nodes of a given type across scene files in the project. Defaults to dry_run=true (returns the matching scenes and node paths without writing). To actually apply: pass force=true AND dry_run=false. Inactive open scenes are skipped and reported in skipped_open_scenes — open them as the active tab first to live-edit. The active open scene is live-edited via UndoRedo so changes are visible in the editor and undoable. Closed scenes are offline-saved. The response includes a per-scene `mode` field: dry_run / offline_saved / live_open_scene.", {
type: z.string().describe("Node type to target (e.g. 'Label', 'Sprite2D')"),
property: z.string().describe("Property name to set"),
value: z.union([z.string(), z.number(), z.boolean()]).describe("Value to set. Strings auto-parsed for Vector2, Color, etc."),
path_filter: z.string().optional().describe("Directory to search in (default: 'res://')"),
exclude_addons: z.boolean().optional().describe("Exclude addons/ directory (default: true)"),
dry_run: z.boolean().optional().describe("Preview only — list affected scenes and nodes without writing. Defaults to true unless force=true is set."),
force: z.boolean().optional().describe("Required (alongside dry_run=false) to actually write. Acknowledges that this can modify many scene files at once."),
}, async (params) => {
try {
const result = await godot.sendCommand("cross_scene_set_property", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=batch-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerEditorTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=editor-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"editor-tools.d.ts","sourceRoot":"","sources":["../../src/tools/editor-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA2SN"}

View File

@@ -0,0 +1,231 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerEditorTools(server, godot) {
server.tool("get_editor_errors", "Get recent errors and stack traces from the Godot editor log", {
max_lines: z.number().optional().describe("Maximum log lines to scan for errors (default: 50)"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_editor_errors", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_output_log", "Read the full Godot editor Output panel content. Unlike get_editor_errors which filters for errors only, this returns all output including print() statements and warnings.", {
max_lines: z.number().optional().describe("Maximum number of lines to return from the end (default: 100)"),
filter: z.string().optional().describe("Filter lines containing this substring (case-sensitive)"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_output_log", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_editor_screenshot", "Capture a screenshot of the Godot editor's 2D/3D viewport", {
save_path: z.string().optional().describe("Optional res:// or user:// path to save the screenshot as PNG file (e.g. 'res://screenshot.png'). When provided, the image is saved to disk and the file path is returned instead of base64 data."),
}, async (params) => {
try {
const result = await godot.sendCommand("get_editor_screenshot", params);
if (result && typeof result === "object" && "saved_path" in result) {
return {
content: [
{
type: "text",
text: `Screenshot saved: ${result.saved_path} (${result.width}x${result.height})`,
},
],
};
}
if (result && typeof result === "object" && "image_base64" in result) {
return {
content: [
{
type: "image",
data: result.image_base64,
mimeType: "image/png",
},
{
type: "text",
text: `Screenshot captured: ${result.width}x${result.height}`,
},
],
};
}
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_game_screenshot", "Capture a single screenshot of the running game (requires a scene to be playing). Good for checking static visual state (UI layout, scene composition, colors). For verifying animations or movement, use capture_frames instead — a single screenshot cannot confirm whether an animation is playing.", {
save_path: z.string().optional().describe("Optional res:// or user:// path to save the screenshot as PNG file (e.g. 'res://screenshot.png'). When provided, the image is saved to disk and the file path is returned instead of base64 data."),
}, async (params) => {
try {
const result = await godot.sendCommand("get_game_screenshot", params);
if (result && typeof result === "object" && "saved_path" in result) {
return {
content: [
{
type: "text",
text: `Game screenshot saved: ${result.saved_path} (${result.width}x${result.height})${result.note ? ` (${result.note})` : ""}`,
},
],
};
}
if (result && typeof result === "object" && "image_base64" in result) {
return {
content: [
{
type: "image",
data: result.image_base64,
mimeType: "image/png",
},
{
type: "text",
text: `Game screenshot: ${result.width}x${result.height}${result.note ? ` (${result.note})` : ""}`,
},
],
};
}
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("execute_editor_script", "Execute arbitrary GDScript code inside the Godot editor. Use _mcp_print() to output values. By default refuses to execute code that contains direct file/resource write APIs (ResourceSaver.save, FileAccess WRITE, ProjectSettings.save, ConfigFile.save, DirAccess filesystem mutations) because those bypass the per-command open-resource guards. Use the dedicated MCP tools (save_scene, create_script, etc.) for those operations, or pass allow_unsafe_editor_io=true ONLY when you have verified no open editor resource will be overwritten.", {
code: z.string().describe("GDScript code to execute. Use _mcp_print(value) to capture output. " +
"The code runs inside a run() function with access to the full editor API."),
allow_unsafe_editor_io: z.boolean().optional().describe("Override the file-write safety guard. Only set this when you are certain no open scene/script/shader will be overwritten by the script. Prefer the dedicated MCP tools for ordinary save flows."),
}, async (params) => {
try {
const result = await godot.sendCommand("execute_editor_script", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("clear_output", "Clear the Godot editor output panel", {}, async () => {
try {
const result = await godot.sendCommand("clear_output");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_signals", "Get all signals of a node, including current connections", {
node_path: z.string().describe("Path to the node to inspect"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_signals", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("reload_plugin", "Reload the Godot MCP Pro plugin (disable/re-enable). Connection will briefly drop and auto-reconnect. NOTE: This does NOT reload GDScript preload() caches. If you changed GDScript command files, use execute_editor_script with 'EditorInterface.restart_editor(true)' instead for a full editor restart.", {}, async () => {
try {
const result = await godot.sendCommand("reload_plugin");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("reload_project", "Rescan the Godot project filesystem and reload changed scripts (no reconnection needed)", {}, async () => {
try {
const result = await godot.sendCommand("reload_project");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("compare_screenshots", "Compare two screenshots pixel-by-pixel and return a diff analysis. Returns changed pixel count, diff percentage, and a highlighted diff image. Useful for visual regression testing. Accepts file paths (res://, user://) or base64 PNG strings.", {
image_a: z.string().describe("First image: file path (e.g. 'user://screenshot_a.png') or base64 PNG string"),
image_b: z.string().describe("Second image: file path (e.g. 'user://screenshot_b.png') or base64 PNG string"),
threshold: z.number().optional().describe("Color difference threshold (0-255, default: 10). Pixels with max channel difference below this are considered identical."),
}, async (params) => {
try {
const result = await godot.sendCommand("compare_screenshots", params);
const content = [];
// Add summary text
content.push({
type: "text",
text: JSON.stringify({
identical: result.identical,
changed_pixels: result.changed_pixels,
total_pixels: result.total_pixels,
diff_percentage: result.diff_percentage,
threshold: result.threshold,
size: `${result.width}x${result.height}`,
}, null, 2),
});
// Add diff image if there are differences
if (result.diff_image_base64 && !result.identical) {
content.push({
type: "image",
data: result.diff_image_base64,
mimeType: "image/png",
});
}
return { content };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_auto_dismiss", "Enable or disable automatic dismissal of blocking editor dialogs (e.g. 'Reload from disk?', 'Save changes?'). Enable this before operations that modify files externally, and disable when done. Disabled by default.", {
enabled: z.boolean().describe("true to enable auto-dismiss, false to disable"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_auto_dismiss", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_editor_camera", "Get the current 3D editor viewport camera position, rotation, and FOV. Use this to understand the current view before taking editor screenshots.", {}, async () => {
try {
const result = await godot.sendCommand("get_editor_camera");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_editor_camera", "Move the 3D editor viewport camera to a specific position and orientation. Use this to frame a view before taking editor screenshots to validate changes visually.", {
position: z.object({
x: z.number().describe("X position"),
y: z.number().describe("Y position"),
z: z.number().describe("Z position"),
}).optional().describe("Camera world position"),
rotation_degrees: z.object({
x: z.number().describe("Pitch (degrees)"),
y: z.number().describe("Yaw (degrees)"),
z: z.number().describe("Roll (degrees)"),
}).optional().describe("Camera rotation in degrees"),
look_at: z.object({
x: z.number().describe("Target X"),
y: z.number().describe("Target Y"),
z: z.number().describe("Target Z"),
}).optional().describe("Point to look at (overrides rotation_degrees if both set)"),
fov: z.number().optional().describe("Field of view in degrees (default: 75)"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_editor_camera", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=editor-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerExportTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=export-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"export-tools.d.ts","sourceRoot":"","sources":["../../src/tools/export-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA8CN"}

View File

@@ -0,0 +1,36 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerExportTools(server, godot) {
server.tool("list_export_presets", "List all export presets configured in export_presets.cfg", {}, async () => {
try {
const result = await godot.sendCommand("list_export_presets");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("export_project", "Get the export command for a preset (direct export from editor is not supported in Godot 4)", {
preset_name: z.string().optional().describe("Export preset name"),
preset_index: z.number().optional().describe("Export preset index (alternative to name)"),
debug: z.boolean().optional().describe("Debug export (default: true)"),
}, async (params) => {
try {
const result = await godot.sendCommand("export_project", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_export_info", "Get export-related project info (executable path, templates, project path)", {}, async () => {
try {
const result = await godot.sendCommand("get_export_info");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=export-tools.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"export-tools.js","sourceRoot":"","sources":["../../src/tools/export-tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,UAAU,mBAAmB,CACjC,MAAiB,EACjB,KAAsB;IAEtB,MAAM,CAAC,IAAI,CACT,qBAAqB,EACrB,0DAA0D,EAC1D,EAAE,EACF,KAAK,IAAI,EAAE;QACT,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,qBAAqB,CAAC,CAAC;YAC9D,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,gBAAgB,EAChB,6FAA6F,EAC7F;QACE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oBAAoB,CAAC;QACjE,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,2CAA2C,CAAC;QACzF,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8BAA8B,CAAC;KACvE,EACD,KAAK,EAAE,MAAM,EAAE,EAAE;QACf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC;YACjE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,iBAAiB,EACjB,4EAA4E,EAC5E,EAAE,EACF,KAAK,IAAI,EAAE;QACT,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,iBAAiB,CAAC,CAAC;YAC1D,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerInputMapTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=input-map-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"input-map-tools.d.ts","sourceRoot":"","sources":["../../src/tools/input-map-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA8CN"}

View File

@@ -0,0 +1,41 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerInputMapTools(server, godot) {
server.tool("get_input_actions", "Get all input actions defined in the project's Input Map with their key/button bindings", {
filter: z.string().optional().describe("Filter action names containing this substring"),
include_builtin: z.boolean().optional().describe("Include built-in ui_* actions (default: false)"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_input_actions", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_input_action", "Create or update an input action with key/mouse/joypad bindings. Saves to project.godot and updates the runtime InputMap.", {
action: z.string().describe("Action name (e.g. 'move_left', 'jump', 'attack')"),
events: z.array(z.object({
type: z.enum(["key", "mouse_button", "joypad_button", "joypad_motion"]).describe("Event type"),
keycode: z.string().optional().describe("Key name for 'key' type (e.g. 'W', 'Space', 'Escape', 'Shift')"),
physical_keycode: z.string().optional().describe("Physical key name for 'key' type"),
ctrl: z.boolean().optional().describe("Ctrl modifier for 'key' type"),
shift: z.boolean().optional().describe("Shift modifier for 'key' type"),
alt: z.boolean().optional().describe("Alt modifier for 'key' type"),
meta: z.boolean().optional().describe("Meta/Cmd modifier for 'key' type"),
button_index: z.number().optional().describe("Button index for mouse/joypad button types"),
axis: z.number().optional().describe("Axis index for joypad_motion type"),
axis_value: z.number().optional().describe("Axis value (-1.0 or 1.0) for joypad_motion type"),
})).describe("Array of input event bindings"),
deadzone: z.number().optional().describe("Deadzone for analog inputs (default: 0.5)"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_input_action", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=input-map-tools.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"input-map-tools.js","sourceRoot":"","sources":["../../src/tools/input-map-tools.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerInputTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=input-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"input-tools.d.ts","sourceRoot":"","sources":["../../src/tools/input-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAIzD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CAgIN"}

View File

@@ -0,0 +1,109 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
import { coerceNumber } from "../utils/zod-coerce.js";
export function registerInputTools(server, godot) {
server.tool("simulate_key", "Simulate a keyboard key press or release in the running game. Use `duration` to hold a key for a set time (auto-releases after). Without duration: keys are NOT auto-released — you must explicitly call with pressed=false to release them.", {
keycode: z.string().describe("Key constant (e.g. 'KEY_SPACE', 'KEY_W', 'KEY_ESCAPE')"),
pressed: z.boolean().optional().describe("true for press, false for release (default: true)"),
duration: coerceNumber().optional().describe("Hold duration in seconds (e.g. 1.5). Key is pressed, held for this duration, then auto-released. Cannot be used with pressed=false."),
shift: z.boolean().optional().describe("Shift modifier (default: false)"),
ctrl: z.boolean().optional().describe("Ctrl modifier (default: false)"),
alt: z.boolean().optional().describe("Alt modifier (default: false)"),
}, async (params) => {
try {
if (params.duration !== undefined && params.duration > 0) {
const { duration, ...keyParams } = params;
// Press
await godot.sendCommand("simulate_key", { ...keyParams, pressed: true });
// Hold
await new Promise(resolve => setTimeout(resolve, duration * 1000));
// Release
await godot.sendCommand("simulate_key", { ...keyParams, pressed: false });
return { content: [{ type: "text", text: JSON.stringify({
event: { keycode: params.keycode, duration, shift: params.shift, ctrl: params.ctrl, alt: params.alt, auto_released: true },
sent: true
}, null, 2) }] };
}
const result = await godot.sendCommand("simulate_key", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("simulate_mouse_click", "Simulate a mouse button click at a position in the running game. By default sends both press and release (auto_release) so UI buttons work correctly.", {
x: z.number().optional().describe("X position in viewport (default: 0)"),
y: z.number().optional().describe("Y position in viewport (default: 0)"),
button: z.number().optional().describe("Mouse button index: 1=left, 2=right, 3=middle (default: 1)"),
pressed: z.boolean().optional().describe("true for press, false for release (default: true)"),
double_click: z.boolean().optional().describe("Double click (default: false)"),
auto_release: z.boolean().optional().describe("Auto-send release after press so buttons fire (default: true). Set false for drag operations."),
}, async (params) => {
try {
const result = await godot.sendCommand("simulate_mouse_click", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("simulate_mouse_move", "Simulate mouse movement in the running game. Use x/y for absolute viewport positioning (UI interaction), or relative_x/relative_y for relative motion (camera rotation in 3D games, FPS-style look). For 3D camera rotation: relative_x rotates yaw (negative = look left, positive = look right), relative_y rotates pitch (negative = look up, positive = look down). Typical values: 200-400px for a ~90° turn. Use navigate_to tool to calculate exact relative_x needed to face a target.", {
x: z.number().optional().describe("Absolute X position in viewport (for UI interaction)"),
y: z.number().optional().describe("Absolute Y position in viewport (for UI interaction)"),
relative_x: z.number().optional().describe("Relative X movement in pixels. For 3D camera: negative = look left, positive = look right. ~400px ≈ 180° turn"),
relative_y: z.number().optional().describe("Relative Y movement in pixels. For 3D camera: negative = look up, positive = look down"),
button_mask: z.number().optional().describe("Mouse button mask to simulate drag. 1=left button held, 2=right button held, 4=middle button held. Required for drag operations like camera pan. (default: 0)"),
unhandled: z.boolean().optional().describe("Force event to bypass GUI layer and go directly to _unhandled_input(). Auto-enabled when button_mask > 0. Use for camera pan/drag when UI overlays consume mouse events. (default: false)"),
}, async (params) => {
try {
const result = await godot.sendCommand("simulate_mouse_move", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("simulate_action", "Simulate a Godot Input Action (e.g. 'jump', 'move_left') in the running game", {
action: z.string().describe("Action name as defined in Input Map (e.g. 'jump', 'move_left')"),
pressed: z.boolean().optional().describe("true for press, false for release (default: true)"),
strength: z.number().optional().describe("Action strength 0.0-1.0 (default: 1.0)"),
}, async (params) => {
try {
const result = await godot.sendCommand("simulate_action", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("simulate_sequence", "Simulate a sequence of input events with optional frame delays between them. Useful for complex input patterns like press W → wait 30 frames → press Space → wait → release all. After the sequence, use capture_frames to verify the visual result.", {
events: z.array(z.object({
type: z.string().describe("Event type: 'key', 'mouse_button', 'mouse_motion', or 'action'"),
keycode: z.string().optional().describe("For 'key': key constant (e.g. 'KEY_SPACE')"),
action: z.string().optional().describe("For 'action': action name"),
button: z.number().optional().describe("For 'mouse_button': button index"),
pressed: z.boolean().optional().describe("Press state (default: true)"),
x: z.number().optional().describe("X position for mouse events"),
y: z.number().optional().describe("Y position for mouse events"),
relative_x: z.number().optional().describe("Relative X for mouse_motion"),
relative_y: z.number().optional().describe("Relative Y for mouse_motion"),
button_mask: z.number().optional().describe("Mouse button mask for mouse_motion drag: 1=left, 2=right, 4=middle"),
unhandled: z.boolean().optional().describe("Bypass GUI, send directly to _unhandled_input. Auto-enabled for mouse_motion with button_mask > 0"),
shift: z.boolean().optional(),
ctrl: z.boolean().optional(),
alt: z.boolean().optional(),
strength: z.number().optional(),
double_click: z.boolean().optional(),
})).describe("Array of input events to send"),
frame_delay: z.number().optional().describe("Frames to wait between events (default: 1, 0 = all in one frame)"),
}, async (params) => {
try {
const result = await godot.sendCommand("simulate_sequence", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=input-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerNavigationTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=navigation-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"navigation-tools.d.ts","sourceRoot":"","sources":["../../src/tools/navigation-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA2GN"}

View File

@@ -0,0 +1,87 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerNavigationTools(server, godot) {
server.tool("setup_navigation_region", "Add a NavigationRegion2D/3D child to a node with auto-created NavigationPolygon or NavigationMesh. Auto-detects 2D/3D from parent context.", {
node_path: z.string().describe("Path to the parent node to add the region to"),
mode: z.string().optional().describe("Force '2d' or '3d' mode, or 'auto' to detect from parent (default: auto)"),
name: z.string().optional().describe("Name for the NavigationRegion node"),
navigation_layers: z.number().optional().describe("Navigation layers bitmask"),
agent_radius: z.number().optional().describe("Agent radius for mesh generation (3D default: 0.5, 2D: from NavigationPolygon)"),
agent_height: z.number().optional().describe("Agent height (3D only, default: 1.5)"),
agent_max_climb: z.number().optional().describe("Max climb height (3D only, default: 0.25)"),
agent_max_slope: z.number().optional().describe("Max slope angle in degrees (3D only, default: 45.0)"),
cell_size: z.number().optional().describe("Cell size for navigation mesh (default: 0.25 for 3D)"),
cell_height: z.number().optional().describe("Cell height (3D only, default: 0.25)"),
source_geometry_mode: z.string().optional().describe("2D only: root_node, groups_with_children, or groups_explicit"),
}, async (params) => {
try {
const result = await godot.sendCommand("setup_navigation_region", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("bake_navigation_mesh", "Bake navigation mesh for a NavigationRegion3D, or set outline vertices and generate polygons for a NavigationRegion2D.", {
node_path: z.string().describe("Path to the NavigationRegion2D or NavigationRegion3D node"),
outline: z.array(z.union([
z.array(z.number()).describe("[x, y] coordinate pair"),
z.object({ x: z.number(), y: z.number() }),
])).optional().describe("2D only: Array of outline vertices as [x,y] pairs or {x,y} objects. At least 3 vertices required."),
}, async (params) => {
try {
const result = await godot.sendCommand("bake_navigation_mesh", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("setup_navigation_agent", "Add a NavigationAgent2D/3D child to a node and configure pathfinding and avoidance properties. Auto-detects 2D/3D from parent context.", {
node_path: z.string().describe("Path to the parent node to add the agent to"),
mode: z.string().optional().describe("Force '2d' or '3d' mode, or 'auto' to detect from parent (default: auto)"),
name: z.string().optional().describe("Name for the NavigationAgent node"),
path_desired_distance: z.number().optional().describe("Distance threshold to advance to next path point"),
target_desired_distance: z.number().optional().describe("Distance threshold to consider target reached"),
radius: z.number().optional().describe("Agent radius for avoidance"),
neighbor_distance: z.number().optional().describe("Max distance to consider other agents as neighbors"),
max_neighbors: z.number().optional().describe("Max number of neighbors for avoidance"),
max_speed: z.number().optional().describe("Maximum movement speed for avoidance"),
avoidance_enabled: z.boolean().optional().describe("Enable avoidance behavior"),
navigation_layers: z.number().optional().describe("Navigation layers bitmask for pathfinding queries"),
}, async (params) => {
try {
const result = await godot.sendCommand("setup_navigation_agent", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_navigation_layers", "Set navigation layers for a NavigationRegion or NavigationAgent. Supports bitmask value, layer bit numbers, or named layers from ProjectSettings.", {
node_path: z.string().describe("Path to a NavigationRegion2D/3D or NavigationAgent2D/3D node"),
layers: z.number().optional().describe("Navigation layers as a bitmask value (e.g. 5 = layers 1 and 3)"),
layer_bits: z.array(z.number()).optional().describe("Array of 1-based layer numbers to enable (e.g. [1, 3] = bitmask 5)"),
layer_names: z.array(z.string()).optional().describe("Array of named layer names from ProjectSettings (layer_names/2d_navigation/layer_N or 3d)"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_navigation_layers", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_navigation_info", "Get navigation setup info for a node and its subtree: all NavigationRegions, NavigationAgents, their layers, and mesh/polygon data.", {
node_path: z.string().describe("Path to the root node to inspect"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_navigation_info", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=navigation-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerNodeTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=node-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"node-tools.d.ts","sourceRoot":"","sources":["../../src/tools/node-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CAqPN"}

View File

@@ -0,0 +1,180 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerNodeTools(server, godot) {
server.tool("add_node", "Add a new node to the current scene. Supports built-in Godot types and script-defined classes (class_name).", {
type: z.string().describe("Node type — built-in (e.g. 'Sprite2D', 'Camera2D') or script class_name (e.g. 'HoverDetector', 'StationBuilder')"),
parent_path: z.string().optional().describe("Parent node path (default: root '.')"),
name: z.string().optional().describe("Node name"),
properties: z.record(z.string(), z.any()).optional().describe("Properties to set (e.g. {\"position\": \"Vector2(100, 200)\"})"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_node", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("delete_node", "Delete a node from the current scene (supports undo)", {
node_path: z.string().describe("Path to the node to delete"),
}, async (params) => {
try {
const result = await godot.sendCommand("delete_node", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("duplicate_node", "Duplicate a node and all its children in the current scene", {
node_path: z.string().describe("Path to the node to duplicate"),
name: z.string().optional().describe("Name for the duplicate (default: original_copy)"),
}, async (params) => {
try {
const result = await godot.sendCommand("duplicate_node", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("move_node", "Move/reparent a node to a new parent in the scene tree", {
node_path: z.string().describe("Path to the node to move"),
new_parent_path: z.string().describe("Path to the new parent node"),
}, async (params) => {
try {
const result = await godot.sendCommand("move_node", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("update_property", "Change a property on any node. Supports Vector2, Color, and other Godot types via string parsing.", {
node_path: z.string().describe("Path to the target node"),
property: z.string().describe("Property name (e.g. 'position', 'modulate', 'visible')"),
value: z.any().describe("New value. Strings are auto-parsed: 'Vector2(10,20)', 'Color(1,0,0)', '#ff0000', etc."),
}, async (params) => {
try {
const result = await godot.sendCommand("update_property", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_node_properties", "Get all editor-visible properties of a node with their current values", {
node_path: z.string().describe("Path to the node"),
category: z.string().optional().describe("Filter by property category prefix (e.g. 'transform', 'texture')"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_node_properties", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("add_resource", "Add a resource (Shape2D, Material, Texture, etc.) to a node's property", {
node_path: z.string().describe("Path to the target node"),
property: z.string().describe("Property to set the resource on (e.g. 'shape', 'material', 'texture')"),
resource_type: z.string().describe("Resource class name (e.g. 'RectangleShape2D', 'CircleShape2D', 'StandardMaterial3D')"),
resource_properties: z.record(z.string(), z.any()).optional().describe("Properties to set on the created resource"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_resource", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_anchor_preset", "Set a Control node's anchor preset (e.g. full_rect, center, top_left)", {
node_path: z.string().describe("Path to the Control node"),
preset: z.string().describe("Anchor preset name: top_left, top_right, bottom_left, bottom_right, center_left, center_top, center_right, center_bottom, center, left_wide, top_wide, right_wide, bottom_wide, vcenter_wide, hcenter_wide, full_rect"),
keep_offsets: z.boolean().optional().describe("Keep current offsets (default: false)"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_anchor_preset", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("rename_node", "Rename a node in the current scene", {
node_path: z.string().describe("Path to the node to rename"),
new_name: z.string().describe("New name for the node"),
}, async (params) => {
try {
const result = await godot.sendCommand("rename_node", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("connect_signal", "Connect a signal from one node to a method on another node", {
source_path: z.string().describe("Path to the source node (emitter)"),
signal_name: z.string().describe("Signal name to connect"),
target_path: z.string().describe("Path to the target node (receiver)"),
method_name: z.string().describe("Method name on target to call"),
}, async (params) => {
try {
const result = await godot.sendCommand("connect_signal", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("disconnect_signal", "Disconnect a signal connection between two nodes", {
source_path: z.string().describe("Path to the source node (emitter)"),
signal_name: z.string().describe("Signal name to disconnect"),
target_path: z.string().describe("Path to the target node (receiver)"),
method_name: z.string().describe("Method name on target"),
}, async (params) => {
try {
const result = await godot.sendCommand("disconnect_signal", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_node_groups", "Get all groups a node belongs to (excludes internal groups starting with '_')", {
node_path: z.string().describe("Path to the node"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_node_groups", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_node_groups", "Set the groups a node belongs to. Computes diff with current groups and adds/removes as needed.", {
node_path: z.string().describe("Path to the node"),
groups: z.array(z.string()).describe("Desired list of group names (replaces current groups)"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_node_groups", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("find_nodes_in_group", "Find all nodes in the current scene that belong to a specific group", {
group: z.string().describe("Group name to search for"),
}, async (params) => {
try {
const result = await godot.sendCommand("find_nodes_in_group", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=node-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerParticleTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=particle-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"particle-tools.d.ts","sourceRoot":"","sources":["../../src/tools/particle-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA8HN"}

View File

@@ -0,0 +1,106 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerParticleTools(server, godot) {
server.tool("create_particles", "Add a GPUParticles2D or GPUParticles3D node with a ParticleProcessMaterial. Configure amount, lifetime, one_shot, explosiveness, and randomness.", {
parent_path: z.string().describe("Path to the parent node to add particles to"),
name: z.string().optional().describe("Name for the particles node (default: 'Particles')"),
is_3d: z.boolean().optional().describe("Create GPUParticles3D instead of GPUParticles2D (default: false)"),
amount: z.number().optional().describe("Number of particles (default: 16)"),
lifetime: z.number().optional().describe("Particle lifetime in seconds (default: 1.0)"),
one_shot: z.boolean().optional().describe("Emit only once (default: false)"),
explosiveness: z.number().optional().describe("Explosiveness ratio 0-1 (default: 0.0)"),
randomness: z.number().optional().describe("Randomness ratio 0-1 (default: 0.0)"),
emitting: z.boolean().optional().describe("Start emitting immediately (default: true)"),
}, async (params) => {
try {
const result = await godot.sendCommand("create_particles", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_particle_material", "Configure ParticleProcessMaterial properties: direction, spread, velocity, gravity, scale, color, emission shape (point/sphere/box/ring), angular/orbit velocity, damping, and attractor interaction.", {
node_path: z.string().describe("Path to the GPUParticles2D/3D node"),
direction: z.object({
x: z.number(),
y: z.number(),
z: z.number(),
}).optional().describe("Emission direction vector"),
spread: z.number().optional().describe("Spread angle in degrees (0-180)"),
initial_velocity_min: z.number().optional().describe("Minimum initial velocity"),
initial_velocity_max: z.number().optional().describe("Maximum initial velocity"),
gravity: z.object({
x: z.number(),
y: z.number(),
z: z.number(),
}).optional().describe("Gravity vector"),
scale_min: z.number().optional().describe("Minimum particle scale"),
scale_max: z.number().optional().describe("Maximum particle scale"),
color: z.string().optional().describe("Particle color (hex '#RRGGBB' or named color)"),
emission_shape: z.string().optional().describe("Emission shape: point, sphere, sphere_surface, box, ring"),
emission_sphere_radius: z.number().optional().describe("Sphere emission radius"),
emission_box_extents: z.object({
x: z.number(),
y: z.number(),
z: z.number(),
}).optional().describe("Box emission extents"),
emission_ring_radius: z.number().optional().describe("Ring outer radius"),
emission_ring_inner_radius: z.number().optional().describe("Ring inner radius"),
emission_ring_height: z.number().optional().describe("Ring height"),
angular_velocity_min: z.number().optional().describe("Minimum angular velocity (degrees/sec)"),
angular_velocity_max: z.number().optional().describe("Maximum angular velocity (degrees/sec)"),
orbit_velocity_min: z.number().optional().describe("Minimum orbit velocity"),
orbit_velocity_max: z.number().optional().describe("Maximum orbit velocity"),
damping_min: z.number().optional().describe("Minimum damping"),
damping_max: z.number().optional().describe("Maximum damping"),
attractor_interaction_enabled: z.boolean().optional().describe("Enable attractor interaction"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_particle_material", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_particle_color_gradient", "Set a color ramp (gradient) on a particle system's material. Provide an array of color stops with offset (0-1) and color.", {
node_path: z.string().describe("Path to the GPUParticles2D/3D node"),
stops: z.array(z.object({
offset: z.number().describe("Gradient position (0.0 to 1.0)"),
color: z.string().describe("Color at this stop (hex '#RRGGBB' or named color)"),
})).describe("Array of gradient color stops"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_particle_color_gradient", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("apply_particle_preset", "Apply a named particle preset. Available presets: explosion (burst, short life), fire (upward, orange gradient), smoke (slow upward, gray), sparks (burst, high velocity), rain (downward, blue), snow (slow downward, drift), magic (orbit, colorful), dust (ambient, subtle).", {
node_path: z.string().describe("Path to the GPUParticles2D/3D node"),
preset: z.string().describe("Preset name: explosion, fire, smoke, sparks, rain, snow, magic, dust"),
}, async (params) => {
try {
const result = await godot.sendCommand("apply_particle_preset", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_particle_info", "Get the full configuration of a particle system: node properties, material settings, emission shape, color gradient stops.", {
node_path: z.string().describe("Path to the GPUParticles2D/3D node"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_particle_info", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=particle-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerPhysicsTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=physics-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"physics-tools.d.ts","sourceRoot":"","sources":["../../src/tools/physics-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA4IN"}

View File

@@ -0,0 +1,115 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerPhysicsTools(server, godot) {
server.tool("setup_collision", "Add a CollisionShape2D/3D child to a physics body or area node with a specified shape. Auto-detects 2D/3D from the parent node type.", {
node_path: z.string().describe("Path to the parent physics body or area node (e.g. CharacterBody2D, StaticBody3D, Area2D)"),
shape: z.string().describe("Shape type: 'rectangle'/'rect', 'circle', 'capsule', 'segment' (2D only), 'cylinder' (3D only), 'custom'/'convex'. For 3D: 'box'/'sphere' also work."),
width: z.number().optional().describe("Width for rectangle/box shape (default: 32 for 2D, 1 for 3D)"),
height: z.number().optional().describe("Height for rectangle/box/capsule/cylinder shape"),
depth: z.number().optional().describe("Depth for 3D box shape (default: 1)"),
radius: z.number().optional().describe("Radius for circle/sphere/capsule/cylinder shape"),
ax: z.number().optional().describe("Segment start X (2D segment only)"),
ay: z.number().optional().describe("Segment start Y (2D segment only)"),
bx: z.number().optional().describe("Segment end X (2D segment only)"),
by: z.number().optional().describe("Segment end Y (2D segment only)"),
points: z.array(z.array(z.number())).optional().describe("Convex polygon points as [[x,y],...] for 2D or [[x,y,z],...] for 3D"),
disabled: z.boolean().optional().describe("Create the collision shape disabled (default: false)"),
one_way_collision: z.boolean().optional().describe("Enable one-way collision (2D only, default: false)"),
dimension: z.string().optional().describe("Force '2d' or '3d' if auto-detection fails"),
}, async (params) => {
try {
const result = await godot.sendCommand("setup_collision", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_physics_layers", "Set collision layer and/or mask on a physics body or area node. Supports bitmask integers or arrays of layer numbers.", {
node_path: z.string().describe("Path to the node with collision layers"),
collision_layer: z.union([z.number(), z.array(z.number())]).optional().describe("Collision layer: bitmask integer or array of layer numbers [1,3,5]"),
collision_mask: z.union([z.number(), z.array(z.number())]).optional().describe("Collision mask: bitmask integer or array of layer numbers [1,2,4]"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_physics_layers", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_physics_layers", "Get the current collision layer and mask for a node, including named layer info from ProjectSettings.", {
node_path: z.string().describe("Path to the node with collision layers"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_physics_layers", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("add_raycast", "Add a RayCast2D/3D child node for collision detection. Auto-detects 2D/3D from the parent node type.", {
node_path: z.string().describe("Path to the parent node"),
name: z.string().optional().describe("Name for the raycast node (default: 'RayCast')"),
target_x: z.number().optional().describe("Target position X (default: 0)"),
target_y: z.number().optional().describe("Target position Y (default: 50 for 2D, -1 for 3D)"),
target_z: z.number().optional().describe("Target position Z (3D only, default: 0)"),
collision_mask: z.number().optional().describe("Collision mask bitmask (default: 1)"),
enabled: z.boolean().optional().describe("Enable the raycast (default: true)"),
collide_with_areas: z.boolean().optional().describe("Collide with Area nodes (default: false)"),
collide_with_bodies: z.boolean().optional().describe("Collide with physics bodies (default: true)"),
hit_from_inside: z.boolean().optional().describe("Detect hits from inside shapes (default: false)"),
dimension: z.string().optional().describe("Force '2d' or '3d' if auto-detection fails"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_raycast", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("setup_physics_body", "Configure physics body properties. For CharacterBody2D/3D: floor settings, motion mode, etc. For RigidBody2D/3D: mass, gravity, damping, etc.", {
node_path: z.string().describe("Path to the physics body node"),
// CharacterBody properties
floor_stop_on_slope: z.boolean().optional().describe("CharacterBody: stop on slopes when not moving"),
floor_max_angle: z.number().optional().describe("CharacterBody: maximum floor angle in radians (default ~0.785 = 45 degrees)"),
floor_snap_length: z.number().optional().describe("CharacterBody: floor snap distance for sticking to the ground"),
wall_min_slide_angle: z.number().optional().describe("CharacterBody: minimum angle for wall sliding in radians"),
motion_mode: z.string().optional().describe("CharacterBody: 'grounded' or 'floating'"),
max_slides: z.number().optional().describe("CharacterBody: maximum slide iterations (default: 6)"),
slide_on_ceiling: z.boolean().optional().describe("CharacterBody: allow sliding on ceiling"),
// RigidBody properties
mass: z.number().optional().describe("RigidBody: mass in kg (default: 1)"),
gravity_scale: z.number().optional().describe("RigidBody: gravity multiplier (default: 1, 0 = no gravity)"),
linear_damp: z.number().optional().describe("RigidBody: linear velocity damping"),
angular_damp: z.number().optional().describe("RigidBody: angular velocity damping"),
freeze: z.boolean().optional().describe("RigidBody: freeze the body (stop physics simulation)"),
freeze_mode: z.string().optional().describe("RigidBody: 'static' or 'kinematic' freeze behavior"),
continuous_cd: z.union([z.string(), z.boolean()]).optional().describe("RigidBody: continuous collision detection. 2D: 'disabled'/'cast_ray'/'cast_shape'. 3D: true/false"),
contact_monitor: z.boolean().optional().describe("RigidBody: enable contact monitoring for body_entered/body_exited signals"),
max_contacts_reported: z.number().optional().describe("RigidBody: max contacts to report (requires contact_monitor)"),
}, async (params) => {
try {
const result = await godot.sendCommand("setup_physics_body", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_collision_info", "Get detailed collision information for a node: all collision shapes, layers/masks, raycasts, and physics body settings. Scans children by default.", {
node_path: z.string().describe("Path to the node to inspect"),
include_children: z.boolean().optional().describe("Include children in the scan (default: true)"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_collision_info", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=physics-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerProfilingTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=profiling-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"profiling-tools.d.ts","sourceRoot":"","sources":["../../src/tools/profiling-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA8BN"}

View File

@@ -0,0 +1,25 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerProfilingTools(server, godot) {
server.tool("get_performance_monitors", "Get all Godot performance monitors (FPS, memory, draw calls, physics, navigation, etc.)", {
category: z.string().optional().describe("Filter by category prefix: 'fps', 'memory', 'render', 'physics_2d', 'physics_3d', 'navigation'"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_performance_monitors", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_editor_performance", "Get a quick performance summary (FPS, frame time, draw calls, memory usage)", {}, async () => {
try {
const result = await godot.sendCommand("get_editor_performance");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=profiling-tools.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"profiling-tools.js","sourceRoot":"","sources":["../../src/tools/profiling-tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,UAAU,sBAAsB,CACpC,MAAiB,EACjB,KAAsB;IAEtB,MAAM,CAAC,IAAI,CACT,0BAA0B,EAC1B,yFAAyF,EACzF;QACE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gGAAgG,CAAC;KAC3I,EACD,KAAK,EAAE,MAAM,EAAE,EAAE;QACf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,0BAA0B,EAAE,MAAM,CAAC,CAAC;YAC3E,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,wBAAwB,EACxB,6EAA6E,EAC7E,EAAE,EACF,KAAK,IAAI,EAAE;QACT,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,wBAAwB,CAAC,CAAC;YACjE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACpF,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerProjectTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=project-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"project-tools.d.ts","sourceRoot":"","sources":["../../src/tools/project-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA0KN"}

View File

@@ -0,0 +1,125 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerProjectTools(server, godot) {
server.tool("get_project_info", "Get Godot project metadata including name, version, viewport settings, renderer, and autoloads", {}, async () => {
try {
const result = await godot.sendCommand("get_project_info");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_filesystem_tree", "Get the project's file/directory tree with optional filtering by extension (e.g. *.gd, *.tscn)", {
path: z.string().optional().describe("Root path to scan (default: res://)"),
filter: z.string().optional().describe("Glob filter pattern (e.g. '*.gd', '*.tscn')"),
max_depth: z.number().optional().describe("Maximum directory depth to scan (default: 10)"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_filesystem_tree", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("search_files", "Search for files by name using fuzzy matching or glob patterns", {
query: z.string().describe("Search query (fuzzy match or glob pattern)"),
path: z.string().optional().describe("Root path to search (default: res://)"),
file_type: z.string().optional().describe("Filter by file extension (e.g. 'gd', 'tscn')"),
max_results: z.number().optional().describe("Maximum results to return (default: 50)"),
}, async (params) => {
try {
const result = await godot.sendCommand("search_files", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("search_in_files", "Search for text content inside project files (grep-like). Searches through GDScript, scenes, resources, shaders, and other text files. Skips addons/ and .godot/ directories.", {
query: z.string().describe("Text to search for (plain text or regex pattern)"),
path: z.string().optional().describe("Root path to search (default: res://)"),
regex: z.boolean().optional().describe("Use regex matching (default: false)"),
file_type: z.string().optional().describe("Filter by file extension (e.g. 'gd', 'tscn'). Default: all text files"),
max_results: z.number().optional().describe("Maximum results to return (default: 50)"),
}, async (params) => {
try {
const result = await godot.sendCommand("search_in_files", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_project_settings", "Read project.godot settings by section or specific key", {
section: z.string().optional().describe("Settings section prefix (e.g. 'display/window')"),
key: z.string().optional().describe("Specific setting key (e.g. 'display/window/size/viewport_width')"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_project_settings", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("set_project_setting", "Set a project setting value (e.g. viewport size, main scene). Saves to project.godot via the editor API.", {
key: z.string().describe("Setting key (e.g. 'display/window/size/viewport_width', 'application/run/main_scene')"),
value: z.union([z.string(), z.number(), z.boolean()]).describe("Value to set. Strings are auto-parsed for Vector2, bool, int, float."),
}, async (params) => {
try {
const result = await godot.sendCommand("set_project_setting", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("uid_to_project_path", "Convert a Godot UID (uid://...) to a project resource path (res://...)", {
uid: z.string().describe("The UID string (e.g. 'uid://abc123')"),
}, async (params) => {
try {
const result = await godot.sendCommand("uid_to_project_path", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("project_path_to_uid", "Convert a project resource path (res://...) to its UID (uid://...)", {
path: z.string().describe("The resource path (e.g. 'res://scenes/player.tscn')"),
}, async (params) => {
try {
const result = await godot.sendCommand("project_path_to_uid", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("add_autoload", "Add an autoload (singleton) to the project. The script/scene will be auto-loaded when the project starts.", {
name: z.string().describe("Autoload name (e.g. 'GameManager', 'AudioManager')"),
path: z.string().describe("Path to the script or scene file (e.g. 'res://scripts/autoload/game_manager.gd')"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_autoload", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("remove_autoload", "Remove an autoload (singleton) from the project settings", {
name: z.string().describe("Autoload name to remove (e.g. 'GameManager')"),
}, async (params) => {
try {
const result = await godot.sendCommand("remove_autoload", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=project-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerResourceTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=resource-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"resource-tools.d.ts","sourceRoot":"","sources":["../../src/tools/resource-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CAoFN"}

View File

@@ -0,0 +1,69 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerResourceTools(server, godot) {
server.tool("read_resource", "Read a .tres resource file and return its properties. Works with any Godot Resource type (StyleBox, Font, Theme, Material, etc.)", {
path: z.string().describe("Path to the resource file (e.g. 'res://themes/main_theme.tres')"),
}, async (params) => {
try {
const result = await godot.sendCommand("read_resource", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("edit_resource", "Edit properties of an existing .tres resource file. Changes are saved to disk immediately.", {
path: z.string().describe("Path to the resource file (e.g. 'res://themes/main_theme.tres')"),
properties: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).describe("Properties to set as key-value pairs. Values auto-parsed for Vector2, Color, etc."),
}, async (params) => {
try {
const result = await godot.sendCommand("edit_resource", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("create_resource", "Create a new .tres resource file of a given type with optional initial properties", {
path: z.string().describe("Path to save the resource (e.g. 'res://resources/player_stats.tres')"),
type: z.string().describe("Resource type to create (e.g. 'StyleBoxFlat', 'LabelSettings', 'Environment')"),
properties: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe("Initial properties to set"),
overwrite: z.boolean().optional().describe("Overwrite if file exists (default: false)"),
}, async (params) => {
try {
const result = await godot.sendCommand("create_resource", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_resource_preview", "Get a visual preview of an image or texture resource as a PNG. Works with .png, .jpg, .webp, .svg image files and Texture2D resources.", {
path: z.string().describe("Path to the resource (e.g. 'res://assets/player.png', 'res://icon.svg')"),
max_size: z.number().optional().describe("Maximum width/height in pixels, preserving aspect ratio (default: 256)"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_resource_preview", params);
if (result && typeof result === "object" && "image_base64" in result) {
return {
content: [
{
type: "image",
data: result.image_base64,
mimeType: "image/png",
},
{
type: "text",
text: `Preview of ${result.path}: ${result.width}x${result.height}`,
},
],
};
}
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=resource-tools.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"resource-tools.js","sourceRoot":"","sources":["../../src/tools/resource-tools.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerRuntimeTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=runtime-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"runtime-tools.d.ts","sourceRoot":"","sources":["../../src/tools/runtime-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAIzD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA6vBN"}

View File

@@ -0,0 +1,552 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
import { coerceStringArray, coerceNumber } from "../utils/zod-coerce.js";
export function registerRuntimeTools(server, godot) {
server.tool("get_game_scene_tree", "Get the scene tree of the currently running game (requires a scene to be playing). Supports filtering by script path, node type, or name.", {
max_depth: z
.number()
.optional()
.describe("Maximum tree depth (-1 for unlimited, default: -1)"),
script_filter: z
.string()
.optional()
.describe("Only include nodes whose script path contains this string (e.g. 'enemy' matches 'enemy.gd', 'enemy_drone.gd')"),
type_filter: z
.string()
.optional()
.describe("Only include nodes of this Godot class (e.g. 'CharacterBody2D', 'Area2D')"),
named_only: z
.boolean()
.optional()
.describe("If true, exclude nodes with auto-generated names (names starting with '@'). Default: false"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_game_scene_tree", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("get_game_node_properties", "Get properties of a node in the running game (requires a scene to be playing)", {
node_path: z
.string()
.describe("Absolute node path in the running game (e.g. '/root/Main/Player')"),
properties: coerceStringArray()
.optional()
.describe("Specific property names to read (default: all editor-visible properties)"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_game_node_properties", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("set_game_node_property", "Set a property on a node in the running game (requires a scene to be playing). Useful for live-tweaking values like position, speed, health, etc.", {
node_path: z
.string()
.describe("Absolute node path in the running game (e.g. '/root/Main/Player')"),
property: z
.string()
.describe("Property name to set (e.g. 'position', 'speed', 'health')"),
value: z
.union([z.string(), z.number(), z.boolean(), z.record(z.string(), z.number())])
.describe("Value to set. Accepts: strings with auto-parsing ('Vector2(100,200)', '#ff0000'), numbers, booleans, or JSON objects for vectors/colors ({\"x\":5,\"y\":3,\"z\":10} for Vector3, {\"x\":100,\"y\":200} for Vector2, {\"r\":1,\"g\":0,\"b\":0,\"a\":1} for Color)"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_game_node_property", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("execute_game_script", "Execute arbitrary GDScript code inside the running game process. Use _mcp_print() to output values. Has access to the live scene tree and all game nodes.", {
code: z
.string()
.describe("GDScript code to execute in the running game. Use _mcp_print(value) to capture output. Code runs inside a run() function with access to the live game scene tree."),
}, async (params) => {
try {
const result = await godot.sendCommand("execute_game_script", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("capture_frames", "Capture multiple screenshots at regular frame intervals from the running game. Returns base64 PNG images. Use this to verify animations are playing correctly — if character poses differ across frames, the animation is working; if all frames show the same pose (e.g. T-pose), animation loading failed. Also useful for verifying movement, physics, and any time-based behavior. Prefer this over get_game_screenshot when you need to confirm something is changing over time.", {
count: coerceNumber()
.optional()
.describe("Number of frames to capture (1-30, default: 5)"),
frame_interval: coerceNumber()
.optional()
.describe("Frames to wait between captures (default: 10, i.e. ~6 captures/sec at 60fps)"),
half_resolution: z
.boolean()
.optional()
.describe("Halve resolution to reduce data size (default: true)"),
node_data: z
.object({
node_path: z.string().describe("Path to a node to track (e.g. '/root/Main/Player')"),
properties: coerceStringArray().describe("Property names to capture per frame (e.g. ['global_position', 'velocity'])"),
})
.optional()
.describe("Optional: capture node property data alongside each frame for debugging (position, velocity, etc.)"),
}, async (params) => {
try {
const result = (await godot.sendCommand("capture_frames", params));
if (result &&
typeof result === "object" &&
"frames" in result &&
Array.isArray(result.frames)) {
const content = [];
const frameData = Array.isArray(result.frame_data) ? result.frame_data : null;
for (let i = 0; i < result.frames.length; i++) {
content.push({
type: "image",
data: result.frames[i],
mimeType: "image/png",
});
if (frameData && frameData[i]) {
content.push({
type: "text",
text: `Frame ${i + 1}: ${JSON.stringify(frameData[i])}`,
});
}
}
content.push({
type: "text",
text: `Captured ${result.count} frames (${result.width}x${result.height}${result.half_resolution ? ", half-res" : ""})`,
});
return { content };
}
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("record_frames", "Record many screenshots to files on disk for long-running debug observation. Unlike capture_frames (which returns base64 images directly), this saves PNG files to user://mcp_recorded_frames/ and returns file paths. Use this when you need more than 30 frames or want to observe behavior over a longer period without flooding the context with image data.", {
count: coerceNumber()
.optional()
.describe("Number of frames to capture (1-600, default: 30)"),
frame_interval: coerceNumber()
.optional()
.describe("Frames to wait between captures (default: 10, i.e. ~6 captures/sec at 60fps)"),
half_resolution: z
.boolean()
.optional()
.describe("Halve resolution to reduce file size (default: true)"),
node_data: z
.object({
node_path: z.string().describe("Path to a node to track (e.g. '/root/Main/Player')"),
properties: coerceStringArray().describe("Property names to capture per frame (e.g. ['global_position', 'velocity'])"),
})
.optional()
.describe("Optional: capture node property data alongside each frame for debugging"),
}, async (params) => {
try {
const result = (await godot.sendCommand("record_frames", params));
const content = [];
content.push({
type: "text",
text: `Recorded ${result.count} frames to ${result.directory}/ (${result.width}x${result.height}${result.half_resolution ? ", half-res" : ""})`,
});
if (Array.isArray(result.files)) {
content.push({
type: "text",
text: `Files:\n${result.files.join("\n")}`,
});
}
if (Array.isArray(result.frame_data) && result.frame_data.length > 0) {
content.push({
type: "text",
text: `Node data:\n${JSON.stringify(result.frame_data, null, 2)}`,
});
}
return { content };
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("monitor_properties", "Record property values over multiple frames from the running game. Returns a timeline of samples. Great for verifying movement (position changing), animation state (current_animation property), physics behavior (velocity), and debugging time-dependent issues.", {
node_path: z
.string()
.describe("Absolute node path in the running game (e.g. '/root/Main/Player')"),
properties: coerceStringArray()
.describe("Property names to monitor (e.g. ['position', 'velocity'])"),
frame_count: coerceNumber()
.optional()
.describe("Number of samples to collect (1-600, default: 60)"),
frame_interval: coerceNumber()
.optional()
.describe("Frames to wait between samples (default: 1, every frame)"),
}, async (params) => {
try {
const result = await godot.sendCommand("monitor_properties", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("watch_signals", "Monitor signal emissions on specified nodes in the running game for a duration. Returns a timestamped log of every signal fired — great for debugging event flow, verifying signal connections, and understanding runtime behavior.", {
node_paths: z
.array(z.string())
.describe("Absolute node paths to watch (e.g. ['/root/Main/Player', '/root/Main/Enemy'])"),
signal_filter: z
.array(z.string())
.optional()
.describe("Only watch signals containing these substrings (e.g. ['health', 'died']). Omit to watch all signals."),
duration_ms: coerceNumber()
.optional()
.describe("How long to watch in milliseconds (500-30000, default: 5000)"),
}, async (params) => {
try {
const result = await godot.sendCommand("watch_signals", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("start_recording", "Start recording all input events (keyboard, mouse, actions) in the running game. Use stop_recording to get the recorded events.", {}, async () => {
try {
const result = await godot.sendCommand("start_recording");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("stop_recording", "Stop recording input events and return the recorded event timeline. Events include timestamps for replay.", {}, async () => {
try {
const result = await godot.sendCommand("stop_recording");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("replay_recording", "Replay a previously recorded input event sequence in the running game. Useful for regression testing — record a test once, replay it after code changes.", {
events: z.array(z.record(z.string(), z.unknown())).describe("Array of recorded event objects (from stop_recording output)"),
speed: z.number().optional().describe("Playback speed multiplier (default: 1.0, 2.0 = double speed)"),
}, async (params) => {
try {
const result = await godot.sendCommand("replay_recording", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("find_nodes_by_script", "Find all nodes in the running game whose script path contains a given string. Returns matching nodes with their properties.", {
script: z
.string()
.describe("Script path substring to search for (e.g. 'enemy', 'player.gd')"),
properties: coerceStringArray()
.optional()
.describe("Specific property names to include for each match (default: all editor-visible properties)"),
}, async (params) => {
try {
const result = await godot.sendCommand("find_nodes_by_script", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("get_autoload", "Get properties of an autoload/singleton node in the running game. Quick access to global game state like GameManager, EventBus, etc.", {
name: z
.string()
.describe("Autoload name (e.g. 'GameManager', 'EventBus', 'SaveManager')"),
properties: coerceStringArray()
.optional()
.describe("Specific property names to read (default: all editor-visible properties)"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_autoload", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("find_ui_elements", "Find all visible UI elements (Button, Label, LineEdit, CheckBox, Slider, etc.) in the running game. Returns each element's text, type, position, and center point for clicking.", {
type_filter: z
.string()
.optional()
.describe("Only return elements of this type (e.g. 'Button', 'Label', 'CheckBox'). Default: all types"),
}, async (params) => {
try {
const result = await godot.sendCommand("find_ui_elements", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("click_button_by_text", "Click a button in the running game by its text label. Finds the button, calculates its center, and simulates a full click (press + release). Much easier than manual coordinate-based clicking.", {
text: z
.string()
.describe("Button text to search for (e.g. 'New Game', 'Start', 'OK')"),
partial: z
.boolean()
.optional()
.describe("Allow partial text matching (default: true). If false, requires exact match."),
}, async (params) => {
try {
const result = await godot.sendCommand("click_button_by_text", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("wait_for_node", "Wait until a node exists at the given path in the running game scene tree. Useful for waiting after scene transitions, node spawning, or UI state changes.", {
node_path: z
.string()
.describe("Absolute node path to wait for (e.g. '/root/Main/Player', '/root/Dungeon')"),
timeout: z
.number()
.optional()
.describe("Maximum seconds to wait (default: 5.0)"),
poll_frames: z
.number()
.optional()
.describe("Frames between each check (default: 5, i.e. ~12 checks/sec at 60fps)"),
}, async (params) => {
try {
const result = await godot.sendCommand("wait_for_node", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("find_nearby_nodes", "Find all nodes within a radius of a position in the running game, sorted by distance. Useful for finding what's near the player (collectibles, enemies, interactables) without manually querying each node's position.", {
position: z
.union([z.string(), z.object({ x: z.number(), y: z.number(), z: z.number().optional() })])
.describe("Origin position: either a node_path string (e.g. '/root/Main/Player') to use that node's global_position, or an {x, y, z} coordinate object"),
radius: coerceNumber()
.optional()
.describe("Search radius in world units (default: 20.0)"),
type_filter: z
.string()
.optional()
.describe("Only include nodes of this Godot class (e.g. 'Area3D', 'CharacterBody3D')"),
group_filter: z
.string()
.optional()
.describe("Only include nodes in this group (e.g. 'enemies', 'collectibles')"),
max_results: coerceNumber()
.optional()
.describe("Maximum number of results to return (default: 10)"),
}, async (params) => {
try {
const result = await godot.sendCommand("find_nearby_nodes", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("navigate_to", "Calculate navigation info from the player to a target in the running 3D game. Returns the world direction, camera-relative suggested WASD keys to press, camera yaw rotation needed (as mouse relative_x pixels for simulate_mouse_move), and estimated walk duration. Use this to plan movement instead of manually calculating directions.", {
target: z
.union([z.string(), z.object({ x: z.number(), y: z.number(), z: z.number() })])
.describe("Target: either a node_path string (e.g. '/root/Main/Crystal') or an {x, y, z} coordinate object"),
player_path: z
.string()
.optional()
.describe("Player node path (default: '/root/Main/Player')"),
camera_path: z
.string()
.optional()
.describe("Camera node path (default: auto-detect active Camera3D)"),
move_speed: coerceNumber()
.optional()
.describe("Player movement speed in units/sec for duration estimation (default: 5.0)"),
}, async (params) => {
try {
const result = await godot.sendCommand("navigate_to", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("move_to", "Autopilot the player character to walk to a target position in the running 3D game. Handles camera rotation and forward movement internally at 60fps — completes in a single call with no manual simulate_key/simulate_mouse_move loops needed. The player's camera pivot is directly rotated toward the target, and W key is injected to walk. Much more reliable and efficient than navigate_to + manual input simulation.", {
target: z
.union([z.string(), z.object({ x: z.number(), y: z.number(), z: z.number() })])
.describe("Target: either a node_path string (e.g. '/root/Main/Crystal') or an {x, y, z} coordinate object"),
player_path: z
.string()
.optional()
.describe("Player node path (default: '/root/Main/Player')"),
camera_path: z
.string()
.optional()
.describe("Camera pivot node path (default: auto-detect SpringArm3D child of player, or active Camera3D parent)"),
arrival_radius: coerceNumber()
.optional()
.describe("Stop when this close to target in world units (default: 1.5)"),
timeout: coerceNumber()
.optional()
.describe("Maximum seconds before giving up (default: 15.0)"),
run: z
.boolean()
.optional()
.describe("Hold Shift for running speed (default: false)"),
look_at_target: z
.boolean()
.optional()
.describe("Rotate camera toward target while moving (default: true). Set false to walk forward without turning."),
}, async (params) => {
try {
const result = await godot.sendCommand("move_to", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
server.tool("batch_get_properties", "Get properties of multiple nodes at once in the running game. More efficient than calling get_game_node_properties multiple times.", {
nodes: z
.array(z.object({
path: z
.string()
.describe("Absolute node path (e.g. '/root/Main/Player')"),
properties: coerceStringArray()
.optional()
.describe("Specific properties to read (default: all editor-visible)"),
}))
.describe("Array of nodes to query"),
}, async (params) => {
try {
const result = await godot.sendCommand("batch_get_properties", params);
return {
content: [
{ type: "text", text: JSON.stringify(result, null, 2) },
],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
}
//# sourceMappingURL=runtime-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerScene3DTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=scene-3d-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"scene-3d-tools.d.ts","sourceRoot":"","sources":["../../src/tools/scene-3d-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CA4dN"}

View File

@@ -0,0 +1,405 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerScene3DTools(server, godot) {
// ─── 1. add_mesh_instance ──────────────────────────────────────────────
server.tool("add_mesh_instance", "Add a MeshInstance3D node with a primitive mesh (Box, Sphere, Cylinder, Capsule, Plane, Prism, Torus, Quad) or load a 3D model file (.glb/.gltf/.obj). Set position, rotation, scale, and mesh-specific properties.", {
mesh_type: z
.string()
.optional()
.describe("Primitive mesh type: BoxMesh, SphereMesh, CylinderMesh, CapsuleMesh, PlaneMesh, PrismMesh, TorusMesh, QuadMesh"),
mesh_file: z
.string()
.optional()
.describe("Path to a 3D model file (res://path/to/model.glb, .gltf, .obj). Use instead of mesh_type for imported models"),
parent_path: z
.string()
.optional()
.describe("Parent node path (default: root '.')"),
name: z.string().optional().describe("Node name (default: MeshInstance3D)"),
position: z
.any()
.optional()
.describe("Position as Vector3 string 'Vector3(x,y,z)', object {x,y,z}, or array [x,y,z]"),
rotation: z
.any()
.optional()
.describe("Rotation in degrees as Vector3 string, object {x,y,z}, or array [x,y,z]"),
scale: z
.any()
.optional()
.describe("Scale as Vector3 string, object {x,y,z}, or array [x,y,z]"),
mesh_properties: z
.record(z.string(), z.any())
.optional()
.describe("Properties to set on the mesh resource (e.g. {\"size\": \"Vector3(2,1,2)\"} for BoxMesh)"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_mesh_instance", params);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
// ─── 2. setup_lighting ─────────────────────────────────────────────────
server.tool("setup_lighting", "Add a light node (DirectionalLight3D, OmniLight3D, SpotLight3D) to the scene. Supports preset configurations: 'sun' (directional with shadows), 'indoor' (warm omni), 'dramatic' (focused spot with shadows).", {
light_type: z
.string()
.optional()
.describe("Light type: DirectionalLight3D, OmniLight3D, SpotLight3D. Not needed if preset is specified"),
preset: z
.string()
.optional()
.describe("Preset configuration: 'sun' (directional, shadows, -45deg), 'indoor' (warm omni, range 8), 'dramatic' (spot, high energy, shadows)"),
parent_path: z
.string()
.optional()
.describe("Parent node path (default: root '.')"),
name: z.string().optional().describe("Node name"),
color: z
.any()
.optional()
.describe("Light color as Color string or hex (default: white)"),
energy: z
.number()
.optional()
.describe("Light energy/intensity (default: 1.0)"),
shadows: z
.boolean()
.optional()
.describe("Enable shadow casting (default: false, true for sun/dramatic presets)"),
range: z
.number()
.optional()
.describe("Range for OmniLight3D/SpotLight3D (default: 5.0)"),
attenuation: z
.number()
.optional()
.describe("Attenuation for OmniLight3D/SpotLight3D (default: 1.0)"),
spot_angle: z
.number()
.optional()
.describe("Spot angle in degrees for SpotLight3D (default: 45.0)"),
spot_angle_attenuation: z
.number()
.optional()
.describe("Spot angle attenuation for SpotLight3D (default: 1.0)"),
position: z
.any()
.optional()
.describe("Position as Vector3 string, object {x,y,z}, or array [x,y,z]"),
rotation: z
.any()
.optional()
.describe("Rotation in degrees as Vector3 string, object {x,y,z}, or array [x,y,z]"),
}, async (params) => {
try {
const result = await godot.sendCommand("setup_lighting", params);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
// ─── 3. set_material_3d ────────────────────────────────────────────────
server.tool("set_material_3d", "Create and apply a StandardMaterial3D to a MeshInstance3D. Configure PBR properties: albedo color/texture, metallic, roughness, emission, transparency, normal maps.", {
node_path: z
.string()
.describe("Path to the MeshInstance3D node"),
surface_index: z
.number()
.optional()
.describe("Surface index to apply material to (default: 0)"),
albedo_color: z
.any()
.optional()
.describe("Albedo color as Color string 'Color(r,g,b,a)', hex '#ff0000', or object {r,g,b,a}"),
albedo_texture: z
.string()
.optional()
.describe("Path to albedo texture (res://path/to/texture.png)"),
metallic: z
.number()
.optional()
.describe("Metallic value 0.0-1.0 (default: 0.0)"),
roughness: z
.number()
.optional()
.describe("Roughness value 0.0-1.0 (default: 1.0)"),
metallic_texture: z
.string()
.optional()
.describe("Path to metallic texture"),
roughness_texture: z
.string()
.optional()
.describe("Path to roughness texture"),
normal_texture: z
.string()
.optional()
.describe("Path to normal map texture (auto-enables normal mapping)"),
emission: z
.any()
.optional()
.describe("Emission color (auto-enables emission). Color string or hex"),
emission_color: z
.any()
.optional()
.describe("Alias for emission"),
emission_energy: z
.number()
.optional()
.describe("Emission energy multiplier (default: 1.0)"),
emission_texture: z
.string()
.optional()
.describe("Path to emission texture"),
transparency: z
.string()
.optional()
.describe("Transparency mode: DISABLED, ALPHA, ALPHA_SCISSOR, ALPHA_HASH, ALPHA_DEPTH_PRE_PASS"),
cull_mode: z
.string()
.optional()
.describe("Cull mode: BACK, FRONT, DISABLED"),
}, async (params) => {
try {
const result = await godot.sendCommand("set_material_3d", params);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
// ─── 4. setup_environment ──────────────────────────────────────────────
server.tool("setup_environment", "Add or configure a WorldEnvironment node with sky, ambient light, tonemap, fog, glow, SSAO, SSR, and SDFGI settings.", {
parent_path: z
.string()
.optional()
.describe("Parent node path (default: root '.')"),
node_path: z
.string()
.optional()
.describe("Path to an existing WorldEnvironment to modify instead of creating a new one"),
name: z
.string()
.optional()
.describe("Node name (default: WorldEnvironment)"),
background_mode: z
.string()
.optional()
.describe("Background mode: 'sky', 'color', 'canvas', 'clear_color' (default: sky)"),
background_color: z
.any()
.optional()
.describe("Background color when mode is 'color'"),
sky: z
.object({
sky_top_color: z.any().optional().describe("Sky top color"),
sky_horizon_color: z.any().optional().describe("Sky horizon color"),
ground_bottom_color: z.any().optional().describe("Ground bottom color"),
ground_horizon_color: z
.any()
.optional()
.describe("Ground horizon color"),
sun_angle_max: z
.number()
.optional()
.describe("Maximum sun angle in degrees"),
sky_curve: z
.number()
.optional()
.describe("Sky color curve (0.0-1.0)"),
})
.optional()
.describe("ProceduralSkyMaterial settings"),
ambient_light_color: z.any().optional().describe("Ambient light color"),
ambient_light_energy: z
.number()
.optional()
.describe("Ambient light energy"),
ambient_light_source: z
.string()
.optional()
.describe("Ambient light source: BACKGROUND, DISABLED, COLOR, SKY"),
tonemap_mode: z
.string()
.optional()
.describe("Tonemap mode: LINEAR, REINHARDT, FILMIC, ACES"),
tonemap_exposure: z.number().optional().describe("Tonemap exposure"),
tonemap_white: z.number().optional().describe("Tonemap white point"),
fog_enabled: z.boolean().optional().describe("Enable volumetric fog"),
fog_light_color: z.any().optional().describe("Fog light color"),
fog_density: z.number().optional().describe("Fog density"),
fog_light_energy: z.number().optional().describe("Fog light energy"),
glow_enabled: z.boolean().optional().describe("Enable glow/bloom"),
glow_intensity: z.number().optional().describe("Glow intensity"),
glow_strength: z.number().optional().describe("Glow strength"),
glow_bloom: z.number().optional().describe("Glow bloom amount"),
ssao_enabled: z
.boolean()
.optional()
.describe("Enable Screen-Space Ambient Occlusion"),
ssao_radius: z.number().optional().describe("SSAO radius"),
ssao_intensity: z.number().optional().describe("SSAO intensity"),
ssr_enabled: z
.boolean()
.optional()
.describe("Enable Screen-Space Reflections"),
ssr_max_steps: z.number().optional().describe("SSR max steps"),
ssr_fade_in: z.number().optional().describe("SSR fade in"),
ssr_fade_out: z.number().optional().describe("SSR fade out"),
sdfgi_enabled: z
.boolean()
.optional()
.describe("Enable Signed Distance Field Global Illumination"),
}, async (params) => {
try {
const result = await godot.sendCommand("setup_environment", params);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
// ─── 5. setup_camera_3d ────────────────────────────────────────────────
server.tool("setup_camera_3d", "Add or configure a Camera3D node. Set projection mode, FOV, near/far planes, position, rotation, look-at target, and cull mask.", {
parent_path: z
.string()
.optional()
.describe("Parent node path (default: root '.')"),
node_path: z
.string()
.optional()
.describe("Path to an existing Camera3D to configure instead of creating a new one"),
name: z.string().optional().describe("Node name (default: Camera3D)"),
projection: z
.string()
.optional()
.describe("Projection mode: 'perspective', 'orthogonal'/'orthographic', 'frustum'"),
fov: z
.number()
.optional()
.describe("Field of view in degrees for perspective (default: 75)"),
size: z
.number()
.optional()
.describe("View size for orthogonal projection"),
near: z
.number()
.optional()
.describe("Near clipping plane (default: 0.05)"),
far: z
.number()
.optional()
.describe("Far clipping plane (default: 4000)"),
cull_mask: z
.number()
.optional()
.describe("Cull mask as integer bitmask"),
current: z
.boolean()
.optional()
.describe("Make this the current/active camera (default: false)"),
position: z
.any()
.optional()
.describe("Position as Vector3 (default: (0, 1, 3) for new cameras)"),
rotation: z
.any()
.optional()
.describe("Rotation in degrees as Vector3"),
look_at: z
.any()
.optional()
.describe("Target position to look at as Vector3 (overrides rotation)"),
environment_path: z
.string()
.optional()
.describe("Path to an Environment resource for camera-specific environment override"),
}, async (params) => {
try {
const result = await godot.sendCommand("setup_camera_3d", params);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
// ─── 6. add_gridmap ────────────────────────────────────────────────────
server.tool("add_gridmap", "Add or configure a GridMap node with a MeshLibrary. Optionally set cells at specific grid positions with item IDs and orientations.", {
parent_path: z
.string()
.optional()
.describe("Parent node path (default: root '.')"),
node_path: z
.string()
.optional()
.describe("Path to an existing GridMap to configure instead of creating a new one"),
name: z.string().optional().describe("Node name (default: GridMap)"),
mesh_library_path: z
.string()
.optional()
.describe("Path to a MeshLibrary resource (res://path/to/library.meshlib or .tres)"),
cell_size: z
.any()
.optional()
.describe("Cell size as Vector3 (default: (2, 2, 2))"),
position: z.any().optional().describe("GridMap position as Vector3"),
cells: z
.array(z.object({
x: z.number().describe("Cell X coordinate"),
y: z.number().describe("Cell Y coordinate"),
z: z.number().describe("Cell Z coordinate"),
item: z
.number()
.optional()
.describe("MeshLibrary item index (default: 0)"),
orientation: z
.number()
.optional()
.describe("Cell orientation index (default: 0)"),
}))
.optional()
.describe("Array of cells to set with grid positions and item IDs"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_gridmap", params);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
catch (e) {
return {
content: [{ type: "text", text: formatErrorForMcp(e) }],
isError: true,
};
}
});
}
//# sourceMappingURL=scene-3d-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerSceneTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=scene-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"scene-tools.d.ts","sourceRoot":"","sources":["../../src/tools/scene-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CAkKN"}

View File

@@ -0,0 +1,117 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerSceneTools(server, godot) {
server.tool("get_scene_tree", "Get the live scene tree of the currently edited scene, showing all nodes, types, and hierarchy", {
max_depth: z.number().optional().describe("Max tree depth to return (-1 for unlimited)"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_scene_tree", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_scene_file_content", "Read the raw .tscn file content of a scene", {
path: z.string().describe("Path to the scene file (e.g. 'res://scenes/main.tscn')"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_scene_file_content", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("create_scene", "Create a new scene file with a specified root node type", {
path: z.string().describe("Path for the new scene (e.g. 'res://scenes/enemy.tscn')"),
root_type: z.string().optional().describe("Root node type (default: Node2D). Examples: Node2D, Node3D, Control, CharacterBody2D"),
root_name: z.string().optional().describe("Root node name (defaults to filename)"),
}, async (params) => {
try {
const result = await godot.sendCommand("create_scene", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("open_scene", "Open a scene file in the Godot editor", {
path: z.string().describe("Path to the scene file (e.g. 'res://scenes/main.tscn')"),
}, async (params) => {
try {
const result = await godot.sendCommand("open_scene", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("delete_scene", "Delete a scene file from the project", {
path: z.string().describe("Path to the scene file to delete"),
}, async (params) => {
try {
const result = await godot.sendCommand("delete_scene", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("add_scene_instance", "Add an existing scene as a child node (instancing) in the current scene", {
scene_path: z.string().describe("Path to the scene to instance (e.g. 'res://scenes/enemy.tscn')"),
parent_path: z.string().optional().describe("Parent node path (default: root '.')"),
name: z.string().optional().describe("Custom name for the instance"),
}, async (params) => {
try {
const result = await godot.sendCommand("add_scene_instance", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("play_scene", "Run a scene in the Godot editor (main scene, current scene, or specific path)", {
mode: z.string().optional().describe("'main' (default), 'current', or a scene file path"),
}, async (params) => {
try {
const result = await godot.sendCommand("play_scene", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("stop_scene", "Stop the currently playing scene", {}, async () => {
try {
const result = await godot.sendCommand("stop_scene");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("save_scene", "Save the currently edited scene to disk", {
path: z.string().optional().describe("Optional path to save to (defaults to current scene path)"),
}, async (params) => {
try {
const result = await godot.sendCommand("save_scene", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_scene_exports", "Get all @export variables from all scripted nodes in a scene file. Useful for inspecting configurable parameters without opening the scene.", {
path: z.string().describe("Path to the scene file (e.g. 'res://scenes/enemy.tscn')"),
}, async (params) => {
try {
const result = await godot.sendCommand("get_scene_exports", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=scene-tools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { GodotConnection } from "../godot-connection.js";
export declare function registerScriptTools(server: McpServer, godot: GodotConnection): void;
//# sourceMappingURL=script-tools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"script-tools.d.ts","sourceRoot":"","sources":["../../src/tools/script-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAGzD,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,eAAe,GACrB,IAAI,CAoIN"}

View File

@@ -0,0 +1,100 @@
import { z } from "zod";
import { formatErrorForMcp } from "../utils/errors.js";
export function registerScriptTools(server, godot) {
server.tool("list_scripts", "List all GDScript/C#/shader files in the project with class info", {
path: z.string().optional().describe("Root path to search (default: res://)"),
recursive: z.boolean().optional().describe("Search recursively (default: true)"),
}, async (params) => {
try {
const result = await godot.sendCommand("list_scripts", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("read_script", "Read the full content of a GDScript file", {
path: z.string().describe("Path to the script (e.g. 'res://scripts/player.gd')"),
}, async (params) => {
try {
const result = await godot.sendCommand("read_script", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("create_script", "Create a new GDScript file with optional content or auto-generated template. Restricted to .gd/.cs paths. Refuses to overwrite a script that is currently open in Godot's script editor unless force=true is set.", {
path: z.string().describe("Path for the new script (e.g. 'res://scripts/enemy_ai.gd'). Must be a .gd or .cs file."),
content: z.string().optional().describe("Full script content. If empty, generates a template."),
extends: z.string().optional().describe("Base class (default: 'Node'). Only used for template generation."),
class_name: z.string().optional().describe("Class name to add. Only used for template generation."),
force: z.boolean().optional().describe("Override the open-script-editor guard and write anyway. Use only when no editor buffer holds unsaved changes for the target path."),
}, async (params) => {
try {
const result = await godot.sendCommand("create_script", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("edit_script", "Edit a script using search-and-replace, full content replacement, line insertion, or 1-based inclusive line-range replacement. Restricted to .gd/.cs paths. Refuses to write a script that is currently open in Godot's script editor unless force=true is set.", {
path: z.string().describe("Path to the script to edit. Must be a .gd or .cs file."),
replacements: z
.array(z.object({
search: z.string().describe("Text to find"),
replace: z.string().describe("Replacement text"),
regex: z.boolean().optional().describe("Use regex for search (default: false)"),
}))
.optional()
.describe("Array of search-and-replace operations"),
content: z.string().optional().describe("Full replacement content (replaces entire file), or replacement lines when combined with start_line/end_line"),
insert_at_line: z.number().optional().describe("Line number to insert text at (0-indexed)"),
text: z.string().optional().describe("Text to insert (used with insert_at_line)"),
start_line: z.number().optional().describe("1-based inclusive starting line for range replacement (used with content)"),
end_line: z.number().optional().describe("1-based inclusive ending line for range replacement (defaults to start_line)"),
force: z.boolean().optional().describe("Override the open-script-editor guard and write anyway."),
}, async (params) => {
try {
const result = await godot.sendCommand("edit_script", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("attach_script", "Attach a GDScript to a node in the current scene", {
node_path: z.string().describe("Path to the target node"),
script_path: z.string().describe("Path to the script file (e.g. 'res://scripts/player.gd')"),
}, async (params) => {
try {
const result = await godot.sendCommand("attach_script", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("validate_script", "Validate a GDScript file by attempting to compile it. Returns whether the script is valid. Use get_output_log or get_editor_errors for detailed error messages on failure.", {
path: z.string().describe("Path to the script to validate (e.g. 'res://scripts/player.gd')"),
}, async (params) => {
try {
const result = await godot.sendCommand("validate_script", params);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
server.tool("get_open_scripts", "Get a list of scripts currently open in the Godot script editor", {}, async () => {
try {
const result = await godot.sendCommand("get_open_scripts");
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
catch (e) {
return { content: [{ type: "text", text: formatErrorForMcp(e) }], isError: true };
}
});
}
//# sourceMappingURL=script-tools.js.map

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More