Skip to main content
Glama
index.ts91 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); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LeeSinLiang/godot-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server