index.ts•91 kB
#!/usr/bin/env node
/**
* Godot MCP Server
*
* This MCP server provides tools for interacting with the Godot game engine.
* It enables AI assistants to launch the Godot editor, run Godot projects,
* capture debug output, and control project execution.
*/
import { fileURLToPath } from 'url';
import { join, dirname, basename, normalize } from 'path';
import { existsSync, readdirSync, mkdirSync } from 'fs';
import { spawn } from 'child_process';
import { promisify } from 'util';
import { exec } from 'child_process';
import { Socket } from 'net';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
// Check if debug mode is enabled
const DEBUG_MODE: boolean = process.env.DEBUG === 'true';
const GODOT_DEBUG_MODE: boolean = true; // Always use GODOT DEBUG MODE
const execAsync = promisify(exec);
// Derive __filename and __dirname in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Interface representing a running Godot process
*/
interface GodotProcess {
process: any;
output: string[];
errors: string[];
}
/**
* Interface for server configuration
*/
interface GodotServerConfig {
godotPath?: string;
debugMode?: boolean;
godotDebugMode?: boolean;
strictPathValidation?: boolean; // New option to control path validation behavior
}
/**
* Interface for operation parameters
*/
interface OperationParams {
[key: string]: any;
}
/**
* Interface representing a remote debug connection to Godot editor
*/
interface RemoteDebugConnection {
socket: Socket;
output: string[];
errors: string[];
rawData: string[]; // Store raw binary data for debugging
connected: boolean;
host: string;
port: number;
sequenceNumber: number; // For tracking DAP request/response pairs
pendingRequests: Map<number, { resolve: (value: any) => void; reject: (error: any) => void }>;
}
/**
* Main server class for the Godot MCP server
*/
class GodotServer {
private server: Server;
private activeProcess: GodotProcess | null = null;
private godotPath: string | null = null;
private operationsScriptPath: string;
private validatedPaths: Map<string, boolean> = new Map();
private strictPathValidation: boolean = false;
private remoteDebugConnection: RemoteDebugConnection | null = null;
/**
* Parameter name mappings between snake_case and camelCase
* This allows the server to accept both formats
*/
private parameterMappings: Record<string, string> = {
'project_path': 'projectPath',
'scene_path': 'scenePath',
'root_node_type': 'rootNodeType',
'parent_node_path': 'parentNodePath',
'node_type': 'nodeType',
'node_name': 'nodeName',
'texture_path': 'texturePath',
'node_path': 'nodePath',
'output_path': 'outputPath',
'mesh_item_names': 'meshItemNames',
'new_path': 'newPath',
'file_path': 'filePath',
'save_path': 'savePath',
'directory': 'directory',
'recursive': 'recursive',
'scene': 'scene',
};
/**
* Reverse mapping from camelCase to snake_case
* Generated from parameterMappings for quick lookups
*/
private reverseParameterMappings: Record<string, string> = {};
constructor(config?: GodotServerConfig) {
// Initialize reverse parameter mappings
for (const [snakeCase, camelCase] of Object.entries(this.parameterMappings)) {
this.reverseParameterMappings[camelCase] = snakeCase;
}
// Apply configuration if provided
let debugMode = DEBUG_MODE;
let godotDebugMode = GODOT_DEBUG_MODE;
if (config) {
if (config.debugMode !== undefined) {
debugMode = config.debugMode;
}
if (config.godotDebugMode !== undefined) {
godotDebugMode = config.godotDebugMode;
}
if (config.strictPathValidation !== undefined) {
this.strictPathValidation = config.strictPathValidation;
}
// Store and validate custom Godot path if provided
if (config.godotPath) {
const normalizedPath = normalize(config.godotPath);
this.godotPath = normalizedPath;
this.logDebug(`Custom Godot path provided: ${this.godotPath}`);
// Validate immediately with sync check
if (!this.isValidGodotPathSync(this.godotPath)) {
console.warn(`[SERVER] Invalid custom Godot path provided: ${this.godotPath}`);
this.godotPath = null; // Reset to trigger auto-detection later
}
}
}
// Set the path to the operations script
this.operationsScriptPath = join(__dirname, 'scripts', 'godot_operations.gd');
if (debugMode) console.debug(`[DEBUG] Operations script path: ${this.operationsScriptPath}`);
// Initialize the MCP server
this.server = new Server(
{
name: 'godot-mcp',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// Set up tool handlers
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
// Cleanup on exit
process.on('SIGINT', async () => {
await this.cleanup();
process.exit(0);
});
}
/**
* Log debug messages if debug mode is enabled
*/
private logDebug(message: string): void {
if (DEBUG_MODE) {
console.debug(`[DEBUG] ${message}`);
}
}
/**
* Create a standardized error response with possible solutions
*/
private createErrorResponse(message: string, possibleSolutions: string[] = []): any {
// Log the error
console.error(`[SERVER] Error response: ${message}`);
if (possibleSolutions.length > 0) {
console.error(`[SERVER] Possible solutions: ${possibleSolutions.join(', ')}`);
}
const response: any = {
content: [
{
type: 'text',
text: message,
},
],
isError: true,
};
if (possibleSolutions.length > 0) {
response.content.push({
type: 'text',
text: 'Possible solutions:\n- ' + possibleSolutions.join('\n- '),
});
}
return response;
}
/**
* Validate a path to prevent path traversal attacks
*/
private validatePath(path: string): boolean {
// Basic validation to prevent path traversal
if (!path || path.includes('..')) {
return false;
}
// Add more validation as needed
return true;
}
/**
* Synchronous validation for constructor use
* This is a quick check that only verifies file existence, not executable validity
* Full validation will be performed later in detectGodotPath
* @param path Path to check
* @returns True if the path exists or is 'godot' (which might be in PATH)
*/
private isValidGodotPathSync(path: string): boolean {
try {
this.logDebug(`Quick-validating Godot path: ${path}`);
return path === 'godot' || existsSync(path);
} catch (error) {
this.logDebug(`Invalid Godot path: ${path}, error: ${error}`);
return false;
}
}
/**
* Validate if a Godot path is valid and executable
*/
private async isValidGodotPath(path: string): Promise<boolean> {
// Check cache first
if (this.validatedPaths.has(path)) {
return this.validatedPaths.get(path)!;
}
try {
this.logDebug(`Validating Godot path: ${path}`);
// Check if the file exists (skip for 'godot' which might be in PATH)
if (path !== 'godot' && !existsSync(path)) {
this.logDebug(`Path does not exist: ${path}`);
this.validatedPaths.set(path, false);
return false;
}
// Try to execute Godot with --version flag
const command = path === 'godot' ? 'godot --version' : `"${path}" --version`;
await execAsync(command);
this.logDebug(`Valid Godot path: ${path}`);
this.validatedPaths.set(path, true);
return true;
} catch (error) {
this.logDebug(`Invalid Godot path: ${path}, error: ${error}`);
this.validatedPaths.set(path, false);
return false;
}
}
/**
* Detect the Godot executable path based on the operating system
*/
private async detectGodotPath() {
// If godotPath is already set and valid, use it
if (this.godotPath && await this.isValidGodotPath(this.godotPath)) {
this.logDebug(`Using existing Godot path: ${this.godotPath}`);
return;
}
// Check environment variable next
if (process.env.GODOT_PATH) {
const normalizedPath = normalize(process.env.GODOT_PATH);
this.logDebug(`Checking GODOT_PATH environment variable: ${normalizedPath}`);
if (await this.isValidGodotPath(normalizedPath)) {
this.godotPath = normalizedPath;
this.logDebug(`Using Godot path from environment: ${this.godotPath}`);
return;
} else {
this.logDebug(`GODOT_PATH environment variable is invalid`);
}
}
// Auto-detect based on platform
const osPlatform = process.platform;
this.logDebug(`Auto-detecting Godot path for platform: ${osPlatform}`);
const possiblePaths: string[] = [
'godot', // Check if 'godot' is in PATH first
];
// Add platform-specific paths
if (osPlatform === 'darwin') {
possiblePaths.push(
'/Applications/Godot.app/Contents/MacOS/Godot',
'/Applications/Godot_4.app/Contents/MacOS/Godot',
`${process.env.HOME}/Applications/Godot.app/Contents/MacOS/Godot`,
`${process.env.HOME}/Applications/Godot_4.app/Contents/MacOS/Godot`,
`${process.env.HOME}/Library/Application Support/Steam/steamapps/common/Godot Engine/Godot.app/Contents/MacOS/Godot`
);
} else if (osPlatform === 'win32') {
possiblePaths.push(
'C:\\Program Files\\Godot\\Godot.exe',
'C:\\Program Files (x86)\\Godot\\Godot.exe',
'C:\\Program Files\\Godot_4\\Godot.exe',
'C:\\Program Files (x86)\\Godot_4\\Godot.exe',
`${process.env.USERPROFILE}\\Godot\\Godot.exe`
);
} else if (osPlatform === 'linux') {
possiblePaths.push(
'/usr/bin/godot',
'/usr/local/bin/godot',
'/snap/bin/godot',
`${process.env.HOME}/.local/bin/godot`
);
}
// Try each possible path
for (const path of possiblePaths) {
const normalizedPath = normalize(path);
if (await this.isValidGodotPath(normalizedPath)) {
this.godotPath = normalizedPath;
this.logDebug(`Found Godot at: ${normalizedPath}`);
return;
}
}
// If we get here, we couldn't find Godot
this.logDebug(`Warning: Could not find Godot in common locations for ${osPlatform}`);
console.warn(`[SERVER] Could not find Godot in common locations for ${osPlatform}`);
console.warn(`[SERVER] Set GODOT_PATH=/path/to/godot environment variable or pass { godotPath: '/path/to/godot' } in the config to specify the correct path.`);
if (this.strictPathValidation) {
// In strict mode, throw an error
throw new Error(`Could not find a valid Godot executable. Set GODOT_PATH or provide a valid path in config.`);
} else {
// Fallback to a default path in non-strict mode; this may not be valid and requires user configuration for reliability
if (osPlatform === 'win32') {
this.godotPath = normalize('C:\\Program Files\\Godot\\Godot.exe');
} else if (osPlatform === 'darwin') {
this.godotPath = normalize('/Applications/Godot.app/Contents/MacOS/Godot');
} else {
this.godotPath = normalize('/usr/bin/godot');
}
this.logDebug(`Using default path: ${this.godotPath}, but this may not work.`);
console.warn(`[SERVER] Using default path: ${this.godotPath}, but this may not work.`);
console.warn(`[SERVER] This fallback behavior will be removed in a future version. Set strictPathValidation: true to opt-in to the new behavior.`);
}
}
/**
* Set a custom Godot path
* @param customPath Path to the Godot executable
* @returns True if the path is valid and was set, false otherwise
*/
public async setGodotPath(customPath: string): Promise<boolean> {
if (!customPath) {
return false;
}
// Normalize the path to ensure consistent format across platforms
// (e.g., backslashes to forward slashes on Windows, resolving relative paths)
const normalizedPath = normalize(customPath);
if (await this.isValidGodotPath(normalizedPath)) {
this.godotPath = normalizedPath;
this.logDebug(`Godot path set to: ${normalizedPath}`);
return true;
}
this.logDebug(`Failed to set invalid Godot path: ${normalizedPath}`);
return false;
}
/**
* Clean up resources when shutting down
*/
private async cleanup() {
this.logDebug('Cleaning up resources');
if (this.activeProcess) {
this.logDebug('Killing active Godot process');
this.activeProcess.process.kill();
this.activeProcess = null;
}
if (this.remoteDebugConnection) {
this.logDebug('Closing remote debugger connection');
this.remoteDebugConnection.socket.destroy();
this.remoteDebugConnection = null;
}
await this.server.close();
}
/**
* Check if the Godot version is 4.4 or later
* @param version The Godot version string
* @returns True if the version is 4.4 or later
*/
private isGodot44OrLater(version: string): boolean {
const match = version.match(/^(\d+)\.(\d+)/);
if (match) {
const major = parseInt(match[1], 10);
const minor = parseInt(match[2], 10);
return major > 4 || (major === 4 && minor >= 4);
}
return false;
}
/**
* Normalize parameters to camelCase format
* @param params Object with either snake_case or camelCase keys
* @returns Object with all keys in camelCase format
*/
private normalizeParameters(params: OperationParams): OperationParams {
if (!params || typeof params !== 'object') {
return params;
}
const result: OperationParams = {};
for (const key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
let normalizedKey = key;
// If the key is in snake_case, convert it to camelCase using our mapping
if (key.includes('_') && this.parameterMappings[key]) {
normalizedKey = this.parameterMappings[key];
}
// Handle nested objects recursively
if (typeof params[key] === 'object' && params[key] !== null && !Array.isArray(params[key])) {
result[normalizedKey] = this.normalizeParameters(params[key] as OperationParams);
} else {
result[normalizedKey] = params[key];
}
}
}
return result;
}
/**
* Convert camelCase keys to snake_case
* @param params Object with camelCase keys
* @returns Object with snake_case keys
*/
private convertCamelToSnakeCase(params: OperationParams): OperationParams {
const result: OperationParams = {};
for (const key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
// Convert camelCase to snake_case
const snakeKey = this.reverseParameterMappings[key] || key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
// Handle nested objects recursively
if (typeof params[key] === 'object' && params[key] !== null && !Array.isArray(params[key])) {
result[snakeKey] = this.convertCamelToSnakeCase(params[key] as OperationParams);
} else {
result[snakeKey] = params[key];
}
}
}
return result;
}
/**
* Execute a Godot operation using the operations script
* @param operation The operation to execute
* @param params The parameters for the operation
* @param projectPath The path to the Godot project
* @returns The stdout and stderr from the operation
*/
private async executeOperation(
operation: string,
params: OperationParams,
projectPath: string
): Promise<{ stdout: string; stderr: string }> {
this.logDebug(`Executing operation: ${operation} in project: ${projectPath}`);
this.logDebug(`Original operation params: ${JSON.stringify(params)}`);
// Convert camelCase parameters to snake_case for Godot script
const snakeCaseParams = this.convertCamelToSnakeCase(params);
this.logDebug(`Converted snake_case params: ${JSON.stringify(snakeCaseParams)}`);
// Ensure godotPath is set
if (!this.godotPath) {
await this.detectGodotPath();
if (!this.godotPath) {
throw new Error('Could not find a valid Godot executable path');
}
}
try {
// Serialize the snake_case parameters to a valid JSON string
const paramsJson = JSON.stringify(snakeCaseParams);
// Escape single quotes in the JSON string to prevent command injection
const escapedParams = paramsJson.replace(/'/g, "'\\''");
// On Windows, cmd.exe does not strip single quotes, so we use
// double quotes and escape them to ensure the JSON is parsed
// correctly by Godot.
const isWindows = process.platform === 'win32';
const quotedParams = isWindows
? `\"${paramsJson.replace(/\"/g, '\\"')}\"`
: `'${escapedParams}'`;
// Add debug arguments if debug mode is enabled
const debugArgs = GODOT_DEBUG_MODE ? ['--debug-godot'] : [];
// Construct the command with the operation and JSON parameters
const cmd = [
`"${this.godotPath}"`,
'--headless',
'--path',
`"${projectPath}"`,
'--script',
`"${this.operationsScriptPath}"`,
operation,
quotedParams, // Pass the JSON string as a single argument
...debugArgs,
].join(' ');
this.logDebug(`Command: ${cmd}`);
const { stdout, stderr } = await execAsync(cmd);
return { stdout, stderr };
} catch (error: unknown) {
// If execAsync throws, it still contains stdout/stderr
if (error instanceof Error && 'stdout' in error && 'stderr' in error) {
const execError = error as Error & { stdout: string; stderr: string };
return {
stdout: execError.stdout,
stderr: execError.stderr,
};
}
throw error;
}
}
/**
* Get the structure of a Godot project
* @param projectPath Path to the Godot project
* @returns Object representing the project structure
*/
private async getProjectStructure(projectPath: string): Promise<any> {
try {
// Get top-level directories in the project
const entries = readdirSync(projectPath, { withFileTypes: true });
const structure: any = {
scenes: [],
scripts: [],
assets: [],
other: [],
};
for (const entry of entries) {
if (entry.isDirectory()) {
const dirName = entry.name.toLowerCase();
// Skip hidden directories
if (dirName.startsWith('.')) {
continue;
}
// Count files in common directories
if (dirName === 'scenes' || dirName.includes('scene')) {
structure.scenes.push(entry.name);
} else if (dirName === 'scripts' || dirName.includes('script')) {
structure.scripts.push(entry.name);
} else if (
dirName === 'assets' ||
dirName === 'textures' ||
dirName === 'models' ||
dirName === 'sounds' ||
dirName === 'music'
) {
structure.assets.push(entry.name);
} else {
structure.other.push(entry.name);
}
}
}
return structure;
} catch (error) {
this.logDebug(`Error getting project structure: ${error}`);
return { error: 'Failed to get project structure' };
}
}
/**
* Find Godot projects in a directory
* @param directory Directory to search
* @param recursive Whether to search recursively
* @returns Array of Godot projects
*/
private findGodotProjects(directory: string, recursive: boolean): Array<{ path: string; name: string }> {
const projects: Array<{ path: string; name: string }> = [];
try {
// Check if the directory itself is a Godot project
const projectFile = join(directory, 'project.godot');
if (existsSync(projectFile)) {
projects.push({
path: directory,
name: basename(directory),
});
}
// If not recursive, only check immediate subdirectories
if (!recursive) {
const entries = readdirSync(directory, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const subdir = join(directory, entry.name);
const projectFile = join(subdir, 'project.godot');
if (existsSync(projectFile)) {
projects.push({
path: subdir,
name: entry.name,
});
}
}
}
} else {
// Recursive search
const entries = readdirSync(directory, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const subdir = join(directory, entry.name);
// Skip hidden directories
if (entry.name.startsWith('.')) {
continue;
}
// Check if this directory is a Godot project
const projectFile = join(subdir, 'project.godot');
if (existsSync(projectFile)) {
projects.push({
path: subdir,
name: entry.name,
});
} else {
// Recursively search this directory
const subProjects = this.findGodotProjects(subdir, true);
projects.push(...subProjects);
}
}
}
}
} catch (error) {
this.logDebug(`Error searching directory ${directory}: ${error}`);
}
return projects;
}
/**
* Set up the tool handlers for the MCP server
*/
private setupToolHandlers() {
// Define available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'launch_editor',
description: 'Launch Godot editor for a specific project',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to the Godot project directory',
},
},
required: ['projectPath'],
},
},
{
name: 'run_project',
description: 'Run the Godot project and capture output',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to the Godot project directory',
},
scene: {
type: 'string',
description: 'Optional: Specific scene to run',
},
},
required: ['projectPath'],
},
},
{
name: 'get_debug_output',
description: 'Get the current debug output and errors',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'stop_project',
description: 'Stop the currently running Godot project',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_godot_version',
description: 'Get the installed Godot version',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'list_projects',
description: 'List Godot projects in a directory',
inputSchema: {
type: 'object',
properties: {
directory: {
type: 'string',
description: 'Directory to search for Godot projects',
},
recursive: {
type: 'boolean',
description: 'Whether to search recursively (default: false)',
},
},
required: ['directory'],
},
},
{
name: 'get_project_info',
description: 'Retrieve metadata about a Godot project',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to the Godot project directory',
},
},
required: ['projectPath'],
},
},
{
name: 'create_scene',
description: 'Create a new Godot scene file',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to the Godot project directory',
},
scenePath: {
type: 'string',
description: 'Path where the scene file will be saved (relative to project)',
},
rootNodeType: {
type: 'string',
description: 'Type of the root node (e.g., Node2D, Node3D)',
default: 'Node2D',
},
},
required: ['projectPath', 'scenePath'],
},
},
{
name: 'add_node',
description: 'Add a node to an existing scene',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to the Godot project directory',
},
scenePath: {
type: 'string',
description: 'Path to the scene file (relative to project)',
},
parentNodePath: {
type: 'string',
description: 'Path to the parent node (e.g., "root" or "root/Player")',
default: 'root',
},
nodeType: {
type: 'string',
description: 'Type of node to add (e.g., Sprite2D, CollisionShape2D)',
},
nodeName: {
type: 'string',
description: 'Name for the new node',
},
properties: {
type: 'object',
description: 'Optional properties to set on the node',
},
},
required: ['projectPath', 'scenePath', 'nodeType', 'nodeName'],
},
},
{
name: 'load_sprite',
description: 'Load a sprite into a Sprite2D node',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to the Godot project directory',
},
scenePath: {
type: 'string',
description: 'Path to the scene file (relative to project)',
},
nodePath: {
type: 'string',
description: 'Path to the Sprite2D node (e.g., "root/Player/Sprite2D")',
},
texturePath: {
type: 'string',
description: 'Path to the texture file (relative to project)',
},
},
required: ['projectPath', 'scenePath', 'nodePath', 'texturePath'],
},
},
{
name: 'export_mesh_library',
description: 'Export a scene as a MeshLibrary resource',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to the Godot project directory',
},
scenePath: {
type: 'string',
description: 'Path to the scene file (.tscn) to export',
},
outputPath: {
type: 'string',
description: 'Path where the mesh library (.res) will be saved',
},
meshItemNames: {
type: 'array',
items: {
type: 'string',
},
description: 'Optional: Names of specific mesh items to include (defaults to all)',
},
},
required: ['projectPath', 'scenePath', 'outputPath'],
},
},
{
name: 'save_scene',
description: 'Save changes to a scene file',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to the Godot project directory',
},
scenePath: {
type: 'string',
description: 'Path to the scene file (relative to project)',
},
newPath: {
type: 'string',
description: 'Optional: New path to save the scene to (for creating variants)',
},
},
required: ['projectPath', 'scenePath'],
},
},
{
name: 'get_uid',
description: 'Get the UID for a specific file in a Godot project (for Godot 4.4+)',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to the Godot project directory',
},
filePath: {
type: 'string',
description: 'Path to the file (relative to project) for which to get the UID',
},
},
required: ['projectPath', 'filePath'],
},
},
{
name: 'update_project_uids',
description: 'Update UID references in a Godot project by resaving resources (for Godot 4.4+)',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to the Godot project directory',
},
},
required: ['projectPath'],
},
},
{
name: 'connect_remote_debugger',
description: 'Connect to Godot editor\'s remote debugger to capture debug output in real-time. Use port 6006 for script debugger (print/errors) or 6007 for live editor sync.',
inputSchema: {
type: 'object',
properties: {
host: {
type: 'string',
description: 'Host address of the Godot editor (default: localhost)',
default: 'localhost',
},
port: {
type: 'number',
description: 'Remote debugger port (default: 6006 for script debugger, 6007 for live sync)',
default: 6006,
},
},
required: [],
},
},
{
name: 'get_remote_debug_output',
description: 'Get the debug output captured from the remote debugger connection',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'disconnect_remote_debugger',
description: 'Disconnect from the Godot editor\'s remote debugger',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'capture_screenshot',
description: 'Capture a screenshot of the running game viewport via remote debugger and return it as base64-encoded image. Requires an active remote debugger connection (use connect_remote_debugger first).',
inputSchema: {
type: 'object',
properties: {
format: {
type: 'string',
description: 'Image format (png or jpg, default: png)',
enum: ['png', 'jpg'],
default: 'png',
},
},
required: [],
},
},
],
}));
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
this.logDebug(`Handling tool request: ${request.params.name}`);
switch (request.params.name) {
case 'launch_editor':
return await this.handleLaunchEditor(request.params.arguments);
case 'run_project':
return await this.handleRunProject(request.params.arguments);
case 'get_debug_output':
return await this.handleGetDebugOutput();
case 'stop_project':
return await this.handleStopProject();
case 'get_godot_version':
return await this.handleGetGodotVersion();
case 'list_projects':
return await this.handleListProjects(request.params.arguments);
case 'get_project_info':
return await this.handleGetProjectInfo(request.params.arguments);
case 'create_scene':
return await this.handleCreateScene(request.params.arguments);
case 'add_node':
return await this.handleAddNode(request.params.arguments);
case 'load_sprite':
return await this.handleLoadSprite(request.params.arguments);
case 'export_mesh_library':
return await this.handleExportMeshLibrary(request.params.arguments);
case 'save_scene':
return await this.handleSaveScene(request.params.arguments);
case 'get_uid':
return await this.handleGetUid(request.params.arguments);
case 'update_project_uids':
return await this.handleUpdateProjectUids(request.params.arguments);
case 'connect_remote_debugger':
return await this.handleConnectRemoteDebugger(request.params.arguments);
case 'get_remote_debug_output':
return await this.handleGetRemoteDebugOutput();
case 'disconnect_remote_debugger':
return await this.handleDisconnectRemoteDebugger();
case 'capture_screenshot':
return await this.handleCaptureScreenshot(request.params.arguments);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
});
}
/**
* Handle the launch_editor tool
* @param args Tool arguments
*/
private async handleLaunchEditor(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
if (!args.projectPath) {
return this.createErrorResponse(
'Project path is required',
['Provide a valid path to a Godot project directory']
);
}
if (!this.validatePath(args.projectPath)) {
return this.createErrorResponse(
'Invalid project path',
['Provide a valid path without ".." or other potentially unsafe characters']
);
}
try {
// Ensure godotPath is set
if (!this.godotPath) {
await this.detectGodotPath();
if (!this.godotPath) {
return this.createErrorResponse(
'Could not find a valid Godot executable path',
[
'Ensure Godot is installed correctly',
'Set GODOT_PATH environment variable to specify the correct path',
]
);
}
}
// Check if the project directory exists and contains a project.godot file
const projectFile = join(args.projectPath, 'project.godot');
if (!existsSync(projectFile)) {
return this.createErrorResponse(
`Not a valid Godot project: ${args.projectPath}`,
[
'Ensure the path points to a directory containing a project.godot file',
'Use list_projects to find valid Godot projects',
]
);
}
this.logDebug(`Launching Godot editor for project: ${args.projectPath}`);
const process = spawn(this.godotPath, ['-e', '--path', args.projectPath], {
stdio: 'pipe',
});
process.on('error', (err: Error) => {
console.error('Failed to start Godot editor:', err);
});
return {
content: [
{
type: 'text',
text: `Godot editor launched successfully for project at ${args.projectPath}.`,
},
],
};
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return this.createErrorResponse(
`Failed to launch Godot editor: ${errorMessage}`,
[
'Ensure Godot is installed correctly',
'Check if the GODOT_PATH environment variable is set correctly',
'Verify the project path is accessible',
]
);
}
}
/**
* Handle the run_project tool
* @param args Tool arguments
*/
private async handleRunProject(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
if (!args.projectPath) {
return this.createErrorResponse(
'Project path is required',
['Provide a valid path to a Godot project directory']
);
}
if (!this.validatePath(args.projectPath)) {
return this.createErrorResponse(
'Invalid project path',
['Provide a valid path without ".." or other potentially unsafe characters']
);
}
try {
// Check if the project directory exists and contains a project.godot file
const projectFile = join(args.projectPath, 'project.godot');
if (!existsSync(projectFile)) {
return this.createErrorResponse(
`Not a valid Godot project: ${args.projectPath}`,
[
'Ensure the path points to a directory containing a project.godot file',
'Use list_projects to find valid Godot projects',
]
);
}
// Kill any existing process
if (this.activeProcess) {
this.logDebug('Killing existing Godot process before starting a new one');
this.activeProcess.process.kill();
}
const cmdArgs = ['-d', '--path', args.projectPath];
if (args.scene && this.validatePath(args.scene)) {
this.logDebug(`Adding scene parameter: ${args.scene}`);
cmdArgs.push(args.scene);
}
this.logDebug(`Running Godot project: ${args.projectPath}`);
const process = spawn(this.godotPath!, cmdArgs, { stdio: 'pipe' });
const output: string[] = [];
const errors: string[] = [];
process.stdout?.on('data', (data: Buffer) => {
const lines = data.toString().split('\n');
output.push(...lines);
lines.forEach((line: string) => {
if (line.trim()) this.logDebug(`[Godot stdout] ${line}`);
});
});
process.stderr?.on('data', (data: Buffer) => {
const lines = data.toString().split('\n');
errors.push(...lines);
lines.forEach((line: string) => {
if (line.trim()) this.logDebug(`[Godot stderr] ${line}`);
});
});
process.on('exit', (code: number | null) => {
this.logDebug(`Godot process exited with code ${code}`);
if (this.activeProcess && this.activeProcess.process === process) {
this.activeProcess = null;
}
});
process.on('error', (err: Error) => {
console.error('Failed to start Godot process:', err);
if (this.activeProcess && this.activeProcess.process === process) {
this.activeProcess = null;
}
});
this.activeProcess = { process, output, errors };
return {
content: [
{
type: 'text',
text: `Godot project started in debug mode. Use get_debug_output to see output.`,
},
],
};
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return this.createErrorResponse(
`Failed to run Godot project: ${errorMessage}`,
[
'Ensure Godot is installed correctly',
'Check if the GODOT_PATH environment variable is set correctly',
'Verify the project path is accessible',
]
);
}
}
/**
* Handle the get_debug_output tool
*/
private async handleGetDebugOutput() {
if (!this.activeProcess) {
return this.createErrorResponse(
'No active Godot process.',
[
'Use run_project to start a Godot project first',
'Check if the Godot process crashed unexpectedly',
]
);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
output: this.activeProcess.output,
errors: this.activeProcess.errors,
},
null,
2
),
},
],
};
}
/**
* Handle the stop_project tool
*/
private async handleStopProject() {
if (!this.activeProcess) {
return this.createErrorResponse(
'No active Godot process to stop.',
[
'Use run_project to start a Godot project first',
'The process may have already terminated',
]
);
}
this.logDebug('Stopping active Godot process');
this.activeProcess.process.kill();
const output = this.activeProcess.output;
const errors = this.activeProcess.errors;
this.activeProcess = null;
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
message: 'Godot project stopped',
finalOutput: output,
finalErrors: errors,
},
null,
2
),
},
],
};
}
/**
* Handle the get_godot_version tool
*/
private async handleGetGodotVersion() {
try {
// Ensure godotPath is set
if (!this.godotPath) {
await this.detectGodotPath();
if (!this.godotPath) {
return this.createErrorResponse(
'Could not find a valid Godot executable path',
[
'Ensure Godot is installed correctly',
'Set GODOT_PATH environment variable to specify the correct path',
]
);
}
}
this.logDebug('Getting Godot version');
const { stdout } = await execAsync(`"${this.godotPath}" --version`);
return {
content: [
{
type: 'text',
text: stdout.trim(),
},
],
};
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return this.createErrorResponse(
`Failed to get Godot version: ${errorMessage}`,
[
'Ensure Godot is installed correctly',
'Check if the GODOT_PATH environment variable is set correctly',
]
);
}
}
/**
* Handle the list_projects tool
*/
private async handleListProjects(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
if (!args.directory) {
return this.createErrorResponse(
'Directory is required',
['Provide a valid directory path to search for Godot projects']
);
}
if (!this.validatePath(args.directory)) {
return this.createErrorResponse(
'Invalid directory path',
['Provide a valid path without ".." or other potentially unsafe characters']
);
}
try {
this.logDebug(`Listing Godot projects in directory: ${args.directory}`);
if (!existsSync(args.directory)) {
return this.createErrorResponse(
`Directory does not exist: ${args.directory}`,
['Provide a valid directory path that exists on the system']
);
}
const recursive = args.recursive === true;
const projects = this.findGodotProjects(args.directory, recursive);
return {
content: [
{
type: 'text',
text: JSON.stringify(projects, null, 2),
},
],
};
} catch (error: any) {
return this.createErrorResponse(
`Failed to list projects: ${error?.message || 'Unknown error'}`,
[
'Ensure the directory exists and is accessible',
'Check if you have permission to read the directory',
]
);
}
}
/**
* Get the structure of a Godot project asynchronously by counting files recursively
* @param projectPath Path to the Godot project
* @returns Promise resolving to an object with counts of scenes, scripts, assets, and other files
*/
private getProjectStructureAsync(projectPath: string): Promise<any> {
return new Promise((resolve) => {
try {
const structure = {
scenes: 0,
scripts: 0,
assets: 0,
other: 0,
};
const scanDirectory = (currentPath: string) => {
const entries = readdirSync(currentPath, { withFileTypes: true });
for (const entry of entries) {
const entryPath = join(currentPath, entry.name);
// Skip hidden files and directories
if (entry.name.startsWith('.')) {
continue;
}
if (entry.isDirectory()) {
// Recursively scan subdirectories
scanDirectory(entryPath);
} else if (entry.isFile()) {
// Count file by extension
const ext = entry.name.split('.').pop()?.toLowerCase();
if (ext === 'tscn') {
structure.scenes++;
} else if (ext === 'gd' || ext === 'gdscript' || ext === 'cs') {
structure.scripts++;
} else if (['png', 'jpg', 'jpeg', 'webp', 'svg', 'ttf', 'wav', 'mp3', 'ogg'].includes(ext || '')) {
structure.assets++;
} else {
structure.other++;
}
}
}
};
// Start scanning from the project root
scanDirectory(projectPath);
resolve(structure);
} catch (error) {
this.logDebug(`Error getting project structure asynchronously: ${error}`);
resolve({
error: 'Failed to get project structure',
scenes: 0,
scripts: 0,
assets: 0,
other: 0
});
}
});
}
/**
* Handle the get_project_info tool
*/
private async handleGetProjectInfo(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
if (!args.projectPath) {
return this.createErrorResponse(
'Project path is required',
['Provide a valid path to a Godot project directory']
);
}
if (!this.validatePath(args.projectPath)) {
return this.createErrorResponse(
'Invalid project path',
['Provide a valid path without ".." or other potentially unsafe characters']
);
}
try {
// Ensure godotPath is set
if (!this.godotPath) {
await this.detectGodotPath();
if (!this.godotPath) {
return this.createErrorResponse(
'Could not find a valid Godot executable path',
[
'Ensure Godot is installed correctly',
'Set GODOT_PATH environment variable to specify the correct path',
]
);
}
}
// Check if the project directory exists and contains a project.godot file
const projectFile = join(args.projectPath, 'project.godot');
if (!existsSync(projectFile)) {
return this.createErrorResponse(
`Not a valid Godot project: ${args.projectPath}`,
[
'Ensure the path points to a directory containing a project.godot file',
'Use list_projects to find valid Godot projects',
]
);
}
this.logDebug(`Getting project info for: ${args.projectPath}`);
// Get Godot version
const execOptions = { timeout: 10000 }; // 10 second timeout
const { stdout } = await execAsync(`"${this.godotPath}" --version`, execOptions);
// Get project structure using the recursive method
const projectStructure = await this.getProjectStructureAsync(args.projectPath);
// Extract project name from project.godot file
let projectName = basename(args.projectPath);
try {
const fs = require('fs');
const projectFileContent = fs.readFileSync(projectFile, 'utf8');
const configNameMatch = projectFileContent.match(/config\/name="([^"]+)"/);
if (configNameMatch && configNameMatch[1]) {
projectName = configNameMatch[1];
this.logDebug(`Found project name in config: ${projectName}`);
}
} catch (error) {
this.logDebug(`Error reading project file: ${error}`);
// Continue with default project name if extraction fails
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
name: projectName,
path: args.projectPath,
godotVersion: stdout.trim(),
structure: projectStructure,
},
null,
2
),
},
],
};
} catch (error: any) {
return this.createErrorResponse(
`Failed to get project info: ${error?.message || 'Unknown error'}`,
[
'Ensure Godot is installed correctly',
'Check if the GODOT_PATH environment variable is set correctly',
'Verify the project path is accessible',
]
);
}
}
/**
* Handle the create_scene tool
*/
private async handleCreateScene(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
if (!args.projectPath || !args.scenePath) {
return this.createErrorResponse(
'Project path and scene path are required',
['Provide valid paths for both the project and the scene']
);
}
if (!this.validatePath(args.projectPath) || !this.validatePath(args.scenePath)) {
return this.createErrorResponse(
'Invalid path',
['Provide valid paths without ".." or other potentially unsafe characters']
);
}
try {
// Check if the project directory exists and contains a project.godot file
const projectFile = join(args.projectPath, 'project.godot');
if (!existsSync(projectFile)) {
return this.createErrorResponse(
`Not a valid Godot project: ${args.projectPath}`,
[
'Ensure the path points to a directory containing a project.godot file',
'Use list_projects to find valid Godot projects',
]
);
}
// Prepare parameters for the operation (already in camelCase)
const params = {
scenePath: args.scenePath,
rootNodeType: args.rootNodeType || 'Node2D',
};
// Execute the operation
const { stdout, stderr } = await this.executeOperation('create_scene', params, args.projectPath);
if (stderr && stderr.includes('Failed to')) {
return this.createErrorResponse(
`Failed to create scene: ${stderr}`,
[
'Check if the root node type is valid',
'Ensure you have write permissions to the scene path',
'Verify the scene path is valid',
]
);
}
return {
content: [
{
type: 'text',
text: `Scene created successfully at: ${args.scenePath}\n\nOutput: ${stdout}`,
},
],
};
} catch (error: any) {
return this.createErrorResponse(
`Failed to create scene: ${error?.message || 'Unknown error'}`,
[
'Ensure Godot is installed correctly',
'Check if the GODOT_PATH environment variable is set correctly',
'Verify the project path is accessible',
]
);
}
}
/**
* Handle the add_node tool
*/
private async handleAddNode(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
if (!args.projectPath || !args.scenePath || !args.nodeType || !args.nodeName) {
return this.createErrorResponse(
'Missing required parameters',
['Provide projectPath, scenePath, nodeType, and nodeName']
);
}
if (!this.validatePath(args.projectPath) || !this.validatePath(args.scenePath)) {
return this.createErrorResponse(
'Invalid path',
['Provide valid paths without ".." or other potentially unsafe characters']
);
}
try {
// Check if the project directory exists and contains a project.godot file
const projectFile = join(args.projectPath, 'project.godot');
if (!existsSync(projectFile)) {
return this.createErrorResponse(
`Not a valid Godot project: ${args.projectPath}`,
[
'Ensure the path points to a directory containing a project.godot file',
'Use list_projects to find valid Godot projects',
]
);
}
// Check if the scene file exists
const scenePath = join(args.projectPath, args.scenePath);
if (!existsSync(scenePath)) {
return this.createErrorResponse(
`Scene file does not exist: ${args.scenePath}`,
[
'Ensure the scene path is correct',
'Use create_scene to create a new scene first',
]
);
}
// Prepare parameters for the operation (already in camelCase)
const params: any = {
scenePath: args.scenePath,
nodeType: args.nodeType,
nodeName: args.nodeName,
};
// Add optional parameters
if (args.parentNodePath) {
params.parentNodePath = args.parentNodePath;
}
if (args.properties) {
params.properties = args.properties;
}
// Execute the operation
const { stdout, stderr } = await this.executeOperation('add_node', params, args.projectPath);
if (stderr && stderr.includes('Failed to')) {
return this.createErrorResponse(
`Failed to add node: ${stderr}`,
[
'Check if the node type is valid',
'Ensure the parent node path exists',
'Verify the scene file is valid',
]
);
}
return {
content: [
{
type: 'text',
text: `Node '${args.nodeName}' of type '${args.nodeType}' added successfully to '${args.scenePath}'.\n\nOutput: ${stdout}`,
},
],
};
} catch (error: any) {
return this.createErrorResponse(
`Failed to add node: ${error?.message || 'Unknown error'}`,
[
'Ensure Godot is installed correctly',
'Check if the GODOT_PATH environment variable is set correctly',
'Verify the project path is accessible',
]
);
}
}
/**
* Handle the load_sprite tool
*/
private async handleLoadSprite(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
if (!args.projectPath || !args.scenePath || !args.nodePath || !args.texturePath) {
return this.createErrorResponse(
'Missing required parameters',
['Provide projectPath, scenePath, nodePath, and texturePath']
);
}
if (
!this.validatePath(args.projectPath) ||
!this.validatePath(args.scenePath) ||
!this.validatePath(args.nodePath) ||
!this.validatePath(args.texturePath)
) {
return this.createErrorResponse(
'Invalid path',
['Provide valid paths without ".." or other potentially unsafe characters']
);
}
try {
// Check if the project directory exists and contains a project.godot file
const projectFile = join(args.projectPath, 'project.godot');
if (!existsSync(projectFile)) {
return this.createErrorResponse(
`Not a valid Godot project: ${args.projectPath}`,
[
'Ensure the path points to a directory containing a project.godot file',
'Use list_projects to find valid Godot projects',
]
);
}
// Check if the scene file exists
const scenePath = join(args.projectPath, args.scenePath);
if (!existsSync(scenePath)) {
return this.createErrorResponse(
`Scene file does not exist: ${args.scenePath}`,
[
'Ensure the scene path is correct',
'Use create_scene to create a new scene first',
]
);
}
// Check if the texture file exists
const texturePath = join(args.projectPath, args.texturePath);
if (!existsSync(texturePath)) {
return this.createErrorResponse(
`Texture file does not exist: ${args.texturePath}`,
[
'Ensure the texture path is correct',
'Upload or create the texture file first',
]
);
}
// Prepare parameters for the operation (already in camelCase)
const params = {
scenePath: args.scenePath,
nodePath: args.nodePath,
texturePath: args.texturePath,
};
// Execute the operation
const { stdout, stderr } = await this.executeOperation('load_sprite', params, args.projectPath);
if (stderr && stderr.includes('Failed to')) {
return this.createErrorResponse(
`Failed to load sprite: ${stderr}`,
[
'Check if the node path is correct',
'Ensure the node is a Sprite2D, Sprite3D, or TextureRect',
'Verify the texture file is a valid image format',
]
);
}
return {
content: [
{
type: 'text',
text: `Sprite loaded successfully with texture: ${args.texturePath}\n\nOutput: ${stdout}`,
},
],
};
} catch (error: any) {
return this.createErrorResponse(
`Failed to load sprite: ${error?.message || 'Unknown error'}`,
[
'Ensure Godot is installed correctly',
'Check if the GODOT_PATH environment variable is set correctly',
'Verify the project path is accessible',
]
);
}
}
/**
* Handle the export_mesh_library tool
*/
private async handleExportMeshLibrary(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
if (!args.projectPath || !args.scenePath || !args.outputPath) {
return this.createErrorResponse(
'Missing required parameters',
['Provide projectPath, scenePath, and outputPath']
);
}
if (
!this.validatePath(args.projectPath) ||
!this.validatePath(args.scenePath) ||
!this.validatePath(args.outputPath)
) {
return this.createErrorResponse(
'Invalid path',
['Provide valid paths without ".." or other potentially unsafe characters']
);
}
try {
// Check if the project directory exists and contains a project.godot file
const projectFile = join(args.projectPath, 'project.godot');
if (!existsSync(projectFile)) {
return this.createErrorResponse(
`Not a valid Godot project: ${args.projectPath}`,
[
'Ensure the path points to a directory containing a project.godot file',
'Use list_projects to find valid Godot projects',
]
);
}
// Check if the scene file exists
const scenePath = join(args.projectPath, args.scenePath);
if (!existsSync(scenePath)) {
return this.createErrorResponse(
`Scene file does not exist: ${args.scenePath}`,
[
'Ensure the scene path is correct',
'Use create_scene to create a new scene first',
]
);
}
// Prepare parameters for the operation (already in camelCase)
const params: any = {
scenePath: args.scenePath,
outputPath: args.outputPath,
};
// Add optional parameters
if (args.meshItemNames && Array.isArray(args.meshItemNames)) {
params.meshItemNames = args.meshItemNames;
}
// Execute the operation
const { stdout, stderr } = await this.executeOperation('export_mesh_library', params, args.projectPath);
if (stderr && stderr.includes('Failed to')) {
return this.createErrorResponse(
`Failed to export mesh library: ${stderr}`,
[
'Check if the scene contains valid 3D meshes',
'Ensure the output path is valid',
'Verify the scene file is valid',
]
);
}
return {
content: [
{
type: 'text',
text: `MeshLibrary exported successfully to: ${args.outputPath}\n\nOutput: ${stdout}`,
},
],
};
} catch (error: any) {
return this.createErrorResponse(
`Failed to export mesh library: ${error?.message || 'Unknown error'}`,
[
'Ensure Godot is installed correctly',
'Check if the GODOT_PATH environment variable is set correctly',
'Verify the project path is accessible',
]
);
}
}
/**
* Handle the save_scene tool
*/
private async handleSaveScene(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
if (!args.projectPath || !args.scenePath) {
return this.createErrorResponse(
'Missing required parameters',
['Provide projectPath and scenePath']
);
}
if (!this.validatePath(args.projectPath) || !this.validatePath(args.scenePath)) {
return this.createErrorResponse(
'Invalid path',
['Provide valid paths without ".." or other potentially unsafe characters']
);
}
// If newPath is provided, validate it
if (args.newPath && !this.validatePath(args.newPath)) {
return this.createErrorResponse(
'Invalid new path',
['Provide a valid new path without ".." or other potentially unsafe characters']
);
}
try {
// Check if the project directory exists and contains a project.godot file
const projectFile = join(args.projectPath, 'project.godot');
if (!existsSync(projectFile)) {
return this.createErrorResponse(
`Not a valid Godot project: ${args.projectPath}`,
[
'Ensure the path points to a directory containing a project.godot file',
'Use list_projects to find valid Godot projects',
]
);
}
// Check if the scene file exists
const scenePath = join(args.projectPath, args.scenePath);
if (!existsSync(scenePath)) {
return this.createErrorResponse(
`Scene file does not exist: ${args.scenePath}`,
[
'Ensure the scene path is correct',
'Use create_scene to create a new scene first',
]
);
}
// Prepare parameters for the operation (already in camelCase)
const params: any = {
scenePath: args.scenePath,
};
// Add optional parameters
if (args.newPath) {
params.newPath = args.newPath;
}
// Execute the operation
const { stdout, stderr } = await this.executeOperation('save_scene', params, args.projectPath);
if (stderr && stderr.includes('Failed to')) {
return this.createErrorResponse(
`Failed to save scene: ${stderr}`,
[
'Check if the scene file is valid',
'Ensure you have write permissions to the output path',
'Verify the scene can be properly packed',
]
);
}
const savePath = args.newPath || args.scenePath;
return {
content: [
{
type: 'text',
text: `Scene saved successfully to: ${savePath}\n\nOutput: ${stdout}`,
},
],
};
} catch (error: any) {
return this.createErrorResponse(
`Failed to save scene: ${error?.message || 'Unknown error'}`,
[
'Ensure Godot is installed correctly',
'Check if the GODOT_PATH environment variable is set correctly',
'Verify the project path is accessible',
]
);
}
}
/**
* Handle the get_uid tool
*/
private async handleGetUid(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
if (!args.projectPath || !args.filePath) {
return this.createErrorResponse(
'Missing required parameters',
['Provide projectPath and filePath']
);
}
if (!this.validatePath(args.projectPath) || !this.validatePath(args.filePath)) {
return this.createErrorResponse(
'Invalid path',
['Provide valid paths without ".." or other potentially unsafe characters']
);
}
try {
// Ensure godotPath is set
if (!this.godotPath) {
await this.detectGodotPath();
if (!this.godotPath) {
return this.createErrorResponse(
'Could not find a valid Godot executable path',
[
'Ensure Godot is installed correctly',
'Set GODOT_PATH environment variable to specify the correct path',
]
);
}
}
// Check if the project directory exists and contains a project.godot file
const projectFile = join(args.projectPath, 'project.godot');
if (!existsSync(projectFile)) {
return this.createErrorResponse(
`Not a valid Godot project: ${args.projectPath}`,
[
'Ensure the path points to a directory containing a project.godot file',
'Use list_projects to find valid Godot projects',
]
);
}
// Check if the file exists
const filePath = join(args.projectPath, args.filePath);
if (!existsSync(filePath)) {
return this.createErrorResponse(
`File does not exist: ${args.filePath}`,
['Ensure the file path is correct']
);
}
// Get Godot version to check if UIDs are supported
const { stdout: versionOutput } = await execAsync(`"${this.godotPath}" --version`);
const version = versionOutput.trim();
if (!this.isGodot44OrLater(version)) {
return this.createErrorResponse(
`UIDs are only supported in Godot 4.4 or later. Current version: ${version}`,
[
'Upgrade to Godot 4.4 or later to use UIDs',
'Use resource paths instead of UIDs for this version of Godot',
]
);
}
// Prepare parameters for the operation (already in camelCase)
const params = {
filePath: args.filePath,
};
// Execute the operation
const { stdout, stderr } = await this.executeOperation('get_uid', params, args.projectPath);
if (stderr && stderr.includes('Failed to')) {
return this.createErrorResponse(
`Failed to get UID: ${stderr}`,
[
'Check if the file is a valid Godot resource',
'Ensure the file path is correct',
]
);
}
return {
content: [
{
type: 'text',
text: `UID for ${args.filePath}: ${stdout.trim()}`,
},
],
};
} catch (error: any) {
return this.createErrorResponse(
`Failed to get UID: ${error?.message || 'Unknown error'}`,
[
'Ensure Godot is installed correctly',
'Check if the GODOT_PATH environment variable is set correctly',
'Verify the project path is accessible',
]
);
}
}
/**
* Handle the update_project_uids tool
*/
private async handleUpdateProjectUids(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
if (!args.projectPath) {
return this.createErrorResponse(
'Project path is required',
['Provide a valid path to a Godot project directory']
);
}
if (!this.validatePath(args.projectPath)) {
return this.createErrorResponse(
'Invalid project path',
['Provide a valid path without ".." or other potentially unsafe characters']
);
}
try {
// Ensure godotPath is set
if (!this.godotPath) {
await this.detectGodotPath();
if (!this.godotPath) {
return this.createErrorResponse(
'Could not find a valid Godot executable path',
[
'Ensure Godot is installed correctly',
'Set GODOT_PATH environment variable to specify the correct path',
]
);
}
}
// Check if the project directory exists and contains a project.godot file
const projectFile = join(args.projectPath, 'project.godot');
if (!existsSync(projectFile)) {
return this.createErrorResponse(
`Not a valid Godot project: ${args.projectPath}`,
[
'Ensure the path points to a directory containing a project.godot file',
'Use list_projects to find valid Godot projects',
]
);
}
// Get Godot version to check if UIDs are supported
const { stdout: versionOutput } = await execAsync(`"${this.godotPath}" --version`);
const version = versionOutput.trim();
if (!this.isGodot44OrLater(version)) {
return this.createErrorResponse(
`UIDs are only supported in Godot 4.4 or later. Current version: ${version}`,
[
'Upgrade to Godot 4.4 or later to use UIDs',
'Use resource paths instead of UIDs for this version of Godot',
]
);
}
// Prepare parameters for the operation (already in camelCase)
const params = {
projectPath: args.projectPath,
};
// Execute the operation
const { stdout, stderr } = await this.executeOperation('resave_resources', params, args.projectPath);
if (stderr && stderr.includes('Failed to')) {
return this.createErrorResponse(
`Failed to update project UIDs: ${stderr}`,
[
'Check if the project is valid',
'Ensure you have write permissions to the project directory',
]
);
}
return {
content: [
{
type: 'text',
text: `Project UIDs updated successfully.\n\nOutput: ${stdout}`,
},
],
};
} catch (error: any) {
return this.createErrorResponse(
`Failed to update project UIDs: ${error?.message || 'Unknown error'}`,
[
'Ensure Godot is installed correctly',
'Check if the GODOT_PATH environment variable is set correctly',
'Verify the project path is accessible',
]
);
}
}
/**
* Handle the connect_remote_debugger tool
* @param args Tool arguments
*/
private async handleConnectRemoteDebugger(args: any) {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
// Set defaults
const host = args.host || 'localhost';
const port = args.port || 6006; // Changed to 6006 (script debugger) instead of 6007 (live sync)
// Check if already connected
if (this.remoteDebugConnection && this.remoteDebugConnection.connected) {
return this.createErrorResponse(
`Already connected to remote debugger at ${this.remoteDebugConnection.host}:${this.remoteDebugConnection.port}`,
[
'Use disconnect_remote_debugger to disconnect first',
'Or use get_remote_debug_output to retrieve current output',
]
);
}
try {
this.logDebug(`Connecting to Godot remote debugger at ${host}:${port}`);
const socket = new Socket();
const output: string[] = [];
const errors: string[] = [];
const rawData: string[] = [];
let connected = false;
// Create connection promise
const connectPromise = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
socket.destroy();
reject(new Error(`Connection timeout after 5 seconds`));
}, 5000);
socket.connect(port, host, () => {
clearTimeout(timeout);
connected = true;
this.logDebug(`Connected to remote debugger at ${host}:${port}`);
resolve();
});
socket.on('error', (err) => {
clearTimeout(timeout);
if (!connected) {
reject(err);
} else {
this.logDebug(`Socket error: ${err.message}`);
errors.push(`Socket error: ${err.message}`);
}
});
});
// Wait for connection
await connectPromise;
// Buffer for incomplete packets
let buffer = Buffer.alloc(0);
// Set up data handlers for Debug Adapter Protocol (DAP)
socket.on('data', (data: Buffer) => {
this.logDebug(`Received ${data.length} bytes from debugger`);
// Store raw data preview for debugging (limit to last 100 entries)
const textPreview = data.toString('utf8', 0, Math.min(data.length, 200)).replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, '.');
rawData.push(`[${data.length} bytes] ${textPreview}...`);
if (rawData.length > 100) {
rawData.shift();
}
// Append to buffer
buffer = Buffer.concat([buffer, data]);
// Parse Debug Adapter Protocol (DAP) messages
// Format: Content-Length: <bytes>\r\n\r\n{JSON}
while (true) {
// Look for Content-Length header
const bufferStr = buffer.toString('utf8', 0, Math.min(buffer.length, 200));
const headerMatch = bufferStr.match(/Content-Length: (\d+)\r\n\r\n/);
if (!headerMatch) {
// No complete header yet, wait for more data
break;
}
const contentLength = parseInt(headerMatch[1], 10);
const headerLength = headerMatch[0].length;
const totalLength = headerLength + contentLength;
if (buffer.length < totalLength) {
// Don't have the full message yet, wait for more data
break;
}
// Extract the JSON message
const jsonStr = buffer.toString('utf8', headerLength, totalLength);
buffer = buffer.subarray(totalLength);
try {
const message = JSON.parse(jsonStr);
this.logDebug(`DAP message: ${JSON.stringify(message).substring(0, 200)}`);
// Handle output events
if (message.type === 'event' && message.event === 'output') {
const outputText = message.body?.output;
const category = message.body?.category || 'console';
if (outputText) {
// Clean up the output text
const cleanOutput = outputText.trim();
if (cleanOutput) {
// Categorize based on category field
if (category === 'stderr' || category === 'error') {
errors.push(cleanOutput);
this.logDebug(`Error output: ${cleanOutput.substring(0, 100)}`);
} else if (category === 'stdout' || category === 'console') {
output.push(cleanOutput);
this.logDebug(`Stdout output: ${cleanOutput.substring(0, 100)}`);
} else {
// Unknown category, treat as output
output.push(`[${category}] ${cleanOutput}`);
this.logDebug(`Other output (${category}]: ${cleanOutput.substring(0, 100)}`);
}
}
}
}
// Handle responses to our requests
else if (message.type === 'response') {
this.logDebug(`DAP response: ${message.command}, request_seq: ${message.request_seq}`);
// Check if we have a pending request for this response
if (this.remoteDebugConnection && message.request_seq) {
const pending = this.remoteDebugConnection.pendingRequests.get(message.request_seq);
if (pending) {
this.remoteDebugConnection.pendingRequests.delete(message.request_seq);
if (message.success) {
pending.resolve(message.body);
} else {
pending.reject(new Error(message.message || 'Request failed'));
}
}
}
}
} catch (parseError) {
this.logDebug(`Failed to parse DAP message: ${parseError}`);
errors.push(`Protocol parse error: ${parseError}`);
}
}
// Prevent buffer from growing too large
if (buffer.length > 100000) {
this.logDebug('Buffer too large, clearing old data');
buffer = Buffer.alloc(0);
}
});
socket.on('close', () => {
this.logDebug('Remote debugger connection closed');
if (this.remoteDebugConnection) {
this.remoteDebugConnection.connected = false;
}
});
socket.on('end', () => {
this.logDebug('Remote debugger connection ended');
if (this.remoteDebugConnection) {
this.remoteDebugConnection.connected = false;
}
});
// Store connection
this.remoteDebugConnection = {
socket,
output,
errors,
rawData,
connected: true,
host,
port,
sequenceNumber: 1,
pendingRequests: new Map(),
};
return {
content: [
{
type: 'text',
text: `Successfully connected to Godot remote debugger at ${host}:${port}.\n\nUse get_remote_debug_output to retrieve captured output.`,
},
],
};
} catch (error: any) {
this.logDebug(`Failed to connect to remote debugger: ${error?.message}`);
return this.createErrorResponse(
`Failed to connect to Godot remote debugger at ${host}:${port}: ${error?.message || 'Unknown error'}`,
[
'Ensure Godot editor is running with remote debugging enabled',
'Port 6006 is for script debugger (print/errors), port 6007 is for live editor sync',
'Verify the host address is accessible',
'In Godot, go to Editor > Editor Settings > Network > Debug to check ports',
'Make sure to run your game from the editor (F5) after connecting',
]
);
}
}
/**
* Handle the get_remote_debug_output tool
*/
private async handleGetRemoteDebugOutput() {
if (!this.remoteDebugConnection) {
return this.createErrorResponse(
'No remote debugger connection active.',
[
'Use connect_remote_debugger to establish a connection first',
'Ensure Godot editor is running with remote debugging enabled',
]
);
}
if (!this.remoteDebugConnection.connected) {
return this.createErrorResponse(
'Remote debugger connection was closed.',
[
'Use connect_remote_debugger to reconnect',
'Check if Godot editor is still running',
]
);
}
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
host: this.remoteDebugConnection.host,
port: this.remoteDebugConnection.port,
connected: this.remoteDebugConnection.connected,
output: this.remoteDebugConnection.output,
errors: this.remoteDebugConnection.errors,
rawData: this.remoteDebugConnection.rawData,
},
null,
2
),
},
],
};
}
/**
* Handle the disconnect_remote_debugger tool
*/
private async handleDisconnectRemoteDebugger() {
if (!this.remoteDebugConnection) {
return this.createErrorResponse(
'No remote debugger connection to disconnect.',
[
'There is no active connection',
'Use connect_remote_debugger to establish a connection first',
]
);
}
try {
this.logDebug('Disconnecting from remote debugger');
// Get final output before disconnecting
const finalOutput = this.remoteDebugConnection.output;
const finalErrors = this.remoteDebugConnection.errors;
const host = this.remoteDebugConnection.host;
const port = this.remoteDebugConnection.port;
// Close the socket
this.remoteDebugConnection.socket.destroy();
this.remoteDebugConnection = null;
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
message: `Disconnected from remote debugger at ${host}:${port}`,
finalOutput,
finalErrors,
},
null,
2
),
},
],
};
} catch (error: any) {
const errorMessage = error?.message || 'Unknown error';
this.remoteDebugConnection = null;
return this.createErrorResponse(
`Error while disconnecting from remote debugger: ${errorMessage}`,
[
'Connection has been reset',
'You can establish a new connection with connect_remote_debugger',
]
);
}
}
/**
* Send a DAP request through the remote debugger connection
* @param command The DAP command to send
* @param args The command arguments
* @returns Promise that resolves with the response body
*/
private async sendDAPRequest(command: string, args?: any): Promise<any> {
if (!this.remoteDebugConnection || !this.remoteDebugConnection.connected) {
throw new Error('No active remote debugger connection');
}
const seq = this.remoteDebugConnection.sequenceNumber++;
const request = {
seq,
type: 'request',
command,
arguments: args || {},
};
// Create a promise that will be resolved when we get the response
const responsePromise = new Promise((resolve, reject) => {
this.remoteDebugConnection!.pendingRequests.set(seq, { resolve, reject });
// Timeout after 10 seconds
setTimeout(() => {
if (this.remoteDebugConnection?.pendingRequests.has(seq)) {
this.remoteDebugConnection.pendingRequests.delete(seq);
reject(new Error(`DAP request timeout: ${command}`));
}
}, 10000);
});
// Send the request
const requestJson = JSON.stringify(request);
const requestMessage = `Content-Length: ${requestJson.length}\r\n\r\n${requestJson}`;
this.logDebug(`Sending DAP request: ${command}, seq: ${seq}`);
this.remoteDebugConnection.socket.write(requestMessage);
return responsePromise;
}
/**
* Execute GDScript code in the running game via DAP evaluate request
* @param expression The GDScript expression to evaluate
* @returns Promise that resolves with the evaluation result
*/
private async evaluateGDScript(expression: string): Promise<any> {
try {
const result = await this.sendDAPRequest('evaluate', {
expression,
context: 'repl',
});
this.logDebug(`Evaluate result: ${JSON.stringify(result).substring(0, 200)}`);
return result;
} catch (error: any) {
this.logDebug(`Evaluate error: ${error?.message}`);
throw error;
}
}
/**
* Handle the capture_screenshot tool (remote debugger version)
* @param args Tool arguments
*/
private async handleCaptureScreenshot(args: any) {
try {
// Normalize parameters to camelCase
args = this.normalizeParameters(args);
// Check if remote debugger is connected
if (!this.remoteDebugConnection || !this.remoteDebugConnection.connected) {
return this.createErrorResponse(
'No active remote debugger connection',
[
'Use connect_remote_debugger to establish a connection first',
'Make sure the game is running in the Godot editor (F5)',
]
);
}
// Set default format
const format = args.format || 'png';
this.logDebug(`Capturing screenshot via remote debugger (format: ${format})`);
// GDScript code to capture screenshot and encode to base64
const gdscript = `
var _capture_screenshot_result = func():
var viewport = get_tree().root.get_viewport()
if not viewport:
return {"error": "Failed to get viewport"}
var img = viewport.get_texture().get_image()
if not img:
return {"error": "Failed to get image from viewport"}
var buffer = PackedByteArray()
if "${format}" == "jpg":
buffer = img.save_jpg_to_buffer(0.9)
else:
buffer = img.save_png_to_buffer()
if buffer.is_empty():
return {"error": "Failed to encode image"}
var base64 = Marshalls.raw_to_base64(buffer)
return {"success": true, "data": base64, "size": buffer.size(), "format": "${format}"}
_capture_screenshot_result.call()
`.trim();
// Execute the GDScript via DAP evaluate
const result = await this.evaluateGDScript(gdscript);
this.logDebug(`Screenshot capture result: ${JSON.stringify(result).substring(0, 200)}`);
// Parse the result
if (!result || !result.result) {
return this.createErrorResponse(
'Failed to capture screenshot: No result from debugger',
[
'The game may not be running',
'Try reconnecting the debugger',
]
);
}
// The result.result should contain the stringified JSON
let screenshotData;
try {
// The DAP result might be a string representation, try to parse it
const resultStr = result.result;
// Try to extract JSON from the result (it might be wrapped in quotes or other formatting)
const jsonMatch = resultStr.match(/\{.*\}/s);
if (jsonMatch) {
screenshotData = JSON.parse(jsonMatch[0]);
} else {
screenshotData = JSON.parse(resultStr);
}
} catch (parseError) {
this.logDebug(`Failed to parse screenshot result: ${parseError}`);
return this.createErrorResponse(
`Failed to parse screenshot result: ${result.result}`,
[
'The screenshot data may be malformed',
'Check the debugger output for details',
]
);
}
// Check for errors
if (screenshotData.error) {
return this.createErrorResponse(
`Screenshot capture failed: ${screenshotData.error}`,
[
'Make sure the game is running',
'The viewport must be active to capture screenshots',
]
);
}
if (!screenshotData.success || !screenshotData.data) {
return this.createErrorResponse(
'Screenshot capture failed: No data returned',
[
'Make sure the game is running',
'Try capturing again',
]
);
}
const mimeType = format === 'jpg' ? 'image/jpeg' : 'image/png';
return {
content: [
{
type: 'image',
data: screenshotData.data,
mimeType,
},
{
type: 'text',
text: `Screenshot captured successfully via remote debugger (${screenshotData.format.toUpperCase()}, ${screenshotData.size} bytes)`,
},
],
};
} catch (error: any) {
return this.createErrorResponse(
`Failed to capture screenshot: ${error?.message || 'Unknown error'}`,
[
'Ensure the remote debugger is connected (use connect_remote_debugger)',
'Make sure the game is running in the Godot editor',
'Check if the viewport is accessible',
]
);
}
}
/**
* Run the MCP server
*/
async run() {
try {
// Detect Godot path before starting the server
await this.detectGodotPath();
if (!this.godotPath) {
console.error('[SERVER] Failed to find a valid Godot executable path');
console.error('[SERVER] Please set GODOT_PATH environment variable or provide a valid path');
process.exit(1);
}
// Check if the path is valid
const isValid = await this.isValidGodotPath(this.godotPath);
if (!isValid) {
if (this.strictPathValidation) {
// In strict mode, exit if the path is invalid
console.error(`[SERVER] Invalid Godot path: ${this.godotPath}`);
console.error('[SERVER] Please set a valid GODOT_PATH environment variable or provide a valid path');
process.exit(1);
} else {
// In compatibility mode, warn but continue with the default path
console.warn(`[SERVER] Warning: Using potentially invalid Godot path: ${this.godotPath}`);
console.warn('[SERVER] This may cause issues when executing Godot commands');
console.warn('[SERVER] This fallback behavior will be removed in a future version. Set strictPathValidation: true to opt-in to the new behavior.');
}
}
console.log(`[SERVER] Using Godot at: ${this.godotPath}`);
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Godot MCP server running on stdio');
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('[SERVER] Failed to start:', errorMessage);
process.exit(1);
}
}
}
// Create and run the server
const server = new GodotServer();
server.run().catch((error: unknown) => {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error('Failed to run server:', errorMessage);
process.exit(1);
});