diff --git a/README.md b/README.md index 4244a5019..77c22c9e6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ # Godot MCP +[![](https://badge.mcpx.dev?type=server 'MCP Server')](https://modelcontextprotocol.io/introduction) +[![Made with Godot](https://img.shields.io/badge/Made%20with-Godot-478CBF?style=flat&logo=godot%20engine&logoColor=white)](https://godotengine.org) +[![](https://img.shields.io/badge/Node.js-339933?style=flat&logo=nodedotjs&logoColor=white 'Node.js')](https://nodejs.org/en/download/) +[![](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white 'TypeScript')](https://www.typescriptlang.org/) + +[![](https://img.shields.io/github/last-commit/Coding-Solo/godot-mcp 'Last Commit')](https://github.com/Coding-Solo/godot-mcp/commits/main) +[![](https://img.shields.io/github/stars/Coding-Solo/godot-mcp 'Stars')](https://github.com/Coding-Solo/godot-mcp/stargazers) +[![](https://img.shields.io/github/forks/Coding-Solo/godot-mcp 'Forks')](https://github.com/Coding-Solo/godot-mcp/network/members) +[![](https://img.shields.io/badge/License-MIT-red.svg 'MIT License')](https://opensource.org/licenses/MIT) + ```text ((((((( ((((((( ((((((((((( ((((((((((( @@ -216,6 +226,10 @@ The bundled script accepts operation type and parameters as JSON, allowing for f - **Connection Issues**: Ensure the server is running and restart your AI assistant - **Invalid Project Path**: Ensure the path points to a directory containing a project.godot file - **Build Issues**: Make sure all dependencies are installed by running `npm install` +- **For Cursor Specifically**: +- Ensure the MCP server shows up and is enabled in Cursor settings (Settings > MCP) +- MCP tools can only be run using the Agent chat profile (Cursor Pro or Business subscription) +- Use "Yolo Mode" to automatically run MCP tool requests ## License diff --git a/package-lock.json b/package-lock.json index 58c8ad0ab..5261a340f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@modelcontextprotocol/sdk": "0.6.0", "axios": "^1.7.9", - "fs-extra": "^11.2.0" + "fs-extra": "^11.2.0", + "godot-mcp": "file:" }, "bin": { "godot-mcp": "build/index.js" @@ -274,6 +275,10 @@ "node": ">= 0.4" } }, + "node_modules/godot-mcp": { + "resolved": "", + "link": true + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", diff --git a/package.json b/package.json index 098ec76fb..98d026aa0 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "dependencies": { "@modelcontextprotocol/sdk": "0.6.0", "axios": "^1.7.9", - "fs-extra": "^11.2.0" + "fs-extra": "^11.2.0", + "godot-mcp": "file:" }, "devDependencies": { "@types/node": "^20.11.24", diff --git a/src/index.ts b/src/index.ts index 05dd3da60..e15537740 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ #!/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. @@ -13,6 +13,7 @@ import { existsSync, readdirSync, mkdirSync } from 'fs'; import { spawn } from 'child_process'; import { promisify } from 'util'; import { exec } from 'child_process'; +import * as fs from 'fs'; // Import the 'fs' module import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; @@ -69,9 +70,9 @@ class GodotServer { private operationsScriptPath: string; private validatedPaths: Map = new Map(); private strictPathValidation: boolean = false; - + /** - * Parameter name mappings from snake_case to camelCase + * Parameter name mappings between snake_case and camelCase * This allows the server to accept both formats */ private parameterMappings: Record = { @@ -86,14 +87,27 @@ class GodotServer { 'output_path': 'outputPath', 'mesh_item_names': 'meshItemNames', 'new_path': 'newPath', - 'file_path': 'filePath' + 'file_path': 'filePath', + 'directory': 'directory', + 'recursive': 'recursive', + 'scene': 'scene', }; + /** + * Reverse mapping from camelCase to snake_case + * Generated from parameterMappings for quick lookups + */ + private reverseParameterMappings: Record = {}; + 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; @@ -104,13 +118,13 @@ class GodotServer { 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}`); @@ -118,7 +132,7 @@ class GodotServer { } } } - + // 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}`); @@ -138,10 +152,10 @@ class GodotServer { // 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(); @@ -167,7 +181,7 @@ class GodotServer { if (possibleSolutions.length > 0) { console.error(`[SERVER] Possible solutions: ${possibleSolutions.join(', ')}`); } - + const response: any = { content: [ { @@ -177,14 +191,14 @@ class GodotServer { ], isError: true, }; - + if (possibleSolutions.length > 0) { response.content.push({ type: 'text', text: 'Possible solutions:\n- ' + possibleSolutions.join('\n- '), }); } - + return response; } @@ -196,7 +210,7 @@ class GodotServer { if (!path || path.includes('..')) { return false; } - + // Add more validation as needed return true; } @@ -226,21 +240,21 @@ class GodotServer { 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; @@ -260,7 +274,7 @@ class GodotServer { 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); @@ -277,7 +291,7 @@ class GodotServer { // 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 ]; @@ -321,7 +335,7 @@ class GodotServer { 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.`); @@ -334,7 +348,7 @@ class GodotServer { } 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.`); @@ -350,7 +364,7 @@ class GodotServer { 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); @@ -359,7 +373,7 @@ class GodotServer { this.logDebug(`Godot path set to: ${normalizedPath}`); return true; } - + this.logDebug(`Failed to set invalid Godot path: ${normalizedPath}`); return false; } @@ -392,6 +406,64 @@ class GodotServer { 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 @@ -400,13 +472,18 @@ class GodotServer { * @returns The stdout and stderr from the operation */ private async executeOperation( - operation: string, - params: OperationParams, + operation: string, + params: OperationParams, projectPath: string - ): Promise<{stdout: string, stderr: string}> { + ): Promise<{ stdout: string; stderr: string }> { this.logDebug(`Executing operation: ${operation} in project: ${projectPath}`); - this.logDebug(`Operation params: ${JSON.stringify(params)}`); - + 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(); @@ -414,959 +491,810 @@ class GodotServer { throw new Error('Could not find a valid Godot executable path'); } } - + try { - // Escape single quotes in the JSON string to prevent command injection - const escapedParams = JSON.stringify(params).replace(/'/g, "'\\''"); - - // Add debug arguments if debug mode is enabled - const debugArgs = GODOT_DEBUG_MODE ? ["--debug-godot"] : []; - - const cmd = [ - `"${this.godotPath}"`, + // Serialize the snake_case parameters to a valid JSON string + const paramsJson = JSON.stringify(snakeCaseParams); + // NO escaping needed when shell: false + + // Construct arguments array for spawn (shell: false) + const args: string[] = [ '--headless', + '--path', + projectPath, // spawn handles spaces in paths correctly when shell: false '--script', - `"${this.operationsScriptPath}"`, + this.operationsScriptPath, operation, - `'${escapedParams}'`, - '--path', - `"${projectPath}"`, - ...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 - }; + paramsJson, // Pass the raw, unescaped JSON string + ]; + + // Add debug flag if enabled + if (GODOT_DEBUG_MODE) { + args.push('--debug-godot'); } - - throw error; + + // *** START DEBUG LOGGING *** + this.logDebug(`[executeOperation] Operation: ${operation}`); + this.logDebug(`[executeOperation] Original camelCase params: ${JSON.stringify(params)}`); + this.logDebug(`[executeOperation] Converted snake_case params: ${JSON.stringify(snakeCaseParams)}`); + this.logDebug(`[executeOperation] Serialized JSON for Godot (passed directly): ${paramsJson}`); + this.logDebug(`[executeOperation] Final arguments array for spawn (shell: false): ${JSON.stringify(args)}`); + this.logDebug(`[executeOperation] Spawning command (shell: false): ${this.godotPath}`); + // *** END DEBUG LOGGING *** + + // Use spawn WITH shell: false + // Ensure godotPath does not have extra quotes if it's a direct path or is in PATH + const commandToSpawn = this.godotPath; // Use the path directly + + this.logDebug(`Spawning Godot (shell: false): "${commandToSpawn}" with args: ${JSON.stringify(args)}`); + + const godotProcess = spawn(commandToSpawn, args, { + shell: false, // Explicitly set to false + stdio: ['pipe', 'pipe', 'pipe'], // Capture stdout, stderr + }); + + let stdout = ''; + let stderr = ''; + + godotProcess.stdout.on('data', (data) => { + const output = data.toString(); + stdout += output; + this.logDebug(`Godot stdout: ${output}`); + }); + + godotProcess.stderr.on('data', (data) => { + const errorOutput = data.toString(); + stderr += errorOutput; + console.error(`Godot stderr: ${errorOutput}`); + }); + + return new Promise((resolve, reject) => { + godotProcess.on('close', (code) => { + this.logDebug(`Godot process exited with code ${code}`); + if (code === 0) { + resolve({ stdout, stderr }); + } else { + // Include stderr in the rejection error for more context + reject(new Error(`Godot process exited with code ${code}. Stderr: ${stderr || 'N/A'}`)); + } + }); + + godotProcess.on('error', (err) => { + console.error('Failed to start Godot process:', err); + reject(err); + }); + }); + } catch (error: any) { + console.error(`Error executing Godot operation: ${error.message}`); + throw error; // Re-throw the error to be handled by the caller } } /** - * Get the structure of a Godot project + * Get the project structure (scenes and scripts) * @param projectPath Path to the Godot project - * @returns Object representing the project structure + * @returns Project structure information */ private async getProjectStructure(projectPath: string): Promise { - 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); + this.logDebug(`Getting project structure for: ${projectPath}`); + if (!this.validatePath(projectPath)) { + throw new Error('Invalid project path'); + } + + const structure: any = { scenes: [], scripts: [] }; + const readDirRecursive = (dir: string) => { + try { + const entries = readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = join(dir, entry.name); + const relativePath = fullPath.substring(projectPath.length + 1).replace(/\\/g, '/'); // Relative path with forward slashes + + if (entry.isDirectory()) { + // Skip common hidden/system directories + if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.vscode' || entry.name === '.godot') { + continue; + } + readDirRecursive(fullPath); + } else if (entry.isFile()) { + if (entry.name.endsWith('.tscn') || entry.name.endsWith('.scn')) { + structure.scenes.push({ name: entry.name, path: relativePath }); + } else if (entry.name.endsWith('.gd')) { + structure.scripts.push({ name: entry.name, path: relativePath }); + } } } + } catch (error: any) { + console.error(`Error reading directory ${dir}: ${error.message}`); + // Optionally re-throw or handle specific errors like permission denied } - - return structure; - } catch (error) { - this.logDebug(`Error getting project structure: ${error}`); - return { error: 'Failed to get project structure' }; - } + }; + + readDirRecursive(projectPath); + this.logDebug(`Project structure retrieved: ${JSON.stringify(structure)}`); + return structure; } + /** - * Find Godot projects in a directory - * @param directory Directory to search + * Find Godot projects within a directory + * @param directory The directory to search in * @param recursive Whether to search recursively - * @returns Array of Godot projects + * @returns An array of found Godot projects with their paths and names */ - 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), - }); - } + private findGodotProjects(directory: string, recursive: boolean): Array<{ path: string; name: string }> { + this.logDebug(`Finding Godot projects in: ${directory}, recursive: ${recursive}`); + if (!this.validatePath(directory)) { + throw new Error('Invalid directory path'); + } - // If not recursive, only check immediate subdirectories - if (!recursive) { - const entries = readdirSync(directory, { withFileTypes: true }); + const projects: Array<{ path: string; name: string }> = []; + const searchDir = (currentDir: string, depth: number) => { + try { + const entries = readdirSync(currentDir, { withFileTypes: true }); for (const entry of entries) { + const fullPath = join(currentDir, entry.name); 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, - }); + // Check if this directory contains a project.godot file + if (existsSync(join(fullPath, 'project.godot'))) { + projects.push({ path: fullPath, name: basename(fullPath) }); + // If not recursive, stop searching deeper in this branch + if (!recursive) continue; } - } - } - } 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; + // Recurse if allowed + if (recursive) { + // Skip common hidden/system directories + if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.vscode' || entry.name === '.godot') { + continue; + } + searchDir(fullPath, depth + 1); } - // 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); + } else if (entry.isFile() && entry.name === 'project.godot' && depth === 0) { + // Found project.godot in the starting directory itself + // Avoid adding the starting directory if it was already added by finding project.godot inside it + if (!projects.some(p => p.path === currentDir)) { + projects.push({ path: currentDir, name: basename(currentDir) }); } } } + } catch (error: any) { + console.error(`Error searching directory ${currentDir}: ${error.message}`); } - } catch (error) { - this.logDebug(`Error searching directory ${directory}: ${error}`); - } + }; + searchDir(directory, 0); + this.logDebug(`Found projects: ${JSON.stringify(projects)}`); return projects; } /** - * Set up the tool handlers for the MCP server + * Set up handlers for all available tools */ 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'], + this.logDebug('Setting up tool handlers'); + + // Define tools metadata (used for ListTools and CallTool routing) + const toolDefinitions = { + launch_editor: { + description: 'Launch the Godot editor for a specific project.', + inputSchema: { + type: 'object', + properties: { + projectPath: { type: 'string', description: 'Absolute 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'], + outputSchema: { type: 'object', properties: { message: { type: 'string' } } }, + handler: this.handleLaunchEditor.bind(this), + }, + run_project: { + description: 'Run a Godot project.', + inputSchema: { + type: 'object', + properties: { + projectPath: { type: 'string', description: 'Absolute path to the Godot project directory.' }, + debug: { type: 'boolean', description: 'Run with debug enabled.', default: false }, }, + required: ['projectPath'], }, - { - 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'], + outputSchema: { type: 'object', properties: { message: { type: 'string' }, pid: { type: 'number' } } }, + handler: this.handleRunProject.bind(this), + }, + get_debug_output: { + description: 'Get the captured debug output from the last run project.', + inputSchema: { type: 'object', properties: {} }, + outputSchema: { type: 'object', properties: { output: { type: 'array', items: { type: 'string' } }, errors: { type: 'array', items: { type: 'string' } } } }, + handler: this.handleGetDebugOutput.bind(this), + }, + stop_project: { + description: 'Stop the currently running Godot project.', + inputSchema: { type: 'object', properties: {} }, + outputSchema: { type: 'object', properties: { message: { type: 'string' } } }, + handler: this.handleStopProject.bind(this), + }, + get_godot_version: { + description: 'Get the version of the configured Godot executable.', + inputSchema: { type: 'object', properties: {} }, + outputSchema: { type: 'object', properties: { version: { type: 'string' } } }, + handler: this.handleGetGodotVersion.bind(this), + }, + list_projects: { + description: 'List Godot projects found in a specified directory.', + inputSchema: { + type: 'object', + properties: { + directory: { type: 'string', description: 'The directory to search for projects.' }, + recursive: { type: 'boolean', description: 'Search recursively.', default: false }, + }, + required: ['directory'], + }, + outputSchema: { + type: 'object', + properties: { + projects: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + path: { type: 'string' }, + }, + required: ['name', 'path'], + }, + }, + }, + }, + handler: this.handleListProjects.bind(this), + }, + get_project_info: { + description: 'Get information about a Godot project, including scenes and scripts.', + inputSchema: { + type: 'object', + properties: { + projectPath: { type: 'string', description: 'Absolute path to the Godot project directory.' }, + }, + required: ['projectPath'], + }, + outputSchema: { + type: 'object', + properties: { + scenes: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, path: { type: 'string' } } } }, + scripts: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, path: { type: 'string' } } } }, + }, + }, + handler: this.handleGetProjectInfo.bind(this), + }, + create_scene: { + description: 'Create a new scene file (.tscn) with a specified root node type.', + inputSchema: { + type: 'object', + properties: { + projectPath: { type: 'string', description: 'Absolute path to the Godot project directory.' }, + scenePath: { type: 'string', description: 'Relative path within the project (e.g., "scenes/main.tscn").' }, + rootNodeType: { type: 'string', description: 'The type of the root node (e.g., "Node2D", "Control").', default: 'Node2D' }, }, + required: ['projectPath', 'scenePath'], }, - { - 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'], + outputSchema: { type: 'object', properties: { message: { type: 'string' }, scenePath: { type: 'string' } } }, + handler: this.handleCreateScene.bind(this), + }, + add_node: { + description: 'Add a new node to an existing scene.', + inputSchema: { + type: 'object', + properties: { + projectPath: { type: 'string', description: 'Absolute path to the Godot project directory.' }, + scenePath: { type: 'string', description: 'Relative path to the scene file (e.g., "scenes/main.tscn").' }, + parentNodePath: { type: 'string', description: 'NodePath to the parent node (e.g., "root/Player"). Defaults to "root".' }, + nodeType: { type: 'string', description: 'The type of node to add (e.g., "Sprite2D", "Button").' }, + nodeName: { type: 'string', description: 'The name for the new node.' }, }, + required: ['projectPath', 'scenePath', 'nodeType', 'nodeName'], }, - ], - })); - - // 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); - 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) { - 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); - }); + outputSchema: { type: 'object', properties: { message: { type: 'string' }, nodePath: { type: 'string' } } }, + handler: this.handleAddNode.bind(this), + }, + load_sprite: { + description: 'Load a sprite texture onto a Sprite2D node in a scene.', + inputSchema: { + type: 'object', + properties: { + projectPath: { type: 'string', description: 'Absolute path to the Godot project directory.' }, + scenePath: { type: 'string', description: 'Relative path to the scene file.' }, + nodePath: { type: 'string', description: 'NodePath to the Sprite2D node.' }, + texturePath: { type: 'string', description: 'Resource path (res://) to the texture file.' }, + }, + required: ['projectPath', 'scenePath', 'nodePath', 'texturePath'], + }, + outputSchema: { type: 'object', properties: { message: { type: 'string' } } }, + handler: this.handleLoadSprite.bind(this), + }, + export_mesh_library: { + description: 'Export selected MeshInstance3D nodes from a scene into a MeshLibrary resource.', + inputSchema: { + type: 'object', + properties: { + projectPath: { type: 'string', description: 'Absolute path to the Godot project directory.' }, + scenePath: { type: 'string', description: 'Relative path to the scene file containing the meshes.' }, + meshItemNames: { type: 'array', items: { type: 'string' }, description: 'Array of names of the MeshInstance3D nodes to include.' }, + outputPath: { type: 'string', description: 'Resource path (res://) for the output MeshLibrary file (e.g., "meshlibs/level1.meshlib").' }, + }, + required: ['projectPath', 'scenePath', 'meshItemNames', 'outputPath'], + }, + outputSchema: { type: 'object', properties: { message: { type: 'string' }, outputPath: { type: 'string' } } }, + handler: this.handleExportMeshLibrary.bind(this), + }, + save_scene: { + description: 'Save changes made to a scene.', + inputSchema: { + type: 'object', + properties: { + projectPath: { type: 'string', description: 'Absolute path to the Godot project directory.' }, + scenePath: { type: 'string', description: 'Relative path to the scene file to save.' }, + newPath: { type: 'string', description: '(Optional) New relative path to save the scene as (Save As).' }, + }, + required: ['projectPath', 'scenePath'], + }, + outputSchema: { type: 'object', properties: { message: { type: 'string' }, savedPath: { type: 'string' } } }, + handler: this.handleSaveScene.bind(this), + }, + get_uid: { + description: 'Get the UID (Unique ID) for a resource path.', + inputSchema: { + type: 'object', + properties: { + projectPath: { type: 'string', description: 'Absolute path to the Godot project directory.' }, + filePath: { type: 'string', description: 'Resource path (res://) to the file.' }, + }, + required: ['projectPath', 'filePath'], + }, + outputSchema: { type: 'object', properties: { uid: { type: 'string' } } }, // UID might be a string or number depending on Godot version/context + handler: this.handleGetUid.bind(this), + }, + resave_resources: { + description: 'Resave all resources in a project to update UIDs and dependencies. Use with caution.', + inputSchema: { + type: 'object', + properties: { + projectPath: { type: 'string', description: 'Absolute path to the Godot project directory.' }, + }, + required: ['projectPath'], + }, + outputSchema: { type: 'object', properties: { message: { type: 'string' } } }, + handler: this.handleUpdateProjectUids.bind(this), // Assuming this handler does the resaving + }, + }; + // Handler for listing available tools + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + this.logDebug('Handling ListToolsRequest'); return { - content: [ - { - type: 'text', - text: `Godot editor launched successfully for project at ${args.projectPath}.`, - }, - ], + tools: Object.entries(toolDefinitions).map(([name, def]) => ({ + name: name, + description: def.description, + inputSchema: def.inputSchema, + // outputSchema is often omitted in ListTools response, but include if needed + })), }; - } 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) { - 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' - ] - ); - } + // Handler for calling a specific tool + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const toolName = request.params.name; + const args = request.params.arguments; + this.logDebug(`Handling CallToolRequest for tool: ${toolName} with args: ${JSON.stringify(args)}`); - // Kill any existing process - if (this.activeProcess) { - this.logDebug('Killing existing Godot process before starting a new one'); - this.activeProcess.process.kill(); - } + const toolDef = toolDefinitions[toolName as keyof typeof toolDefinitions]; - const cmdArgs = ['-d', '--path', args.projectPath]; - if (args.scene && this.validatePath(args.scene)) { - this.logDebug(`Adding scene parameter: ${args.scene}`); - cmdArgs.push(args.scene); + if (!toolDef) { + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`); } - 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; + try { + // Ensure Godot path is detected before executing any tool handler + if (!this.godotPath) { + await this.detectGodotPath(); } - }); - - process.on('error', (err: Error) => { - console.error('Failed to start Godot process:', err); - if (this.activeProcess && this.activeProcess.process === process) { - this.activeProcess = null; + // Call the specific handler associated with the tool name + const result = await toolDef.handler(args); // Assuming handlers return the expected structure + + // Ensure the result has a 'content' property which is an array + if (!result || !Array.isArray(result.content)) { + console.warn(`Tool handler for '${toolName}' returned an unexpected structure. Wrapping in standard response.`); + // Attempt to create a standard response structure + const textContent = typeof result === 'string' ? result : JSON.stringify(result); + return { + content: [{ type: 'text', text: textContent }], + // Include other properties from the original result if they exist + ...(typeof result === 'object' && result !== null ? result : {}) + }; } - }); - this.activeProcess = { process, output, errors }; + return result; // Return the result from the specific handler + + } catch (error: any) { + console.error(`[Handler Error - ${toolName}] ${error.message}`); + // Error handling logic (copied from the original loop, adjust as needed) + let solutions: string[] = []; + if (error.message.includes('Could not find a valid Godot executable')) { + solutions = [ + 'Ensure Godot is installed and accessible.', + 'Set the GODOT_PATH environment variable to the full path of the Godot executable.', + 'Provide the correct `godotPath` in the server configuration.', + ]; + } else if (error.message.includes('Invalid project path') || error.message.includes('ENOENT')) { + solutions = [ + 'Verify the provided `projectPath` is correct and exists.', + 'Ensure the server has permissions to access the project directory.', + ]; + } else if (error.message.includes('Failed to parse JSON')) { + solutions = [ + 'Check the format of the parameters being sent to the Godot script.', + 'Ensure proper escaping of arguments passed via the command line.', + ]; + } else if (error.message.includes('Godot process exited with code')) { + solutions = [ + 'Check the Godot stderr output in the server logs for specific errors from the engine or script.', + 'Ensure the `godot_operations.gd` script is correctly placed and has no syntax errors.', + 'Verify file paths and permissions within the Godot project.', + ]; + } + + // Use the existing createErrorResponse method + return this.createErrorResponse( + `Error executing tool '${toolName}': ${error.message}`, + solutions + ); + } + }); - 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' - ] - ); - } + this.logDebug('Tool handlers set up using setRequestHandler'); } - /** - * 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 - ), - }, - ], - }; - } + // --- Tool Handler Implementations --- /** - * Handle the stop_project tool + * Handle the launch_editor 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 - ), - }, - ], - }; - } + private async handleLaunchEditor(args: any) { + this.logDebug(`Handling launch_editor: ${JSON.stringify(args)}`); + args = this.normalizeParameters(args); // Normalize parameters - /** - * 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' - ] - ); + if (!this.validatePath(args.projectPath)) { + return this.createErrorResponse('Invalid project path provided.'); } - } - - /** - * Handle the list_projects tool - */ - private async handleListProjects(args: any) { - if (!args.directory) { - return this.createErrorResponse( - 'Directory is required', - ['Provide a valid directory path to search for Godot projects'] - ); + if (!this.godotPath) { + return this.createErrorResponse('Godot executable path not found.'); } - if (!this.validatePath(args.directory)) { - return this.createErrorResponse( - 'Invalid directory path', - ['Provide a valid path without ".." or other potentially unsafe characters'] - ); + // Check if a process is already running + if (this.activeProcess) { + return this.createErrorResponse('Another Godot process (editor or project) is already running.'); } + const command = `"${this.godotPath}" --editor --path "${args.projectPath}"`; + this.logDebug(`Executing: ${command}`); + 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'] - ); + // Use exec for launching the editor as it's typically a one-off process + const { stdout, stderr } = await execAsync(command); + this.logDebug(`Editor stdout: ${stdout}`); + if (stderr) { + console.error(`Editor stderr: ${stderr}`); } - - const recursive = args.recursive === true; - const projects = this.findGodotProjects(args.directory, recursive); - - return { - content: [ - { - type: 'text', - text: JSON.stringify(projects, null, 2), - }, - ], - }; + return { content: [{ type: 'text', text: 'Godot editor launched successfully.' }] }; } 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' - ] - ); + console.error(`Error launching editor: ${error.message}`); + return this.createErrorResponse(`Failed to launch Godot editor: ${error.message}`); } } /** - * Handle the get_project_info tool + * Handle the run_project tool */ - private async handleGetProjectInfo(args: any) { - if (!args.projectPath) { - return this.createErrorResponse( - 'Project path is required', - ['Provide a valid path to a Godot project directory'] - ); - } + private async handleRunProject(args: any) { + this.logDebug(`Handling run_project: ${JSON.stringify(args)}`); + args = this.normalizeParameters(args); // Normalize parameters + + if (!this.validatePath(args.projectPath)) { + return this.createErrorResponse('Invalid project path provided.'); + } + if (!this.godotPath) { + return this.createErrorResponse('Godot executable path not found.'); + } + + if (this.activeProcess) { + return this.createErrorResponse('A Godot project is already running. Stop it first using stop_project.'); + } + + const commandArgs = ['--path', args.projectPath]; + if (args.debug || GODOT_DEBUG_MODE) { // Use debug if requested or globally enabled + commandArgs.push('--debug'); + } + + // Define the log file path within the project directory + const logFilePath = join(args.projectPath, 'cline_godot_run.log'); + // Add the --log-file argument for Godot + commandArgs.push('--log-file', logFilePath); + this.logDebug(`Using Godot's --log-file argument: ${logFilePath}`); + + + this.logDebug(`Spawning Godot project: "${this.godotPath}" with args: ${commandArgs.join(' ')}`); + + try { + // Use spawn, revert stdio back to pipes to capture potential early launch errors + const godotProcess = spawn(this.godotPath, commandArgs, { + shell: false, // Keep shell false + detached: false, // Keep attached to monitor exit/errors more easily initially + stdio: ['ignore', 'pipe', 'pipe'], // Ignore stdin, capture stdout/stderr via pipes + }); + + // No need to unref if not detached + + this.activeProcess = { + process: godotProcess, + output: [], // Capture output via pipes again + errors: [], // Capture errors via pipes again + }; + + // Capture piped output/error + godotProcess.stdout.on('data', (data) => { + const output = data.toString(); + this.activeProcess?.output.push(output); + // Optionally log to server console, but primary log is the file + // this.logDebug(`Project stdout pipe: ${output}`); + }); + + godotProcess.stderr.on('data', (data) => { + const errorOutput = data.toString(); + this.activeProcess?.errors.push(errorOutput); + // Log stderr immediately to server console for visibility + console.error(`Project stderr pipe: ${errorOutput}`); + }); + + godotProcess.on('close', (code) => { + this.logDebug(`Godot project process exited with code ${code}`); + this.activeProcess = null; // Clear active process when it closes + }); + + godotProcess.on('error', (err) => { + console.error('Failed to start Godot project process:', err); + this.activeProcess = null; // Clear on error too + // We might not be able to return an error directly here as the handler might have already returned + }); + + return { + content: [{ type: 'text', text: `Godot project started (PID: ${godotProcess.pid}).` }], + pid: godotProcess.pid + }; + } catch (error: any) { + console.error(`Error running project: ${error.message}`); + this.activeProcess = null; + return this.createErrorResponse(`Failed to run Godot project: ${error.message}`); + } + } - if (!this.validatePath(args.projectPath)) { - return this.createErrorResponse( - 'Invalid project path', - ['Provide a valid path without ".." or other potentially unsafe characters'] - ); - } + /** + * Handle the get_debug_output tool + */ + private async handleGetDebugOutput() { + this.logDebug('Handling get_debug_output'); + if (this.activeProcess) { + // Limit the number of error lines returned + const maxErrorLines = 100; // Keep the last 100 error lines + const totalErrorLines = this.activeProcess.errors.length; + const recentErrors = this.activeProcess.errors.slice(-maxErrorLines); + const recentErrorsText = recentErrors.join('\n'); + + let responseText = `--- Recent Errors (${Math.min(maxErrorLines, totalErrorLines)}/${totalErrorLines} lines shown) ---\n`; + responseText += recentErrorsText || '(No recent errors)'; + + // Also include total output line count for context + const totalOutputLines = this.activeProcess.output.length; + responseText += `\n(Total output lines: ${totalOutputLines})`; + + + return { + content: [ + { type: 'text', text: responseText } + ], + // Return only the sliced recent errors and a limited amount of recent output for context if needed + // For now, let's keep the full arrays but the primary content is limited + output: [...this.activeProcess.output], // Keep full output array for now + errors: [...this.activeProcess.errors] // Keep full error array for now + }; + } else { + return this.createErrorResponse('No active Godot project is running.'); + } + } - 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' - ] - ); + /** + * Handle the stop_project tool + */ + private async handleStopProject() { + this.logDebug('Handling stop_project'); + if (this.activeProcess) { + this.logDebug(`Attempting to kill process with PID: ${this.activeProcess.process.pid}`); + const killed = this.activeProcess.process.kill(); // Sends SIGTERM by default + if (killed) { + this.logDebug('Kill signal sent successfully.'); + // Give it a moment, then force kill if necessary (optional) + // setTimeout(() => { + // if (this.activeProcess && !this.activeProcess.process.killed) { + // this.logDebug('Process did not terminate, sending SIGKILL.'); + // this.activeProcess.process.kill('SIGKILL'); + // } + // }, 1000); + this.activeProcess = null; // Assume killed for now, will be cleared on 'close' anyway + return { content: [{ type: 'text', text: 'Stop signal sent to Godot project.' }] }; + } else { + this.logDebug('Failed to send kill signal.'); + // Attempt cleanup anyway + this.activeProcess = null; + return this.createErrorResponse('Failed to send stop signal to the Godot process. It might have already exited.'); } - - this.logDebug(`Getting project info for: ${args.projectPath}`); - - // Get project version - const { stdout } = await execAsync(`"${this.godotPath}" --headless --path "${args.projectPath}" --version-project`); - - // Get project structure - const projectStructure = await this.getProjectStructure(args.projectPath); - - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - name: args.projectPath.split('/').pop(), - 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' - ] - ); + } else { + return this.createErrorResponse('No active Godot project is running.'); } } + /** + * Handle the get_godot_version tool + */ + private async handleGetGodotVersion() { + this.logDebug('Handling get_godot_version'); + if (!this.godotPath) { + // Attempt detection if not already set + await this.detectGodotPath(); + if (!this.godotPath) { + return this.createErrorResponse('Godot executable path not found.'); + } + } + + try { + const command = `"${this.godotPath}" --version`; + this.logDebug(`Executing: ${command}`); + const { stdout } = await execAsync(command); + const version = stdout.trim(); + this.logDebug(`Godot version: ${version}`); + return { content: [{ type: 'text', text: `Godot version: ${version}` }], version: version }; + } catch (error: any) { + console.error(`Error getting Godot version: ${error.message}`); + return this.createErrorResponse(`Failed to get Godot version: ${error.message}`); + } + } + + /** + * Handle the list_projects tool + */ + private async handleListProjects(args: any) { + this.logDebug(`Handling list_projects: ${JSON.stringify(args)}`); + args = this.normalizeParameters(args); // Normalize parameters + + if (!this.validatePath(args.directory)) { + return this.createErrorResponse('Invalid directory path provided.'); + } + + try { + const projects = this.findGodotProjects(args.directory, args.recursive ?? false); + return { + content: [{ type: 'text', text: `Found ${projects.length} projects.` }], + projects: projects + }; + } catch (error: any) { + console.error(`Error listing projects: ${error.message}`); + return this.createErrorResponse(`Failed to list projects: ${error.message}`); + } + } + + + /** + * Asynchronously get project structure (wrapper for sync method) + * @param projectPath Path to the Godot project + * @returns Promise resolving with project structure + */ + private getProjectStructureAsync(projectPath: string): Promise { + return new Promise((resolve, reject) => { + try { + const structure = this.getProjectStructure(projectPath); + resolve(structure); + } catch (error) { + reject(error); + } + }); + } + + + /** + * Handle the get_project_info tool + */ + private async handleGetProjectInfo(args: any) { + this.logDebug(`Handling get_project_info: ${JSON.stringify(args)}`); + args = this.normalizeParameters(args); // Normalize parameters + + if (!this.validatePath(args.projectPath)) { + return this.createErrorResponse('Invalid project path provided.'); + } + + // Basic check for project.godot file + const projectFilePath = join(args.projectPath, 'project.godot'); + if (!existsSync(projectFilePath)) { + return this.createErrorResponse(`Not a valid Godot project directory (missing project.godot): ${args.projectPath}`); + } + + + try { + const structure = await this.getProjectStructureAsync(args.projectPath); + return { + content: [{ type: 'text', text: `Found ${structure.scenes.length} scenes and ${structure.scripts.length} scripts.` }], + scenes: structure.scenes, + scripts: structure.scripts + }; + } catch (error: any) { + console.error(`Error getting project info: ${error.message}`); + return this.createErrorResponse(`Failed to get project info: ${error.message}`); + } + } + /** * Handle the create_scene tool */ private async handleCreateScene(args: any) { + this.logDebug(`[handleCreateScene] Received raw args: ${JSON.stringify(args)}`); + // 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'] + ['Provide both `projectPath` (absolute) and `scenePath` (relative).'] ); } - - if (!this.validatePath(args.projectPath) || !this.validatePath(args.scenePath)) { - return this.createErrorResponse( - 'Invalid path', - ['Provide valid paths without ".." or other potentially unsafe characters'] - ); + if (!this.validatePath(args.projectPath)) { + return this.createErrorResponse('Invalid project path provided.'); } + // Validate scenePath basic structure (prevent traversal, ensure .tscn/.scn) + if (args.scenePath.includes('..') || (!args.scenePath.endsWith('.tscn') && !args.scenePath.endsWith('.scn'))) { + return this.createErrorResponse('Invalid scene path. Must be relative, end with .tscn or .scn, and not contain "..".'); + } - 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 - const params = { + try { + const operationArgs = { scene_path: args.scenePath, - root_node_type: args.rootNodeType || 'Node2D' + root_node_type: args.rootNodeType || 'Node2D', // Default if not provided }; - - // 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' - ] - ); + const { stdout, stderr } = await this.executeOperation('create_scene', operationArgs, args.projectPath); + + // Check stderr for specific Godot errors even if the process exits with 0 + if (stderr && stderr.toLowerCase().includes('error')) { + console.error(`Godot stderr indicates potential error during scene creation: ${stderr}`); + // Try to extract a more specific error message if possible + const errorMatch = stderr.match(/ERROR: (.+)/); + const specificError = errorMatch ? errorMatch[1] : stderr; + return this.createErrorResponse(`Godot reported an error during scene creation: ${specificError}`); + } + + // Check stdout for success message (optional but good practice) + if (stdout.includes('Scene created successfully')) { + const createdPath = stdout.split('at: ')[1]?.trim() || args.scenePath; // Extract path if possible + return { + content: [{ type: 'text', text: `Scene created successfully at: ${createdPath}` }], + scenePath: createdPath + }; + } else { + // If no success message and no clear error, return a generic success/check message + console.warn(`Scene creation process completed, but success message not found in stdout. Stdout: ${stdout}`); + return { + content: [{ type: 'text', text: `Scene creation process completed for ${args.scenePath}. Verify the file exists.` }], + scenePath: args.scenePath + }; } - - 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' - ] - ); + console.error(`Error creating scene: ${error.message}`); + // Check if the error message contains stderr from the executeOperation rejection + const stderrMatch = error.message.match(/Stderr: (.+)/); + const godotError = stderrMatch ? stderrMatch[1] : 'Check server logs for details.'; + return this.createErrorResponse(`Failed to create scene: ${godotError}`); } } @@ -1374,92 +1302,63 @@ class GodotServer { * Handle the add_node tool */ private async handleAddNode(args: any) { + this.logDebug(`Handling add_node: ${JSON.stringify(args)}`); + args = this.normalizeParameters(args); // Normalize parameters + if (!args.projectPath || !args.scenePath || !args.nodeType || !args.nodeName) { return this.createErrorResponse( - 'Missing required parameters', - ['Provide projectPath, scenePath, nodeType, and nodeName'] + 'Project path, scene path, node type, and node name are required', + ['Provide `projectPath`, `scenePath`, `nodeType`, and `nodeName`.'] ); } + if (!this.validatePath(args.projectPath)) { + return this.createErrorResponse('Invalid project path provided.'); + } + if (args.scenePath.includes('..') || (!args.scenePath.endsWith('.tscn') && !args.scenePath.endsWith('.scn'))) { + return this.createErrorResponse('Invalid scene path.'); + } + if (args.parentNodePath && args.parentNodePath.includes('..')) { + return this.createErrorResponse('Invalid parent node path.'); + } + // Basic validation for node name (avoid empty or path-like names) + if (!args.nodeName || args.nodeName.includes('/') || args.nodeName.includes('\\')) { + return this.createErrorResponse('Invalid node name.'); + } - 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 - const params: any = { + const operationArgs = { scene_path: args.scenePath, + parent_node_path: args.parentNodePath || 'root', // Default to 'root' node_type: args.nodeType, - node_name: args.nodeName + node_name: args.nodeName, }; - - // Add optional parameters - if (args.parentNodePath) { - params.parent_node_path = args.parentNodePath; - } - - if (args.properties) { - params.properties = args.properties; + const { stdout, stderr } = await this.executeOperation('add_node', operationArgs, args.projectPath); + + if (stderr && stderr.toLowerCase().includes('error')) { + console.error(`Godot stderr indicates potential error during node addition: ${stderr}`); + const errorMatch = stderr.match(/ERROR: (.+)/); + const specificError = errorMatch ? errorMatch[1] : stderr; + return this.createErrorResponse(`Godot reported an error while adding node: ${specificError}`); } - - // 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' - ] - ); + + if (stdout.includes('Node added successfully')) { + const addedNodePath = stdout.split('at path: ')[1]?.trim(); + return { + content: [{ type: 'text', text: `Node '${args.nodeName}' added successfully${addedNodePath ? ` at path: ${addedNodePath}` : ''}. Remember to save the scene.` }], + nodePath: addedNodePath // Return the actual path if available + }; + } else { + console.warn(`Add node process completed, but success message not found. Stdout: ${stdout}`); + return { + content: [{ type: 'text', text: `Add node process completed for ${args.nodeName}. Verify and save the scene.` }], + }; } - - 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' - ] - ); + console.error(`Error adding node: ${error.message}`); + const stderrMatch = error.message.match(/Stderr: (.+)/); + const godotError = stderrMatch ? stderrMatch[1] : 'Check server logs for details.'; + return this.createErrorResponse(`Failed to add node: ${godotError}`); } } @@ -1467,536 +1366,330 @@ class GodotServer { * Handle the load_sprite tool */ private async handleLoadSprite(args: any) { - 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' - ] - ); + this.logDebug(`Handling load_sprite: ${JSON.stringify(args)}`); + args = this.normalizeParameters(args); // Normalize parameters + + if (!args.projectPath || !args.scenePath || !args.nodePath || !args.texturePath) { + return this.createErrorResponse( + 'Project path, scene path, node path, and texture path are required', + ['Provide `projectPath`, `scenePath`, `nodePath`, and `texturePath`.'] + ); + } + if (!this.validatePath(args.projectPath)) { + return this.createErrorResponse('Invalid project path provided.'); } - - // 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' - ] - ); + if (args.scenePath.includes('..') || (!args.scenePath.endsWith('.tscn') && !args.scenePath.endsWith('.scn'))) { + return this.createErrorResponse('Invalid scene path.'); } - - // 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' - ] - ); + if (args.nodePath.includes('..')) { + return this.createErrorResponse('Invalid node path.'); } - - // Prepare parameters for the operation - const params = { - scene_path: args.scenePath, - node_path: args.nodePath, - texture_path: 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' - ] - ); + if (!args.texturePath.startsWith('res://') || args.texturePath.includes('..')) { + return this.createErrorResponse('Invalid texture path. Must start with res:// and not contain "..".'); } - - 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' - ] - ); - } - } + + + try { + const operationArgs = { + scene_path: args.scenePath, + node_path: args.nodePath, + texture_path: args.texturePath, + }; + const { stdout, stderr } = await this.executeOperation('load_sprite', operationArgs, args.projectPath); + + if (stderr && stderr.toLowerCase().includes('error')) { + console.error(`Godot stderr indicates potential error during sprite loading: ${stderr}`); + const errorMatch = stderr.match(/ERROR: (.+)/); + const specificError = errorMatch ? errorMatch[1] : stderr; + return this.createErrorResponse(`Godot reported an error while loading sprite: ${specificError}`); + } + + if (stdout.includes('Sprite loaded successfully')) { + return { content: [{ type: 'text', text: `Sprite texture '${args.texturePath}' loaded onto node '${args.nodePath}' successfully. Remember to save the scene.` }] }; + } else { + console.warn(`Load sprite process completed, but success message not found. Stdout: ${stdout}`); + return { content: [{ type: 'text', text: `Load sprite process completed for ${args.nodePath}. Verify and save the scene.` }] }; + } + } catch (error: any) { + console.error(`Error loading sprite: ${error.message}`); + const stderrMatch = error.message.match(/Stderr: (.+)/); + const godotError = stderrMatch ? stderrMatch[1] : 'Check server logs for details.'; + return this.createErrorResponse(`Failed to load sprite: ${godotError}`); + } + } /** * Handle the export_mesh_library tool */ private async handleExportMeshLibrary(args: any) { - 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' - ] - ); + this.logDebug(`Handling export_mesh_library: ${JSON.stringify(args)}`); + args = this.normalizeParameters(args); // Normalize parameters + + if (!args.projectPath || !args.scenePath || !args.meshItemNames || !Array.isArray(args.meshItemNames) || args.meshItemNames.length === 0 || !args.outputPath) { + return this.createErrorResponse( + 'Project path, scene path, a non-empty array of mesh item names, and output path are required', + ['Provide `projectPath`, `scenePath`, `meshItemNames` (array), and `outputPath` (res:// path ending in .meshlib).'] + ); + } + if (!this.validatePath(args.projectPath)) { + return this.createErrorResponse('Invalid project path provided.'); } - - // 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' - ] - ); + if (args.scenePath.includes('..') || (!args.scenePath.endsWith('.tscn') && !args.scenePath.endsWith('.scn'))) { + return this.createErrorResponse('Invalid scene path.'); } - - // Prepare parameters for the operation - const params: any = { - scene_path: args.scenePath, - output_path: args.outputPath - }; - - // Add optional parameters - if (args.meshItemNames && Array.isArray(args.meshItemNames)) { - params.mesh_item_names = args.meshItemNames; + if (!args.outputPath.startsWith('res://') || !args.outputPath.endsWith('.meshlib') || args.outputPath.includes('..')) { + return this.createErrorResponse('Invalid output path. Must start with res://, end with .meshlib, and not contain "..".'); } - - // 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' - ] - ); + // Validate mesh item names (basic check) + if (args.meshItemNames.some((name: string) => !name || name.includes('/') || name.includes('\\') || name.includes('..'))) { + return this.createErrorResponse('Invalid mesh item name found in the array.'); } - - 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' - ] - ); - } - } + + + try { + const operationArgs = { + scene_path: args.scenePath, + mesh_item_names: args.meshItemNames, + output_path: args.outputPath, + }; + const { stdout, stderr } = await this.executeOperation('export_mesh_library', operationArgs, args.projectPath); + + if (stderr && stderr.toLowerCase().includes('error')) { + console.error(`Godot stderr indicates potential error during mesh library export: ${stderr}`); + const errorMatch = stderr.match(/ERROR: (.+)/); + const specificError = errorMatch ? errorMatch[1] : stderr; + return this.createErrorResponse(`Godot reported an error during mesh library export: ${specificError}`); + } + + if (stdout.includes('MeshLibrary exported successfully')) { + const exportedPath = stdout.split('to: ')[1]?.trim() || args.outputPath; + return { + content: [{ type: 'text', text: `MeshLibrary exported successfully to: ${exportedPath}` }], + outputPath: exportedPath + }; + } else { + console.warn(`Export mesh library process completed, but success message not found. Stdout: ${stdout}`); + return { + content: [{ type: 'text', text: `MeshLibrary export process completed for ${args.outputPath}. Verify the file.` }], + outputPath: args.outputPath + }; + } + } catch (error: any) { + console.error(`Error exporting mesh library: ${error.message}`); + const stderrMatch = error.message.match(/Stderr: (.+)/); + const godotError = stderrMatch ? stderrMatch[1] : 'Check server logs for details.'; + return this.createErrorResponse(`Failed to export mesh library: ${godotError}`); + } + } /** * Handle the save_scene tool */ private async handleSaveScene(args: any) { - 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' - ] - ); + this.logDebug(`Handling save_scene: ${JSON.stringify(args)}`); + args = this.normalizeParameters(args); // Normalize parameters + + if (!args.projectPath || !args.scenePath) { + return this.createErrorResponse( + 'Project path and scene path are required', + ['Provide `projectPath` and `scenePath`.'] + ); + } + if (!this.validatePath(args.projectPath)) { + return this.createErrorResponse('Invalid project path provided.'); } - - // Prepare parameters for the operation - const params: any = { - scene_path: args.scenePath - }; - - // Add optional parameters - if (args.newPath) { - params.new_path = args.newPath; + if (args.scenePath.includes('..') || (!args.scenePath.endsWith('.tscn') && !args.scenePath.endsWith('.scn'))) { + return this.createErrorResponse('Invalid scene path.'); } - - // 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' - ] - ); + if (args.newPath && (args.newPath.includes('..') || (!args.newPath.endsWith('.tscn') && !args.newPath.endsWith('.scn')))) { + return this.createErrorResponse('Invalid new scene path for saving.'); } - - 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' - ] - ); - } - } + + + try { + const operationArgs: OperationParams = { + scene_path: args.scenePath, + }; + if (args.newPath) { + operationArgs.new_path = args.newPath; + } + + const { stdout, stderr } = await this.executeOperation('save_scene', operationArgs, args.projectPath); + + if (stderr && stderr.toLowerCase().includes('error')) { + console.error(`Godot stderr indicates potential error during scene save: ${stderr}`); + const errorMatch = stderr.match(/ERROR: (.+)/); + const specificError = errorMatch ? errorMatch[1] : stderr; + return this.createErrorResponse(`Godot reported an error while saving scene: ${specificError}`); + } + + if (stdout.includes('Scene saved successfully')) { + const savedPath = stdout.split('to: ')[1]?.trim() || args.newPath || args.scenePath; + return { + content: [{ type: 'text', text: `Scene saved successfully to: ${savedPath}` }], + savedPath: savedPath + }; + } else { + console.warn(`Save scene process completed, but success message not found. Stdout: ${stdout}`); + return { + content: [{ type: 'text', text: `Save scene process completed for ${args.newPath || args.scenePath}.` }], + savedPath: args.newPath || args.scenePath + }; + } + } catch (error: any) { + console.error(`Error saving scene: ${error.message}`); + const stderrMatch = error.message.match(/Stderr: (.+)/); + const godotError = stderrMatch ? stderrMatch[1] : 'Check server logs for details.'; + return this.createErrorResponse(`Failed to save scene: ${godotError}`); + } + } /** * Handle the get_uid tool */ private async handleGetUid(args: any) { - 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' - ] - ); - } + this.logDebug(`Handling get_uid: ${JSON.stringify(args)}`); + args = this.normalizeParameters(args); // Normalize parameters + + if (!args.projectPath || !args.filePath) { + return this.createErrorResponse( + 'Project path and file path are required', + ['Provide `projectPath` and `filePath` (res:// path).'] + ); + } + if (!this.validatePath(args.projectPath)) { + return this.createErrorResponse('Invalid project path provided.'); } - - // 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' - ] - ); + if (!args.filePath.startsWith('res://') || args.filePath.includes('..')) { + return this.createErrorResponse('Invalid file path. Must start with res:// and not contain "..".'); } - // 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' - ] - ); - } + try { + const operationArgs = { + file_path: args.filePath, + }; + const { stdout, stderr } = await this.executeOperation('get_uid', operationArgs, args.projectPath); - // Prepare parameters for the operation - const params = { - file_path: 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' - ] - ); - } - } + if (stderr && stderr.toLowerCase().includes('error')) { + // Special case: "UID not found" might be expected, not necessarily a server error + if (stderr.includes('UID not found')) { + this.logDebug(`UID not found for path: ${args.filePath}`); + return { content: [{ type: 'text', text: `UID not found for resource: ${args.filePath}` }], uid: null }; + } else { + console.error(`Godot stderr indicates potential error during UID lookup: ${stderr}`); + const errorMatch = stderr.match(/ERROR: (.+)/); + const specificError = errorMatch ? errorMatch[1] : stderr; + return this.createErrorResponse(`Godot reported an error while getting UID: ${specificError}`); + } + } + + // Expect UID in stdout, e.g., "UID: uid://...." + const uidMatch = stdout.match(/UID: (uid:\/\/[a-zA-Z0-9]+)/); + if (uidMatch && uidMatch[1]) { + const uid = uidMatch[1]; + this.logDebug(`Found UID: ${uid} for path: ${args.filePath}`); + return { + content: [{ type: 'text', text: `UID for ${args.filePath}: ${uid}` }], + uid: uid + }; + } else { + // If no UID found in stdout and no error in stderr, it might mean the resource doesn't have one yet + console.warn(`Get UID process completed, but UID not found in stdout. Stdout: ${stdout}`); + return { content: [{ type: 'text', text: `Could not extract UID for resource: ${args.filePath}. It might not exist or have a UID.` }], uid: null }; + } + } catch (error: any) { + console.error(`Error getting UID: ${error.message}`); + const stderrMatch = error.message.match(/Stderr: (.+)/); + const godotError = stderrMatch ? stderrMatch[1] : 'Check server logs for details.'; + return this.createErrorResponse(`Failed to get UID: ${godotError}`); + } + } /** - * Handle the update_project_uids tool + * Handle the update_project_uids (resave_resources) tool */ private async handleUpdateProjectUids(args: any) { - if (!args.projectPath) { - return this.createErrorResponse( - 'Project path is required', - ['Provide a valid path to a Godot project directory'] - ); - } + this.logDebug(`Handling update_project_uids (resave_resources): ${JSON.stringify(args)}`); + args = this.normalizeParameters(args); // Normalize parameters + + if (!args.projectPath) { + return this.createErrorResponse( + 'Project path is required', + ['Provide `projectPath`.'] + ); + } + if (!this.validatePath(args.projectPath)) { + return this.createErrorResponse('Invalid project path provided.'); + } - if (!this.validatePath(args.projectPath)) { - return this.createErrorResponse( - 'Invalid project path', - ['Provide a valid path without ".." or other potentially unsafe characters'] - ); - } + // Add a confirmation step or strong warning? This is a potentially destructive operation. + // For now, proceed directly based on the tool call. - 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' - ] - ); - } + try { + const operationArgs = {}; // No specific args needed for the script operation itself + const { stdout, stderr } = await this.executeOperation('resave_resources', operationArgs, args.projectPath); - // 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' - ] - ); - } + if (stderr && stderr.toLowerCase().includes('error')) { + console.error(`Godot stderr indicates potential error during resource resave: ${stderr}`); + const errorMatch = stderr.match(/ERROR: (.+)/); + const specificError = errorMatch ? errorMatch[1] : stderr; + // If stderr contains "ERROR:", report it as an error regardless of exit code + return this.createErrorResponse(`Godot reported an error during resource resave: ${specificError}`); + } + + // If we reach here, the process exited with code 0 and stderr did not contain "ERROR:" + // Consider this a success, even if the final stdout message wasn't captured reliably. + this.logDebug('Resource resave process exited cleanly (code 0) with no errors in stderr. Assuming success.'); + return { content: [{ type: 'text', text: 'All project resources resaved successfully.' }] }; + + } catch (error: any) { + // This catch block handles errors from executeOperation (e.g., non-zero exit code, spawn errors) + console.error(`Error resaving resources: ${error.message}`); + const stderrMatch = error.message.match(/Stderr: (.+)/); + const godotError = stderrMatch ? stderrMatch[1] : 'Check server logs for details.'; + return this.createErrorResponse(`Failed to resave resources: ${godotError}`); + } + } - // Prepare parameters for the operation - const params = { - project_path: 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' - ] - ); - } - } /** - * Run the MCP server + * Start the MCP server */ async run() { + this.logDebug('Starting Godot MCP server...'); + // Perform initial Godot path detection on startup 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); + await this.detectGodotPath(); + } catch (error: any) { + // Log the error but allow the server to start if not in strict mode + console.error(`[Startup Error] Failed initial Godot path detection: ${error.message}`); + if (this.strictPathValidation) { + console.error("[Startup Error] Strict path validation enabled. Server cannot start without a valid Godot path."); + process.exit(1); // Exit if strict validation fails + } else { + console.warn("[Startup Warning] Server starting without a confirmed valid Godot path due to non-strict mode."); + } } + + const transport = new StdioServerTransport(); + // Use connect instead of listen + await this.server.connect(transport); + this.logDebug('Godot MCP server connected via stdio'); // Updated log message } } -// 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); +// --- Main Execution --- + +// Create and run the server instance +const server = new GodotServer({ + // Example configuration: + // godotPath: '/path/to/your/godot', // Optional: Override auto-detection + // debugMode: true, // Optional: Force enable/disable server debug logs + // godotDebugMode: true, // Optional: Force enable/disable Godot debug flags + // strictPathValidation: true // Optional: Fail startup if Godot path isn't validated +}); +server.run().catch((error) => { + console.error('Failed to start Godot MCP server:', error); process.exit(1); }); diff --git a/src/scripts/godot_operations.gd b/src/scripts/godot_operations.gd index 65ea869b8..b1fd3396e 100644 --- a/src/scripts/godot_operations.gd +++ b/src/scripts/godot_operations.gd @@ -7,6 +7,10 @@ var debug_mode = false func _init(): var args = OS.get_cmdline_args() + # *** START DEBUG LOGGING *** + print("[GDScript] Raw Command Line Args: ", args) + # *** END DEBUG LOGGING *** + # Check for debug flag debug_mode = "--debug-godot" in args @@ -18,62 +22,141 @@ func _init(): # The operation should be 2 positions after the script path (script_index + 1 is the script path itself) var operation_index = script_index + 2 - # The params should be 3 positions after the script path + # The params should be 3 positions after the script path (optional) var params_index = script_index + 3 - - if args.size() <= params_index: - log_error("Usage: godot --headless --script godot_operations.gd ") - log_error("Not enough command-line arguments provided.") + + # Check if operation argument exists + if args.size() <= operation_index: + log_error("Usage: godot --headless --script godot_operations.gd [json_params]") + log_error("Operation argument missing.") quit(1) - + # Log all arguments for debugging log_debug("All arguments: " + str(args)) log_debug("Script index: " + str(script_index)) log_debug("Operation index: " + str(operation_index)) - log_debug("Params index: " + str(params_index)) - + log_debug("Params index: " + str(params_index)) # Note: params_index might be out of bounds + var operation = args[operation_index] - var params_json = args[params_index] - - log_info("Operation: " + operation) - log_debug("Params JSON: " + params_json) - - # Parse JSON using Godot 4.x API - var json = JSON.new() - var error = json.parse(params_json) - var params = null - - if error == OK: - params = json.get_data() + var params_json = "{}" # Default to empty JSON object + var params = {} # Default to empty Dictionary + + # Check if params argument exists and is not empty + if args.size() > params_index and not args[params_index].is_empty(): + params_json = args[params_index] + log_debug("Params JSON provided: '" + params_json + "'") else: - log_error("Failed to parse JSON parameters: " + params_json) - log_error("JSON Error: " + json.get_error_message() + " at line " + str(json.get_error_line())) - quit(1) - - if not params: - log_error("Failed to parse JSON parameters: " + params_json) - quit(1) - + log_debug("No Params JSON provided or empty, defaulting to {}") + + # *** START DEBUG LOGGING *** + print("[GDScript] Extracted Operation: ", operation) + print("[GDScript] Extracted Params JSON String: '", params_json, "'") + printerr("[GDScript][stderr] Extracted Params JSON String: '", params_json, "'") # Also print to stderr + # *** END DEBUG LOGGING *** + + log_info("Operation: " + operation) + log_debug("Params JSON to be processed: " + params_json) + + # Only parse if params_json is not the default empty object string. + # Parsing "{}" is safe and handles potential whitespace like "{ }". + if params_json != "{}": + var json = JSON.new() + log_debug("Attempting to parse JSON: '" + params_json + "'") + var error = json.parse(params_json) + # params remains {} unless parsing succeeds and returns non-null data + + # *** START DEBUG LOGGING *** + print("[GDScript] JSON Parse Result Code: ", error, " (OK = ", OK, ")") + printerr("[GDScript][stderr] JSON Parse Result Code: ", error, " (OK = ", OK, ")") # Also print to stderr + # *** END DEBUG LOGGING *** + + if error == OK: + # *** START MORE DEBUG LOGGING *** + print("[GDScript] JSON parse reported OK. Getting data...") + printerr("[GDScript][stderr] JSON parse reported OK. Getting data...") + var parsed_data = json.get_data() + print("[GDScript] Value returned by get_data(): ", parsed_data) + print("[GDScript] Type of value returned by get_data(): ", typeof(parsed_data)) + printerr("[GDScript][stderr] Value returned by get_data(): ", parsed_data) + printerr("[GDScript][stderr] Type of value returned by get_data(): ", typeof(parsed_data)) + # *** END MORE DEBUG LOGGING *** + if parsed_data != null: # Check if get_data() returned null despite OK + params = parsed_data + # *** START DEBUG LOGGING *** + print("[GDScript] Parsed Params Dictionary: ", params) + printerr("[GDScript][stderr] Parsed Params Dictionary: ", params) # Also print to stderr + # *** END DEBUG LOGGING *** + else: + # This case should ideally not happen if error is OK, but handle defensively + log_error("JSON parsing returned OK but get_data() returned null. Using empty dictionary.") + printerr("[GDScript][stderr] JSON parsing returned OK but get_data() returned null. Using empty dictionary.") + params = {} # Fallback to empty dictionary + else: + log_error("Failed to parse JSON parameters: " + params_json) + log_error("JSON Error: " + json.get_error_message() + " at line " + str(json.get_error_line())) + quit(1) + else: # This 'else' corresponds to 'if params_json != "{}"' + log_debug("Using default empty dictionary for params.") + # params is already {} + + # The problematic 'if not params:' check is removed. + log_info("Executing operation: " + operation) - + + # Add parameter validation within specific functions that require them match operation: "create_scene": + if not params.has("scene_path"): + log_error("Missing required parameter 'scene_path' for create_scene") + quit(1) + # *** START DEBUG LOGGING *** + print("[GDScript] Entering create_scene function.") + printerr("[GDScript][stderr] Entering create_scene function.") + print("[GDScript] Params type in create_scene: ", typeof(params)) + printerr("[GDScript][stderr] Params type in create_scene: ", typeof(params)) + if params == null: + print("[GDScript] Params is NULL right before accessing scene_path!") + printerr("[GDScript][stderr] Params is NULL right before accessing scene_path!") + else: + print("[GDScript] Params content in create_scene: ", params) + printerr("[GDScript][stderr] Params content in create_scene: ", params) + # *** END DEBUG LOGGING *** create_scene(params) "add_node": - add_node(params) + if not params.has("scene_path") or not params.has("node_type") or not params.has("node_name"): + log_error("Missing required parameters for add_node (scene_path, node_type, node_name)") + quit(1) + add_node(params) "load_sprite": - load_sprite(params) + if not params.has("scene_path") or not params.has("node_path") or not params.has("texture_path"): + log_error("Missing required parameters for load_sprite (scene_path, node_path, texture_path)") + quit(1) + load_sprite(params) "export_mesh_library": - export_mesh_library(params) + if not params.has("scene_path") or not params.has("mesh_item_names") or not params.has("output_path"): + log_error("Missing required parameters for export_mesh_library (scene_path, mesh_item_names, output_path)") + quit(1) + export_mesh_library(params) "save_scene": - save_scene(params) + if not params.has("scene_path"): + log_error("Missing required parameter 'scene_path' for save_scene") + quit(1) + save_scene(params) "get_uid": - get_uid(params) + if not params.has("file_path"): + log_error("Missing required parameter 'file_path' for get_uid") + quit(1) + get_uid(params) "resave_resources": + # No required params, call directly resave_resources(params) _: log_error("Unknown operation: " + operation) quit(1) + + # Add a small delay before quitting to allow stdout/stderr buffers to flush, especially in headless mode + OS.delay_msec(100) # 100ms delay + log_debug("Quitting after delay.") quit() @@ -257,9 +340,6 @@ func create_scene(params): if debug_mode: print("Root node created with name: " + scene_root.name) - # Set the owner of the root node to itself (important for scene saving) - scene_root.owner = scene_root - # Pack the scene var packed_scene = PackedScene.new() var result = packed_scene.pack(scene_root) @@ -462,7 +542,13 @@ func create_scene(params): else: printerr("Failed to pack scene: " + str(result)) printerr("Error code: " + str(result)) - quit(1) + quit(1) # Quit for pack error + + # --- Final Cleanup for create_scene --- + if scene_root and is_instance_valid(scene_root): + if debug_mode: print("Freeing instantiated scene root node at end of create_scene") + scene_root.free() + # --- End Final Cleanup --- # Add a node to an existing scene func add_node(params): @@ -540,26 +626,43 @@ func add_node(params): if debug_mode: print("Pack result: " + str(result) + " (OK=" + str(OK) + ")") + var save_error = ERR_CANT_CREATE # Initialize with an error state + if result == OK: + absolute_scene_path = ProjectSettings.globalize_path(full_scene_path) # Ensure absolute path is defined here (Removed var) if debug_mode: print("Saving scene to: " + absolute_scene_path) - var save_error = ResourceSaver.save(packed_scene, absolute_scene_path) + save_error = ResourceSaver.save(packed_scene, absolute_scene_path) # Use absolute path for saving if debug_mode: print("Save result: " + str(save_error) + " (OK=" + str(OK) + ")") + if save_error == OK: + # Simplified success message for clarity + print("Node '" + params.node_name + "' of type '" + params.node_type + "' added successfully") + # Add a small delay after successful save before cleanup if debug_mode: - var file_check_after = FileAccess.file_exists(absolute_scene_path) - print("File exists check after save: " + str(file_check_after)) - if file_check_after: - print("Node '" + params.node_name + "' of type '" + params.node_type + "' added successfully") - else: - printerr("File reported as saved but does not exist at: " + absolute_scene_path) - else: - print("Node '" + params.node_name + "' of type '" + params.node_type + "' added successfully") + print("Waiting briefly after save...") + OS.delay_msec(100) # 100ms delay else: printerr("Failed to save scene: " + str(save_error)) + # No quit here, proceed to cleanup else: printerr("Failed to pack scene: " + str(result)) + # No quit here, proceed to cleanup + + # --- Start Cleanup --- + # Removed packed_scene.free() as PackedScene is RefCounted + # Reinstate scene_root.free() to prevent RID leak + if scene_root and is_instance_valid(scene_root): + if debug_mode: + print("Freeing instantiated scene root node after delay") + scene_root.free() # Free the instantiated scene root + # --- End Cleanup --- + + # Quit only if there was a critical error during packing or saving + if result != OK or save_error != OK: + quit(1) # Exit with error code if packing or saving failed + # Otherwise, the script will naturally exit after _init finishes # Load a sprite into a Sprite2D node func load_sprite(params): @@ -693,6 +796,12 @@ func load_sprite(params): else: printerr("Failed to pack scene: " + str(result)) + # --- Final Cleanup for load_sprite --- + if scene_root and is_instance_valid(scene_root): + if debug_mode: print("Freeing instantiated scene root node at end of load_sprite") + scene_root.free() + # --- End Final Cleanup --- + # Export a scene as a MeshLibrary resource func export_mesh_library(params): print("Exporting MeshLibrary from scene: " + params.scene_path) @@ -872,6 +981,12 @@ func export_mesh_library(params): else: printerr("No valid meshes found in the scene") + # --- Final Cleanup for export_mesh_library --- + if scene_root and is_instance_valid(scene_root): + if debug_mode: print("Freeing instantiated scene root node at end of export_mesh_library") + scene_root.free() + # --- End Final Cleanup --- + # Find files with a specific extension recursively func find_files(path, extension): var files = [] @@ -1190,3 +1305,9 @@ func save_scene(params): printerr("Failed to save scene: " + str(error)) else: printerr("Failed to pack scene: " + str(result)) + + # --- Final Cleanup for save_scene --- + if scene_root and is_instance_valid(scene_root): + if debug_mode: print("Freeing instantiated scene root node at end of save_scene") + scene_root.free() + # --- End Final Cleanup ---