# Directory Structure
```
├── .github
│ └── FUNDING.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│ └── build.js
├── src
│ ├── index.ts
│ └── scripts
│ └── godot_operations.gd
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Dependencies
2 | node_modules/
3 |
4 | # Build output
5 | build/
6 | dist/
7 |
8 | # Logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Environment variables
15 | .env*
16 |
17 | # OS files
18 | .DS_Store
19 | Thumbs.db
20 |
21 | # Editor directories and files
22 | .vscode/
23 | .idea/
24 | *.swp
25 | *.swo
26 |
27 | # Coverage directory
28 | coverage/
29 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 |
2 | # Godot MCP
3 |
4 | [](https://github.com/sponsors/Coding-Solo)
5 |
6 | [](https://modelcontextprotocol.io/introduction)
7 | [](https://godotengine.org)
8 | [](https://nodejs.org/en/download/)
9 | [](https://www.typescriptlang.org/)
10 |
11 | [](https://github.com/Coding-Solo/godot-mcp/commits/main)
12 | [](https://github.com/Coding-Solo/godot-mcp/stargazers)
13 | [](https://github.com/Coding-Solo/godot-mcp/network/members)
14 | [](https://opensource.org/licenses/MIT)
15 |
16 | ```text
17 | ((((((( (((((((
18 | ((((((((((( (((((((((((
19 | ((((((((((((( (((((((((((((
20 | (((((((((((((((((((((((((((((((((
21 | (((((((((((((((((((((((((((((((((
22 | ((((( ((((((((((((((((((((((((((((((((((((((((( (((((
23 | (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
24 | ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
25 | ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
26 | (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
27 | (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
28 | (((((((((((@@@@@@@(((((((((((((((((((((((((((@@@@@@@(((((((((((
29 | (((((((((@@@@,,,,,@@@(((((((((((((((((((((@@@,,,,,@@@@(((((((((
30 | ((((((((@@@,,,,,,,,,@@(((((((@@@@@(((((((@@,,,,,,,,,@@@((((((((
31 | ((((((((@@@,,,,,,,,,@@(((((((@@@@@(((((((@@,,,,,,,,,@@@((((((((
32 | (((((((((@@@,,,,,,,@@((((((((@@@@@((((((((@@,,,,,,,@@@(((((((((
33 | ((((((((((((@@@@@@(((((((((((@@@@@(((((((((((@@@@@@((((((((((((
34 | (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
35 | (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
36 | @@@@@@@@@@@@@((((((((((((@@@@@@@@@@@@@((((((((((((@@@@@@@@@@@@@
37 | ((((((((( @@@(((((((((((@@(((((((((((@@(((((((((((@@@ (((((((((
38 | (((((((((( @@((((((((((@@@(((((((((((@@@((((((((((@@ ((((((((((
39 | (((((((((((@@@@@@@@@@@@@@(((((((((((@@@@@@@@@@@@@@(((((((((((
40 | (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
41 | (((((((((((((((((((((((((((((((((((((((((((((((((((((
42 | (((((((((((((((((((((((((((((((((((((((((((((((
43 | (((((((((((((((((((((((((((((((((
44 |
45 |
46 | /$$ /$$ /$$$$$$ /$$$$$$$
47 | | $$$ /$$$ /$$__ $$| $$__ $$
48 | | $$$$ /$$$$| $$ \__/| $$ \ $$
49 | | $$ $$/$$ $$| $$ | $$$$$$$/
50 | | $$ $$$| $$| $$ | $$____/
51 | | $$\ $ | $$| $$ $$| $$
52 | | $$ \/ | $$| $$$$$$/| $$
53 | |__/ |__/ \______/ |__/
54 | ```
55 |
56 | A Model Context Protocol (MCP) server for interacting with the Godot game engine.
57 |
58 | ## Introduction
59 |
60 | Godot MCP enables AI assistants to launch the Godot editor, run projects, capture debug output, and control project execution - all through a standardized interface.
61 |
62 | This direct feedback loop helps AI assistants like Claude understand what works and what doesn't in real Godot projects, leading to better code generation and debugging assistance.
63 |
64 | ## Features
65 |
66 | - **Launch Godot Editor**: Open the Godot editor for a specific project
67 | - **Run Godot Projects**: Execute Godot projects in debug mode
68 | - **Capture Debug Output**: Retrieve console output and error messages
69 | - **Control Execution**: Start and stop Godot projects programmatically
70 | - **Get Godot Version**: Retrieve the installed Godot version
71 | - **List Godot Projects**: Find Godot projects in a specified directory
72 | - **Project Analysis**: Get detailed information about project structure
73 | - **Scene Management**:
74 | - Create new scenes with specified root node types
75 | - Add nodes to existing scenes with customizable properties
76 | - Load sprites and textures into Sprite2D nodes
77 | - Export 3D scenes as MeshLibrary resources for GridMap
78 | - Save scenes with options for creating variants
79 | - **UID Management** (for Godot 4.4+):
80 | - Get UID for specific files
81 | - Update UID references by resaving resources
82 |
83 | ## Requirements
84 |
85 | - [Godot Engine](https://godotengine.org/download) installed on your system
86 | - Node.js and npm
87 | - An AI assistant that supports MCP (Cline, Cursor, etc.)
88 |
89 | ## Installation and Configuration
90 |
91 | ### Step 1: Install and Build
92 |
93 | First, clone the repository and build the MCP server:
94 |
95 | ```bash
96 | git clone https://github.com/Coding-Solo/godot-mcp.git
97 | cd godot-mcp
98 | npm install
99 | npm run build
100 | ```
101 |
102 | ### Step 2: Configure with Your AI Assistant
103 |
104 | #### Option A: Configure with Cline
105 |
106 | Add to your Cline MCP settings file (`~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`):
107 |
108 | ```json
109 | {
110 | "mcpServers": {
111 | "godot": {
112 | "command": "node",
113 | "args": ["/absolute/path/to/godot-mcp/build/index.js"],
114 | "env": {
115 | "DEBUG": "true" // Optional: Enable detailed logging
116 | },
117 | "disabled": false,
118 | "autoApprove": [
119 | "launch_editor",
120 | "run_project",
121 | "get_debug_output",
122 | "stop_project",
123 | "get_godot_version",
124 | "list_projects",
125 | "get_project_info",
126 | "create_scene",
127 | "add_node",
128 | "load_sprite",
129 | "export_mesh_library",
130 | "save_scene",
131 | "get_uid",
132 | "update_project_uids"
133 | ]
134 | }
135 | }
136 | }
137 | ```
138 |
139 | #### Option B: Configure with Cursor
140 |
141 | **Using the Cursor UI:**
142 |
143 | 1. Go to **Cursor Settings** > **Features** > **MCP**
144 | 2. Click on the **+ Add New MCP Server** button
145 | 3. Fill out the form:
146 | - Name: `godot` (or any name you prefer)
147 | - Type: `command`
148 | - Command: `node /absolute/path/to/godot-mcp/build/index.js`
149 | 4. Click "Add"
150 | 5. You may need to press the refresh button in the top right corner of the MCP server card to populate the tool list
151 |
152 | **Using Project-Specific Configuration:**
153 |
154 | Create a file at `.cursor/mcp.json` in your project directory with the following content:
155 |
156 | ```json
157 | {
158 | "mcpServers": {
159 | "godot": {
160 | "command": "node",
161 | "args": ["/absolute/path/to/godot-mcp/build/index.js"],
162 | "env": {
163 | "DEBUG": "true" // Enable detailed logging
164 | }
165 | }
166 | }
167 | }
168 | ```
169 |
170 | ### Step 3: Optional Environment Variables
171 |
172 | You can customize the server behavior with these environment variables:
173 |
174 | - `GODOT_PATH`: Path to the Godot executable (overrides automatic detection)
175 | - `DEBUG`: Set to "true" to enable detailed server-side debug logging
176 |
177 | ## Example Prompts
178 |
179 | Once configured, your AI assistant will automatically run the MCP server when needed. You can use prompts like:
180 |
181 | ```text
182 | "Launch the Godot editor for my project at /path/to/project"
183 |
184 | "Run my Godot project and show me any errors"
185 |
186 | "Get information about my Godot project structure"
187 |
188 | "Analyze my Godot project structure and suggest improvements"
189 |
190 | "Help me debug this error in my Godot project: [paste error]"
191 |
192 | "Write a GDScript for a character controller with double jump and wall sliding"
193 |
194 | "Create a new scene with a Player node in my Godot project"
195 |
196 | "Add a Sprite2D node to my player scene and load the character texture"
197 |
198 | "Export my 3D models as a MeshLibrary for use with GridMap"
199 |
200 | "Create a UI scene with buttons and labels for my game's main menu"
201 |
202 | "Get the UID for a specific script file in my Godot 4.4 project"
203 |
204 | "Update UID references in my Godot project after upgrading to 4.4"
205 | ```
206 |
207 | ## Implementation Details
208 |
209 | ### Architecture
210 |
211 | The Godot MCP server uses a bundled GDScript approach for complex operations:
212 |
213 | 1. **Direct Commands**: Simple operations like launching the editor or getting project info use Godot's built-in CLI commands directly.
214 | 2. **Bundled Operations Script**: Complex operations like creating scenes or adding nodes use a single, comprehensive GDScript file (`godot_operations.gd`) that handles all operations.
215 |
216 | This architecture provides several benefits:
217 |
218 | - **No Temporary Files**: Eliminates the need for temporary script files, keeping your system clean
219 | - **Simplified Codebase**: Centralizes all Godot operations in one (somewhat) organized file
220 | - **Better Maintainability**: Makes it easier to add new operations or modify existing ones
221 | - **Improved Error Handling**: Provides consistent error reporting across all operations
222 | - **Reduced Overhead**: Minimizes file I/O operations for better performance
223 |
224 | The bundled script accepts operation type and parameters as JSON, allowing for flexible and dynamic operation execution without generating temporary files for each operation.
225 |
226 | ## Troubleshooting
227 |
228 | - **Godot Not Found**: Set the GODOT_PATH environment variable to your Godot executable
229 | - **Connection Issues**: Ensure the server is running and restart your AI assistant
230 | - **Invalid Project Path**: Ensure the path points to a directory containing a project.godot file
231 | - **Build Issues**: Make sure all dependencies are installed by running `npm install`
232 | - **For Cursor Specifically**:
233 | - Ensure the MCP server shows up and is enabled in Cursor settings (Settings > MCP)
234 | - MCP tools can only be run using the Agent chat profile (Cursor Pro or Business subscription)
235 | - Use "Yolo Mode" to automatically run MCP tool requests
236 |
237 | ## License
238 |
239 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
240 |
241 | [](https://mseep.ai/app/coding-solo-godot-mcp)
242 |
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Contributing to Godot MCP
2 |
3 | Thank you for considering contributing to Godot MCP! This document outlines the process for contributing to the project.
4 |
5 | ## Code of Conduct
6 |
7 | By participating in this project, you agree to maintain a respectful and inclusive environment for everyone.
8 |
9 | ## How Can I Contribute?
10 |
11 | ### Reporting Bugs
12 |
13 | - Check if the bug has already been reported in the Issues section
14 | - Use the bug report template if available
15 | - Include detailed steps to reproduce the bug
16 | - Include any relevant logs or screenshots
17 | - Specify your environment (OS, Godot version, etc.)
18 |
19 | ### Suggesting Enhancements
20 |
21 | - Check if the enhancement has already been suggested in the Issues section
22 | - Use the feature request template if available
23 | - Clearly describe the enhancement and its benefits
24 | - Consider how the enhancement fits into the project's scope
25 |
26 | ### Pull Requests
27 |
28 | 1. Fork the repository
29 | 2. Create a new branch for your feature or bugfix (`git checkout -b feature/amazing-feature`)
30 | 3. Make your changes
31 | 4. Run tests if available
32 | 5. Commit your changes with clear commit messages
33 | 6. Push to your branch (`git push origin feature/amazing-feature`)
34 | 7. Open a Pull Request
35 |
36 | ## Development Process
37 |
38 | ### Setting Up the Development Environment
39 |
40 | 1. Clone the repository
41 | 2. Install dependencies with `npm install`
42 | 3. Build the project with `npm run build`
43 | 4. For development with auto-rebuild, use `npm run watch`
44 |
45 | ### Project Structure
46 |
47 | ```
48 | godot-mcp/
49 | ├── src/ # Source code
50 | │ └── index.ts # Main server implementation
51 | ├── build/ # Compiled JavaScript (generated)
52 | ├── tests/ # Test files (future)
53 | ├── examples/ # Example Godot projects (future)
54 | ├── LICENSE # MIT License
55 | ├── README.md # Documentation
56 | ├── CONTRIBUTING.md # Contribution guidelines
57 | ├── package.json # Project configuration
58 | └── tsconfig.json # TypeScript configuration
59 | ```
60 |
61 | ### Code Style
62 |
63 | - Follow the existing code style in the project
64 | - Use TypeScript for type safety
65 | - Include JSDoc comments for all functions and classes
66 | - Write clear and descriptive variable and function names
67 | - Use meaningful interfaces for complex objects
68 | - Handle errors gracefully with detailed error messages
69 |
70 | ### Debugging
71 |
72 | For debugging the MCP server:
73 |
74 | 1. Set the `DEBUG` environment variable to `true`
75 | 2. Use the MCP Inspector for interactive debugging:
76 | ```bash
77 | npm run inspector
78 | ```
79 | 3. Check the logs for detailed information about what's happening
80 |
81 | ### Adding New Tools
82 |
83 | When adding new tools to the MCP server:
84 |
85 | 1. Define the tool in the `setupToolHandlers` method
86 | 2. Create a handler method for the tool
87 | 3. Add proper input validation and error handling
88 | 4. Update the README.md with documentation for the new tool
89 | 5. Update the Features section in the README.md
90 | 6. Update the autoApprove section in the configuration examples
91 | 7. Add tests for the new functionality
92 |
93 | #### Recently Added Tools
94 |
95 | The following tools have been recently added:
96 |
97 | - **get_project_info**: Retrieves metadata about a Godot project
98 | - Analyzes project structure
99 | - Returns information about scenes, scripts, and assets
100 | - Helps LLMs understand the organization of Godot projects
101 |
102 | - **capture_screenshot**: Takes a screenshot of a running Godot project
103 | - Requires an active Godot process
104 | - Saves the screenshot to the specified path
105 | - Useful for visual debugging and feedback
106 |
107 | Example:
108 |
109 | ```typescript
110 | // In setupToolHandlers
111 | {
112 | name: 'your_new_tool',
113 | description: 'Description of what your tool does',
114 | inputSchema: {
115 | type: 'object',
116 | properties: {
117 | param1: {
118 | type: 'string',
119 | description: 'Description of parameter 1',
120 | },
121 | },
122 | required: ['param1'],
123 | },
124 | }
125 |
126 | // Add handler method
127 | private async handleYourNewTool(args: any) {
128 | // Validate input
129 | if (!args.param1) {
130 | return this.createErrorResponse(
131 | 'Parameter 1 is required',
132 | ['Provide a valid value for parameter 1']
133 | );
134 | }
135 |
136 | try {
137 | // Implement tool functionality
138 | // ...
139 |
140 | return {
141 | content: [
142 | {
143 | type: 'text',
144 | text: 'Result of your tool',
145 | },
146 | ],
147 | };
148 | } catch (error: any) {
149 | return this.createErrorResponse(
150 | `Failed to execute tool: ${error?.message || 'Unknown error'}`,
151 | [
152 | 'Possible solution 1',
153 | 'Possible solution 2'
154 | ]
155 | );
156 | }
157 | }
158 | ```
159 |
160 | ### Cross-Platform Compatibility
161 |
162 | When making changes, ensure they work across different platforms:
163 |
164 | - Use path utilities from Node.js (`path.join`, etc.) instead of hardcoded path separators
165 | - Test on different operating systems if possible
166 | - Consider different Godot installation locations
167 | - Use environment variables for configuration
168 |
169 | ## Testing
170 |
171 | - Add tests for new features when possible
172 | - Ensure all tests pass before submitting a Pull Request
173 | - Test on different platforms if possible
174 | - Test with different Godot versions
175 |
176 | ## Documentation
177 |
178 | - Keep README.md up to date with new features
179 | - Document all tools and their parameters
180 | - Include examples for new functionality
181 | - Update the troubleshooting section with common issues
182 |
183 | ## Questions?
184 |
185 | If you have any questions about contributing, feel free to open an issue for discussion.
186 |
187 | Thank you for your contributions!
188 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "allowJs": true,
13 | "resolveJsonModule": true
14 | },
15 | "include": ["src/**/*"],
16 | "exclude": ["node_modules"]
17 | }
18 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "godot-mcp",
3 | "version": "0.1.0",
4 | "description": "MCP server for interfacing with Godot game engine. Provides tools for launching the editor, running projects, and capturing debug output.",
5 | "type": "module",
6 | "bin": {
7 | "godot-mcp": "./build/index.js"
8 | },
9 | "files": [
10 | "build"
11 | ],
12 | "scripts": {
13 | "build": "tsc && node scripts/build.js",
14 | "prepare": "npm run build",
15 | "watch": "tsc --watch",
16 | "inspector": "npx @modelcontextprotocol/inspector build/index.js"
17 | },
18 | "dependencies": {
19 | "@modelcontextprotocol/sdk": "0.6.0",
20 | "axios": "^1.7.9",
21 | "fs-extra": "^11.2.0"
22 | },
23 | "devDependencies": {
24 | "@types/node": "^20.11.24",
25 | "typescript": "^5.3.3"
26 | },
27 | "license": "MIT",
28 | "repository": {
29 | "type": "git",
30 | "url": "https://github.com/Coding-Solo/godot-mcp.git"
31 | },
32 | "keywords": [
33 | "godot",
34 | "mcp",
35 | "ai",
36 | "claude",
37 | "cline"
38 | ]
39 | }
40 |
```
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
```yaml
1 | # These are supported funding model platforms
2 |
3 | github: [Coding-Solo] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
```
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
```javascript
1 | import fs from 'fs-extra';
2 | import path from 'path';
3 | import { fileURLToPath } from 'url';
4 |
5 | // Get the directory name
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = path.dirname(__filename);
8 |
9 | // Make the build/index.js file executable
10 | fs.chmodSync(path.join(__dirname, '..', 'build', 'index.js'), '755');
11 |
12 | // Copy the scripts directory to the build directory
13 | try {
14 | // Ensure the build/scripts directory exists
15 | fs.ensureDirSync(path.join(__dirname, '..', 'build', 'scripts'));
16 |
17 | // Copy the godot_operations.gd file
18 | fs.copyFileSync(
19 | path.join(__dirname, '..', 'src', 'scripts', 'godot_operations.gd'),
20 | path.join(__dirname, '..', 'build', 'scripts', 'godot_operations.gd')
21 | );
22 |
23 | console.log('Successfully copied godot_operations.gd to build/scripts');
24 | } catch (error) {
25 | console.error('Error copying scripts:', error);
26 | process.exit(1);
27 | }
28 |
29 | console.log('Build scripts completed successfully!');
30 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | /**
3 | * Godot MCP Server
4 | *
5 | * This MCP server provides tools for interacting with the Godot game engine.
6 | * It enables AI assistants to launch the Godot editor, run Godot projects,
7 | * capture debug output, and control project execution.
8 | */
9 |
10 | import { fileURLToPath } from 'url';
11 | import { join, dirname, basename, normalize } from 'path';
12 | import { existsSync, readdirSync, mkdirSync } from 'fs';
13 | import { spawn } from 'child_process';
14 | import { promisify } from 'util';
15 | import { exec } from 'child_process';
16 |
17 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
18 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
19 | import {
20 | CallToolRequestSchema,
21 | ErrorCode,
22 | ListToolsRequestSchema,
23 | McpError,
24 | } from '@modelcontextprotocol/sdk/types.js';
25 |
26 | // Check if debug mode is enabled
27 | const DEBUG_MODE: boolean = process.env.DEBUG === 'true';
28 | const GODOT_DEBUG_MODE: boolean = true; // Always use GODOT DEBUG MODE
29 |
30 | const execAsync = promisify(exec);
31 |
32 | // Derive __filename and __dirname in ESM
33 | const __filename = fileURLToPath(import.meta.url);
34 | const __dirname = dirname(__filename);
35 |
36 | /**
37 | * Interface representing a running Godot process
38 | */
39 | interface GodotProcess {
40 | process: any;
41 | output: string[];
42 | errors: string[];
43 | }
44 |
45 | /**
46 | * Interface for server configuration
47 | */
48 | interface GodotServerConfig {
49 | godotPath?: string;
50 | debugMode?: boolean;
51 | godotDebugMode?: boolean;
52 | strictPathValidation?: boolean; // New option to control path validation behavior
53 | }
54 |
55 | /**
56 | * Interface for operation parameters
57 | */
58 | interface OperationParams {
59 | [key: string]: any;
60 | }
61 |
62 | /**
63 | * Main server class for the Godot MCP server
64 | */
65 | class GodotServer {
66 | private server: Server;
67 | private activeProcess: GodotProcess | null = null;
68 | private godotPath: string | null = null;
69 | private operationsScriptPath: string;
70 | private validatedPaths: Map<string, boolean> = new Map();
71 | private strictPathValidation: boolean = false;
72 |
73 | /**
74 | * Parameter name mappings between snake_case and camelCase
75 | * This allows the server to accept both formats
76 | */
77 | private parameterMappings: Record<string, string> = {
78 | 'project_path': 'projectPath',
79 | 'scene_path': 'scenePath',
80 | 'root_node_type': 'rootNodeType',
81 | 'parent_node_path': 'parentNodePath',
82 | 'node_type': 'nodeType',
83 | 'node_name': 'nodeName',
84 | 'texture_path': 'texturePath',
85 | 'node_path': 'nodePath',
86 | 'output_path': 'outputPath',
87 | 'mesh_item_names': 'meshItemNames',
88 | 'new_path': 'newPath',
89 | 'file_path': 'filePath',
90 | 'directory': 'directory',
91 | 'recursive': 'recursive',
92 | 'scene': 'scene',
93 | };
94 |
95 | /**
96 | * Reverse mapping from camelCase to snake_case
97 | * Generated from parameterMappings for quick lookups
98 | */
99 | private reverseParameterMappings: Record<string, string> = {};
100 |
101 | constructor(config?: GodotServerConfig) {
102 | // Initialize reverse parameter mappings
103 | for (const [snakeCase, camelCase] of Object.entries(this.parameterMappings)) {
104 | this.reverseParameterMappings[camelCase] = snakeCase;
105 | }
106 | // Apply configuration if provided
107 | let debugMode = DEBUG_MODE;
108 | let godotDebugMode = GODOT_DEBUG_MODE;
109 |
110 | if (config) {
111 | if (config.debugMode !== undefined) {
112 | debugMode = config.debugMode;
113 | }
114 | if (config.godotDebugMode !== undefined) {
115 | godotDebugMode = config.godotDebugMode;
116 | }
117 | if (config.strictPathValidation !== undefined) {
118 | this.strictPathValidation = config.strictPathValidation;
119 | }
120 |
121 | // Store and validate custom Godot path if provided
122 | if (config.godotPath) {
123 | const normalizedPath = normalize(config.godotPath);
124 | this.godotPath = normalizedPath;
125 | this.logDebug(`Custom Godot path provided: ${this.godotPath}`);
126 |
127 | // Validate immediately with sync check
128 | if (!this.isValidGodotPathSync(this.godotPath)) {
129 | console.warn(`[SERVER] Invalid custom Godot path provided: ${this.godotPath}`);
130 | this.godotPath = null; // Reset to trigger auto-detection later
131 | }
132 | }
133 | }
134 |
135 | // Set the path to the operations script
136 | this.operationsScriptPath = join(__dirname, 'scripts', 'godot_operations.gd');
137 | if (debugMode) console.debug(`[DEBUG] Operations script path: ${this.operationsScriptPath}`);
138 |
139 | // Initialize the MCP server
140 | this.server = new Server(
141 | {
142 | name: 'godot-mcp',
143 | version: '0.1.0',
144 | },
145 | {
146 | capabilities: {
147 | tools: {},
148 | },
149 | }
150 | );
151 |
152 | // Set up tool handlers
153 | this.setupToolHandlers();
154 |
155 | // Error handling
156 | this.server.onerror = (error) => console.error('[MCP Error]', error);
157 |
158 | // Cleanup on exit
159 | process.on('SIGINT', async () => {
160 | await this.cleanup();
161 | process.exit(0);
162 | });
163 | }
164 |
165 | /**
166 | * Log debug messages if debug mode is enabled
167 | */
168 | private logDebug(message: string): void {
169 | if (DEBUG_MODE) {
170 | console.debug(`[DEBUG] ${message}`);
171 | }
172 | }
173 |
174 | /**
175 | * Create a standardized error response with possible solutions
176 | */
177 | private createErrorResponse(message: string, possibleSolutions: string[] = []): any {
178 | // Log the error
179 | console.error(`[SERVER] Error response: ${message}`);
180 | if (possibleSolutions.length > 0) {
181 | console.error(`[SERVER] Possible solutions: ${possibleSolutions.join(', ')}`);
182 | }
183 |
184 | const response: any = {
185 | content: [
186 | {
187 | type: 'text',
188 | text: message,
189 | },
190 | ],
191 | isError: true,
192 | };
193 |
194 | if (possibleSolutions.length > 0) {
195 | response.content.push({
196 | type: 'text',
197 | text: 'Possible solutions:\n- ' + possibleSolutions.join('\n- '),
198 | });
199 | }
200 |
201 | return response;
202 | }
203 |
204 | /**
205 | * Validate a path to prevent path traversal attacks
206 | */
207 | private validatePath(path: string): boolean {
208 | // Basic validation to prevent path traversal
209 | if (!path || path.includes('..')) {
210 | return false;
211 | }
212 |
213 | // Add more validation as needed
214 | return true;
215 | }
216 |
217 | /**
218 | * Synchronous validation for constructor use
219 | * This is a quick check that only verifies file existence, not executable validity
220 | * Full validation will be performed later in detectGodotPath
221 | * @param path Path to check
222 | * @returns True if the path exists or is 'godot' (which might be in PATH)
223 | */
224 | private isValidGodotPathSync(path: string): boolean {
225 | try {
226 | this.logDebug(`Quick-validating Godot path: ${path}`);
227 | return path === 'godot' || existsSync(path);
228 | } catch (error) {
229 | this.logDebug(`Invalid Godot path: ${path}, error: ${error}`);
230 | return false;
231 | }
232 | }
233 |
234 | /**
235 | * Validate if a Godot path is valid and executable
236 | */
237 | private async isValidGodotPath(path: string): Promise<boolean> {
238 | // Check cache first
239 | if (this.validatedPaths.has(path)) {
240 | return this.validatedPaths.get(path)!;
241 | }
242 |
243 | try {
244 | this.logDebug(`Validating Godot path: ${path}`);
245 |
246 | // Check if the file exists (skip for 'godot' which might be in PATH)
247 | if (path !== 'godot' && !existsSync(path)) {
248 | this.logDebug(`Path does not exist: ${path}`);
249 | this.validatedPaths.set(path, false);
250 | return false;
251 | }
252 |
253 | // Try to execute Godot with --version flag
254 | const command = path === 'godot' ? 'godot --version' : `"${path}" --version`;
255 | await execAsync(command);
256 |
257 | this.logDebug(`Valid Godot path: ${path}`);
258 | this.validatedPaths.set(path, true);
259 | return true;
260 | } catch (error) {
261 | this.logDebug(`Invalid Godot path: ${path}, error: ${error}`);
262 | this.validatedPaths.set(path, false);
263 | return false;
264 | }
265 | }
266 |
267 | /**
268 | * Detect the Godot executable path based on the operating system
269 | */
270 | private async detectGodotPath() {
271 | // If godotPath is already set and valid, use it
272 | if (this.godotPath && await this.isValidGodotPath(this.godotPath)) {
273 | this.logDebug(`Using existing Godot path: ${this.godotPath}`);
274 | return;
275 | }
276 |
277 | // Check environment variable next
278 | if (process.env.GODOT_PATH) {
279 | const normalizedPath = normalize(process.env.GODOT_PATH);
280 | this.logDebug(`Checking GODOT_PATH environment variable: ${normalizedPath}`);
281 | if (await this.isValidGodotPath(normalizedPath)) {
282 | this.godotPath = normalizedPath;
283 | this.logDebug(`Using Godot path from environment: ${this.godotPath}`);
284 | return;
285 | } else {
286 | this.logDebug(`GODOT_PATH environment variable is invalid`);
287 | }
288 | }
289 |
290 | // Auto-detect based on platform
291 | const osPlatform = process.platform;
292 | this.logDebug(`Auto-detecting Godot path for platform: ${osPlatform}`);
293 |
294 | const possiblePaths: string[] = [
295 | 'godot', // Check if 'godot' is in PATH first
296 | ];
297 |
298 | // Add platform-specific paths
299 | if (osPlatform === 'darwin') {
300 | possiblePaths.push(
301 | '/Applications/Godot.app/Contents/MacOS/Godot',
302 | '/Applications/Godot_4.app/Contents/MacOS/Godot',
303 | `${process.env.HOME}/Applications/Godot.app/Contents/MacOS/Godot`,
304 | `${process.env.HOME}/Applications/Godot_4.app/Contents/MacOS/Godot`,
305 | `${process.env.HOME}/Library/Application Support/Steam/steamapps/common/Godot Engine/Godot.app/Contents/MacOS/Godot`
306 | );
307 | } else if (osPlatform === 'win32') {
308 | possiblePaths.push(
309 | 'C:\\Program Files\\Godot\\Godot.exe',
310 | 'C:\\Program Files (x86)\\Godot\\Godot.exe',
311 | 'C:\\Program Files\\Godot_4\\Godot.exe',
312 | 'C:\\Program Files (x86)\\Godot_4\\Godot.exe',
313 | `${process.env.USERPROFILE}\\Godot\\Godot.exe`
314 | );
315 | } else if (osPlatform === 'linux') {
316 | possiblePaths.push(
317 | '/usr/bin/godot',
318 | '/usr/local/bin/godot',
319 | '/snap/bin/godot',
320 | `${process.env.HOME}/.local/bin/godot`
321 | );
322 | }
323 |
324 | // Try each possible path
325 | for (const path of possiblePaths) {
326 | const normalizedPath = normalize(path);
327 | if (await this.isValidGodotPath(normalizedPath)) {
328 | this.godotPath = normalizedPath;
329 | this.logDebug(`Found Godot at: ${normalizedPath}`);
330 | return;
331 | }
332 | }
333 |
334 | // If we get here, we couldn't find Godot
335 | this.logDebug(`Warning: Could not find Godot in common locations for ${osPlatform}`);
336 | console.warn(`[SERVER] Could not find Godot in common locations for ${osPlatform}`);
337 | 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.`);
338 |
339 | if (this.strictPathValidation) {
340 | // In strict mode, throw an error
341 | throw new Error(`Could not find a valid Godot executable. Set GODOT_PATH or provide a valid path in config.`);
342 | } else {
343 | // Fallback to a default path in non-strict mode; this may not be valid and requires user configuration for reliability
344 | if (osPlatform === 'win32') {
345 | this.godotPath = normalize('C:\\Program Files\\Godot\\Godot.exe');
346 | } else if (osPlatform === 'darwin') {
347 | this.godotPath = normalize('/Applications/Godot.app/Contents/MacOS/Godot');
348 | } else {
349 | this.godotPath = normalize('/usr/bin/godot');
350 | }
351 |
352 | this.logDebug(`Using default path: ${this.godotPath}, but this may not work.`);
353 | console.warn(`[SERVER] Using default path: ${this.godotPath}, but this may not work.`);
354 | console.warn(`[SERVER] This fallback behavior will be removed in a future version. Set strictPathValidation: true to opt-in to the new behavior.`);
355 | }
356 | }
357 |
358 | /**
359 | * Set a custom Godot path
360 | * @param customPath Path to the Godot executable
361 | * @returns True if the path is valid and was set, false otherwise
362 | */
363 | public async setGodotPath(customPath: string): Promise<boolean> {
364 | if (!customPath) {
365 | return false;
366 | }
367 |
368 | // Normalize the path to ensure consistent format across platforms
369 | // (e.g., backslashes to forward slashes on Windows, resolving relative paths)
370 | const normalizedPath = normalize(customPath);
371 | if (await this.isValidGodotPath(normalizedPath)) {
372 | this.godotPath = normalizedPath;
373 | this.logDebug(`Godot path set to: ${normalizedPath}`);
374 | return true;
375 | }
376 |
377 | this.logDebug(`Failed to set invalid Godot path: ${normalizedPath}`);
378 | return false;
379 | }
380 |
381 | /**
382 | * Clean up resources when shutting down
383 | */
384 | private async cleanup() {
385 | this.logDebug('Cleaning up resources');
386 | if (this.activeProcess) {
387 | this.logDebug('Killing active Godot process');
388 | this.activeProcess.process.kill();
389 | this.activeProcess = null;
390 | }
391 | await this.server.close();
392 | }
393 |
394 | /**
395 | * Check if the Godot version is 4.4 or later
396 | * @param version The Godot version string
397 | * @returns True if the version is 4.4 or later
398 | */
399 | private isGodot44OrLater(version: string): boolean {
400 | const match = version.match(/^(\d+)\.(\d+)/);
401 | if (match) {
402 | const major = parseInt(match[1], 10);
403 | const minor = parseInt(match[2], 10);
404 | return major > 4 || (major === 4 && minor >= 4);
405 | }
406 | return false;
407 | }
408 |
409 | /**
410 | * Normalize parameters to camelCase format
411 | * @param params Object with either snake_case or camelCase keys
412 | * @returns Object with all keys in camelCase format
413 | */
414 | private normalizeParameters(params: OperationParams): OperationParams {
415 | if (!params || typeof params !== 'object') {
416 | return params;
417 | }
418 |
419 | const result: OperationParams = {};
420 |
421 | for (const key in params) {
422 | if (Object.prototype.hasOwnProperty.call(params, key)) {
423 | let normalizedKey = key;
424 |
425 | // If the key is in snake_case, convert it to camelCase using our mapping
426 | if (key.includes('_') && this.parameterMappings[key]) {
427 | normalizedKey = this.parameterMappings[key];
428 | }
429 |
430 | // Handle nested objects recursively
431 | if (typeof params[key] === 'object' && params[key] !== null && !Array.isArray(params[key])) {
432 | result[normalizedKey] = this.normalizeParameters(params[key] as OperationParams);
433 | } else {
434 | result[normalizedKey] = params[key];
435 | }
436 | }
437 | }
438 |
439 | return result;
440 | }
441 |
442 | /**
443 | * Convert camelCase keys to snake_case
444 | * @param params Object with camelCase keys
445 | * @returns Object with snake_case keys
446 | */
447 | private convertCamelToSnakeCase(params: OperationParams): OperationParams {
448 | const result: OperationParams = {};
449 |
450 | for (const key in params) {
451 | if (Object.prototype.hasOwnProperty.call(params, key)) {
452 | // Convert camelCase to snake_case
453 | const snakeKey = this.reverseParameterMappings[key] || key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
454 |
455 | // Handle nested objects recursively
456 | if (typeof params[key] === 'object' && params[key] !== null && !Array.isArray(params[key])) {
457 | result[snakeKey] = this.convertCamelToSnakeCase(params[key] as OperationParams);
458 | } else {
459 | result[snakeKey] = params[key];
460 | }
461 | }
462 | }
463 |
464 | return result;
465 | }
466 |
467 | /**
468 | * Execute a Godot operation using the operations script
469 | * @param operation The operation to execute
470 | * @param params The parameters for the operation
471 | * @param projectPath The path to the Godot project
472 | * @returns The stdout and stderr from the operation
473 | */
474 | private async executeOperation(
475 | operation: string,
476 | params: OperationParams,
477 | projectPath: string
478 | ): Promise<{ stdout: string; stderr: string }> {
479 | this.logDebug(`Executing operation: ${operation} in project: ${projectPath}`);
480 | this.logDebug(`Original operation params: ${JSON.stringify(params)}`);
481 |
482 | // Convert camelCase parameters to snake_case for Godot script
483 | const snakeCaseParams = this.convertCamelToSnakeCase(params);
484 | this.logDebug(`Converted snake_case params: ${JSON.stringify(snakeCaseParams)}`);
485 |
486 |
487 | // Ensure godotPath is set
488 | if (!this.godotPath) {
489 | await this.detectGodotPath();
490 | if (!this.godotPath) {
491 | throw new Error('Could not find a valid Godot executable path');
492 | }
493 | }
494 |
495 | try {
496 | // Serialize the snake_case parameters to a valid JSON string
497 | const paramsJson = JSON.stringify(snakeCaseParams);
498 | // Escape single quotes in the JSON string to prevent command injection
499 | const escapedParams = paramsJson.replace(/'/g, "'\\''");
500 | // On Windows, cmd.exe does not strip single quotes, so we use
501 | // double quotes and escape them to ensure the JSON is parsed
502 | // correctly by Godot.
503 | const isWindows = process.platform === 'win32';
504 | const quotedParams = isWindows
505 | ? `\"${paramsJson.replace(/\"/g, '\\"')}\"`
506 | : `'${escapedParams}'`;
507 |
508 |
509 | // Add debug arguments if debug mode is enabled
510 | const debugArgs = GODOT_DEBUG_MODE ? ['--debug-godot'] : [];
511 |
512 | // Construct the command with the operation and JSON parameters
513 | const cmd = [
514 | `"${this.godotPath}"`,
515 | '--headless',
516 | '--path',
517 | `"${projectPath}"`,
518 | '--script',
519 | `"${this.operationsScriptPath}"`,
520 | operation,
521 | quotedParams, // Pass the JSON string as a single argument
522 | ...debugArgs,
523 | ].join(' ');
524 |
525 | this.logDebug(`Command: ${cmd}`);
526 |
527 | const { stdout, stderr } = await execAsync(cmd);
528 |
529 | return { stdout, stderr };
530 | } catch (error: unknown) {
531 | // If execAsync throws, it still contains stdout/stderr
532 | if (error instanceof Error && 'stdout' in error && 'stderr' in error) {
533 | const execError = error as Error & { stdout: string; stderr: string };
534 | return {
535 | stdout: execError.stdout,
536 | stderr: execError.stderr,
537 | };
538 | }
539 |
540 | throw error;
541 | }
542 | }
543 |
544 | /**
545 | * Get the structure of a Godot project
546 | * @param projectPath Path to the Godot project
547 | * @returns Object representing the project structure
548 | */
549 | private async getProjectStructure(projectPath: string): Promise<any> {
550 | try {
551 | // Get top-level directories in the project
552 | const entries = readdirSync(projectPath, { withFileTypes: true });
553 |
554 | const structure: any = {
555 | scenes: [],
556 | scripts: [],
557 | assets: [],
558 | other: [],
559 | };
560 |
561 | for (const entry of entries) {
562 | if (entry.isDirectory()) {
563 | const dirName = entry.name.toLowerCase();
564 |
565 | // Skip hidden directories
566 | if (dirName.startsWith('.')) {
567 | continue;
568 | }
569 |
570 | // Count files in common directories
571 | if (dirName === 'scenes' || dirName.includes('scene')) {
572 | structure.scenes.push(entry.name);
573 | } else if (dirName === 'scripts' || dirName.includes('script')) {
574 | structure.scripts.push(entry.name);
575 | } else if (
576 | dirName === 'assets' ||
577 | dirName === 'textures' ||
578 | dirName === 'models' ||
579 | dirName === 'sounds' ||
580 | dirName === 'music'
581 | ) {
582 | structure.assets.push(entry.name);
583 | } else {
584 | structure.other.push(entry.name);
585 | }
586 | }
587 | }
588 |
589 | return structure;
590 | } catch (error) {
591 | this.logDebug(`Error getting project structure: ${error}`);
592 | return { error: 'Failed to get project structure' };
593 | }
594 | }
595 |
596 | /**
597 | * Find Godot projects in a directory
598 | * @param directory Directory to search
599 | * @param recursive Whether to search recursively
600 | * @returns Array of Godot projects
601 | */
602 | private findGodotProjects(directory: string, recursive: boolean): Array<{ path: string; name: string }> {
603 | const projects: Array<{ path: string; name: string }> = [];
604 |
605 | try {
606 | // Check if the directory itself is a Godot project
607 | const projectFile = join(directory, 'project.godot');
608 | if (existsSync(projectFile)) {
609 | projects.push({
610 | path: directory,
611 | name: basename(directory),
612 | });
613 | }
614 |
615 | // If not recursive, only check immediate subdirectories
616 | if (!recursive) {
617 | const entries = readdirSync(directory, { withFileTypes: true });
618 | for (const entry of entries) {
619 | if (entry.isDirectory()) {
620 | const subdir = join(directory, entry.name);
621 | const projectFile = join(subdir, 'project.godot');
622 | if (existsSync(projectFile)) {
623 | projects.push({
624 | path: subdir,
625 | name: entry.name,
626 | });
627 | }
628 | }
629 | }
630 | } else {
631 | // Recursive search
632 | const entries = readdirSync(directory, { withFileTypes: true });
633 | for (const entry of entries) {
634 | if (entry.isDirectory()) {
635 | const subdir = join(directory, entry.name);
636 | // Skip hidden directories
637 | if (entry.name.startsWith('.')) {
638 | continue;
639 | }
640 | // Check if this directory is a Godot project
641 | const projectFile = join(subdir, 'project.godot');
642 | if (existsSync(projectFile)) {
643 | projects.push({
644 | path: subdir,
645 | name: entry.name,
646 | });
647 | } else {
648 | // Recursively search this directory
649 | const subProjects = this.findGodotProjects(subdir, true);
650 | projects.push(...subProjects);
651 | }
652 | }
653 | }
654 | }
655 | } catch (error) {
656 | this.logDebug(`Error searching directory ${directory}: ${error}`);
657 | }
658 |
659 | return projects;
660 | }
661 |
662 | /**
663 | * Set up the tool handlers for the MCP server
664 | */
665 | private setupToolHandlers() {
666 | // Define available tools
667 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
668 | tools: [
669 | {
670 | name: 'launch_editor',
671 | description: 'Launch Godot editor for a specific project',
672 | inputSchema: {
673 | type: 'object',
674 | properties: {
675 | projectPath: {
676 | type: 'string',
677 | description: 'Path to the Godot project directory',
678 | },
679 | },
680 | required: ['projectPath'],
681 | },
682 | },
683 | {
684 | name: 'run_project',
685 | description: 'Run the Godot project and capture output',
686 | inputSchema: {
687 | type: 'object',
688 | properties: {
689 | projectPath: {
690 | type: 'string',
691 | description: 'Path to the Godot project directory',
692 | },
693 | scene: {
694 | type: 'string',
695 | description: 'Optional: Specific scene to run',
696 | },
697 | },
698 | required: ['projectPath'],
699 | },
700 | },
701 | {
702 | name: 'get_debug_output',
703 | description: 'Get the current debug output and errors',
704 | inputSchema: {
705 | type: 'object',
706 | properties: {},
707 | required: [],
708 | },
709 | },
710 | {
711 | name: 'stop_project',
712 | description: 'Stop the currently running Godot project',
713 | inputSchema: {
714 | type: 'object',
715 | properties: {},
716 | required: [],
717 | },
718 | },
719 | {
720 | name: 'get_godot_version',
721 | description: 'Get the installed Godot version',
722 | inputSchema: {
723 | type: 'object',
724 | properties: {},
725 | required: [],
726 | },
727 | },
728 | {
729 | name: 'list_projects',
730 | description: 'List Godot projects in a directory',
731 | inputSchema: {
732 | type: 'object',
733 | properties: {
734 | directory: {
735 | type: 'string',
736 | description: 'Directory to search for Godot projects',
737 | },
738 | recursive: {
739 | type: 'boolean',
740 | description: 'Whether to search recursively (default: false)',
741 | },
742 | },
743 | required: ['directory'],
744 | },
745 | },
746 | {
747 | name: 'get_project_info',
748 | description: 'Retrieve metadata about a Godot project',
749 | inputSchema: {
750 | type: 'object',
751 | properties: {
752 | projectPath: {
753 | type: 'string',
754 | description: 'Path to the Godot project directory',
755 | },
756 | },
757 | required: ['projectPath'],
758 | },
759 | },
760 | {
761 | name: 'create_scene',
762 | description: 'Create a new Godot scene file',
763 | inputSchema: {
764 | type: 'object',
765 | properties: {
766 | projectPath: {
767 | type: 'string',
768 | description: 'Path to the Godot project directory',
769 | },
770 | scenePath: {
771 | type: 'string',
772 | description: 'Path where the scene file will be saved (relative to project)',
773 | },
774 | rootNodeType: {
775 | type: 'string',
776 | description: 'Type of the root node (e.g., Node2D, Node3D)',
777 | default: 'Node2D',
778 | },
779 | },
780 | required: ['projectPath', 'scenePath'],
781 | },
782 | },
783 | {
784 | name: 'add_node',
785 | description: 'Add a node to an existing scene',
786 | inputSchema: {
787 | type: 'object',
788 | properties: {
789 | projectPath: {
790 | type: 'string',
791 | description: 'Path to the Godot project directory',
792 | },
793 | scenePath: {
794 | type: 'string',
795 | description: 'Path to the scene file (relative to project)',
796 | },
797 | parentNodePath: {
798 | type: 'string',
799 | description: 'Path to the parent node (e.g., "root" or "root/Player")',
800 | default: 'root',
801 | },
802 | nodeType: {
803 | type: 'string',
804 | description: 'Type of node to add (e.g., Sprite2D, CollisionShape2D)',
805 | },
806 | nodeName: {
807 | type: 'string',
808 | description: 'Name for the new node',
809 | },
810 | properties: {
811 | type: 'object',
812 | description: 'Optional properties to set on the node',
813 | },
814 | },
815 | required: ['projectPath', 'scenePath', 'nodeType', 'nodeName'],
816 | },
817 | },
818 | {
819 | name: 'load_sprite',
820 | description: 'Load a sprite into a Sprite2D node',
821 | inputSchema: {
822 | type: 'object',
823 | properties: {
824 | projectPath: {
825 | type: 'string',
826 | description: 'Path to the Godot project directory',
827 | },
828 | scenePath: {
829 | type: 'string',
830 | description: 'Path to the scene file (relative to project)',
831 | },
832 | nodePath: {
833 | type: 'string',
834 | description: 'Path to the Sprite2D node (e.g., "root/Player/Sprite2D")',
835 | },
836 | texturePath: {
837 | type: 'string',
838 | description: 'Path to the texture file (relative to project)',
839 | },
840 | },
841 | required: ['projectPath', 'scenePath', 'nodePath', 'texturePath'],
842 | },
843 | },
844 | {
845 | name: 'export_mesh_library',
846 | description: 'Export a scene as a MeshLibrary resource',
847 | inputSchema: {
848 | type: 'object',
849 | properties: {
850 | projectPath: {
851 | type: 'string',
852 | description: 'Path to the Godot project directory',
853 | },
854 | scenePath: {
855 | type: 'string',
856 | description: 'Path to the scene file (.tscn) to export',
857 | },
858 | outputPath: {
859 | type: 'string',
860 | description: 'Path where the mesh library (.res) will be saved',
861 | },
862 | meshItemNames: {
863 | type: 'array',
864 | items: {
865 | type: 'string',
866 | },
867 | description: 'Optional: Names of specific mesh items to include (defaults to all)',
868 | },
869 | },
870 | required: ['projectPath', 'scenePath', 'outputPath'],
871 | },
872 | },
873 | {
874 | name: 'save_scene',
875 | description: 'Save changes to a scene file',
876 | inputSchema: {
877 | type: 'object',
878 | properties: {
879 | projectPath: {
880 | type: 'string',
881 | description: 'Path to the Godot project directory',
882 | },
883 | scenePath: {
884 | type: 'string',
885 | description: 'Path to the scene file (relative to project)',
886 | },
887 | newPath: {
888 | type: 'string',
889 | description: 'Optional: New path to save the scene to (for creating variants)',
890 | },
891 | },
892 | required: ['projectPath', 'scenePath'],
893 | },
894 | },
895 | {
896 | name: 'get_uid',
897 | description: 'Get the UID for a specific file in a Godot project (for Godot 4.4+)',
898 | inputSchema: {
899 | type: 'object',
900 | properties: {
901 | projectPath: {
902 | type: 'string',
903 | description: 'Path to the Godot project directory',
904 | },
905 | filePath: {
906 | type: 'string',
907 | description: 'Path to the file (relative to project) for which to get the UID',
908 | },
909 | },
910 | required: ['projectPath', 'filePath'],
911 | },
912 | },
913 | {
914 | name: 'update_project_uids',
915 | description: 'Update UID references in a Godot project by resaving resources (for Godot 4.4+)',
916 | inputSchema: {
917 | type: 'object',
918 | properties: {
919 | projectPath: {
920 | type: 'string',
921 | description: 'Path to the Godot project directory',
922 | },
923 | },
924 | required: ['projectPath'],
925 | },
926 | },
927 | ],
928 | }));
929 |
930 | // Handle tool calls
931 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
932 | this.logDebug(`Handling tool request: ${request.params.name}`);
933 | switch (request.params.name) {
934 | case 'launch_editor':
935 | return await this.handleLaunchEditor(request.params.arguments);
936 | case 'run_project':
937 | return await this.handleRunProject(request.params.arguments);
938 | case 'get_debug_output':
939 | return await this.handleGetDebugOutput();
940 | case 'stop_project':
941 | return await this.handleStopProject();
942 | case 'get_godot_version':
943 | return await this.handleGetGodotVersion();
944 | case 'list_projects':
945 | return await this.handleListProjects(request.params.arguments);
946 | case 'get_project_info':
947 | return await this.handleGetProjectInfo(request.params.arguments);
948 | case 'create_scene':
949 | return await this.handleCreateScene(request.params.arguments);
950 | case 'add_node':
951 | return await this.handleAddNode(request.params.arguments);
952 | case 'load_sprite':
953 | return await this.handleLoadSprite(request.params.arguments);
954 | case 'export_mesh_library':
955 | return await this.handleExportMeshLibrary(request.params.arguments);
956 | case 'save_scene':
957 | return await this.handleSaveScene(request.params.arguments);
958 | case 'get_uid':
959 | return await this.handleGetUid(request.params.arguments);
960 | case 'update_project_uids':
961 | return await this.handleUpdateProjectUids(request.params.arguments);
962 | default:
963 | throw new McpError(
964 | ErrorCode.MethodNotFound,
965 | `Unknown tool: ${request.params.name}`
966 | );
967 | }
968 | });
969 | }
970 |
971 | /**
972 | * Handle the launch_editor tool
973 | * @param args Tool arguments
974 | */
975 | private async handleLaunchEditor(args: any) {
976 | // Normalize parameters to camelCase
977 | args = this.normalizeParameters(args);
978 |
979 | if (!args.projectPath) {
980 | return this.createErrorResponse(
981 | 'Project path is required',
982 | ['Provide a valid path to a Godot project directory']
983 | );
984 | }
985 |
986 | if (!this.validatePath(args.projectPath)) {
987 | return this.createErrorResponse(
988 | 'Invalid project path',
989 | ['Provide a valid path without ".." or other potentially unsafe characters']
990 | );
991 | }
992 |
993 | try {
994 | // Ensure godotPath is set
995 | if (!this.godotPath) {
996 | await this.detectGodotPath();
997 | if (!this.godotPath) {
998 | return this.createErrorResponse(
999 | 'Could not find a valid Godot executable path',
1000 | [
1001 | 'Ensure Godot is installed correctly',
1002 | 'Set GODOT_PATH environment variable to specify the correct path',
1003 | ]
1004 | );
1005 | }
1006 | }
1007 |
1008 | // Check if the project directory exists and contains a project.godot file
1009 | const projectFile = join(args.projectPath, 'project.godot');
1010 | if (!existsSync(projectFile)) {
1011 | return this.createErrorResponse(
1012 | `Not a valid Godot project: ${args.projectPath}`,
1013 | [
1014 | 'Ensure the path points to a directory containing a project.godot file',
1015 | 'Use list_projects to find valid Godot projects',
1016 | ]
1017 | );
1018 | }
1019 |
1020 | this.logDebug(`Launching Godot editor for project: ${args.projectPath}`);
1021 | const process = spawn(this.godotPath, ['-e', '--path', args.projectPath], {
1022 | stdio: 'pipe',
1023 | });
1024 |
1025 | process.on('error', (err: Error) => {
1026 | console.error('Failed to start Godot editor:', err);
1027 | });
1028 |
1029 | return {
1030 | content: [
1031 | {
1032 | type: 'text',
1033 | text: `Godot editor launched successfully for project at ${args.projectPath}.`,
1034 | },
1035 | ],
1036 | };
1037 | } catch (error: unknown) {
1038 | const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1039 | return this.createErrorResponse(
1040 | `Failed to launch Godot editor: ${errorMessage}`,
1041 | [
1042 | 'Ensure Godot is installed correctly',
1043 | 'Check if the GODOT_PATH environment variable is set correctly',
1044 | 'Verify the project path is accessible',
1045 | ]
1046 | );
1047 | }
1048 | }
1049 |
1050 | /**
1051 | * Handle the run_project tool
1052 | * @param args Tool arguments
1053 | */
1054 | private async handleRunProject(args: any) {
1055 | // Normalize parameters to camelCase
1056 | args = this.normalizeParameters(args);
1057 |
1058 | if (!args.projectPath) {
1059 | return this.createErrorResponse(
1060 | 'Project path is required',
1061 | ['Provide a valid path to a Godot project directory']
1062 | );
1063 | }
1064 |
1065 | if (!this.validatePath(args.projectPath)) {
1066 | return this.createErrorResponse(
1067 | 'Invalid project path',
1068 | ['Provide a valid path without ".." or other potentially unsafe characters']
1069 | );
1070 | }
1071 |
1072 | try {
1073 | // Check if the project directory exists and contains a project.godot file
1074 | const projectFile = join(args.projectPath, 'project.godot');
1075 | if (!existsSync(projectFile)) {
1076 | return this.createErrorResponse(
1077 | `Not a valid Godot project: ${args.projectPath}`,
1078 | [
1079 | 'Ensure the path points to a directory containing a project.godot file',
1080 | 'Use list_projects to find valid Godot projects',
1081 | ]
1082 | );
1083 | }
1084 |
1085 | // Kill any existing process
1086 | if (this.activeProcess) {
1087 | this.logDebug('Killing existing Godot process before starting a new one');
1088 | this.activeProcess.process.kill();
1089 | }
1090 |
1091 | const cmdArgs = ['-d', '--path', args.projectPath];
1092 | if (args.scene && this.validatePath(args.scene)) {
1093 | this.logDebug(`Adding scene parameter: ${args.scene}`);
1094 | cmdArgs.push(args.scene);
1095 | }
1096 |
1097 | this.logDebug(`Running Godot project: ${args.projectPath}`);
1098 | const process = spawn(this.godotPath!, cmdArgs, { stdio: 'pipe' });
1099 | const output: string[] = [];
1100 | const errors: string[] = [];
1101 |
1102 | process.stdout?.on('data', (data: Buffer) => {
1103 | const lines = data.toString().split('\n');
1104 | output.push(...lines);
1105 | lines.forEach((line: string) => {
1106 | if (line.trim()) this.logDebug(`[Godot stdout] ${line}`);
1107 | });
1108 | });
1109 |
1110 | process.stderr?.on('data', (data: Buffer) => {
1111 | const lines = data.toString().split('\n');
1112 | errors.push(...lines);
1113 | lines.forEach((line: string) => {
1114 | if (line.trim()) this.logDebug(`[Godot stderr] ${line}`);
1115 | });
1116 | });
1117 |
1118 | process.on('exit', (code: number | null) => {
1119 | this.logDebug(`Godot process exited with code ${code}`);
1120 | if (this.activeProcess && this.activeProcess.process === process) {
1121 | this.activeProcess = null;
1122 | }
1123 | });
1124 |
1125 | process.on('error', (err: Error) => {
1126 | console.error('Failed to start Godot process:', err);
1127 | if (this.activeProcess && this.activeProcess.process === process) {
1128 | this.activeProcess = null;
1129 | }
1130 | });
1131 |
1132 | this.activeProcess = { process, output, errors };
1133 |
1134 | return {
1135 | content: [
1136 | {
1137 | type: 'text',
1138 | text: `Godot project started in debug mode. Use get_debug_output to see output.`,
1139 | },
1140 | ],
1141 | };
1142 | } catch (error: unknown) {
1143 | const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1144 | return this.createErrorResponse(
1145 | `Failed to run Godot project: ${errorMessage}`,
1146 | [
1147 | 'Ensure Godot is installed correctly',
1148 | 'Check if the GODOT_PATH environment variable is set correctly',
1149 | 'Verify the project path is accessible',
1150 | ]
1151 | );
1152 | }
1153 | }
1154 |
1155 | /**
1156 | * Handle the get_debug_output tool
1157 | */
1158 | private async handleGetDebugOutput() {
1159 | if (!this.activeProcess) {
1160 | return this.createErrorResponse(
1161 | 'No active Godot process.',
1162 | [
1163 | 'Use run_project to start a Godot project first',
1164 | 'Check if the Godot process crashed unexpectedly',
1165 | ]
1166 | );
1167 | }
1168 |
1169 | return {
1170 | content: [
1171 | {
1172 | type: 'text',
1173 | text: JSON.stringify(
1174 | {
1175 | output: this.activeProcess.output,
1176 | errors: this.activeProcess.errors,
1177 | },
1178 | null,
1179 | 2
1180 | ),
1181 | },
1182 | ],
1183 | };
1184 | }
1185 |
1186 | /**
1187 | * Handle the stop_project tool
1188 | */
1189 | private async handleStopProject() {
1190 | if (!this.activeProcess) {
1191 | return this.createErrorResponse(
1192 | 'No active Godot process to stop.',
1193 | [
1194 | 'Use run_project to start a Godot project first',
1195 | 'The process may have already terminated',
1196 | ]
1197 | );
1198 | }
1199 |
1200 | this.logDebug('Stopping active Godot process');
1201 | this.activeProcess.process.kill();
1202 | const output = this.activeProcess.output;
1203 | const errors = this.activeProcess.errors;
1204 | this.activeProcess = null;
1205 |
1206 | return {
1207 | content: [
1208 | {
1209 | type: 'text',
1210 | text: JSON.stringify(
1211 | {
1212 | message: 'Godot project stopped',
1213 | finalOutput: output,
1214 | finalErrors: errors,
1215 | },
1216 | null,
1217 | 2
1218 | ),
1219 | },
1220 | ],
1221 | };
1222 | }
1223 |
1224 | /**
1225 | * Handle the get_godot_version tool
1226 | */
1227 | private async handleGetGodotVersion() {
1228 | try {
1229 | // Ensure godotPath is set
1230 | if (!this.godotPath) {
1231 | await this.detectGodotPath();
1232 | if (!this.godotPath) {
1233 | return this.createErrorResponse(
1234 | 'Could not find a valid Godot executable path',
1235 | [
1236 | 'Ensure Godot is installed correctly',
1237 | 'Set GODOT_PATH environment variable to specify the correct path',
1238 | ]
1239 | );
1240 | }
1241 | }
1242 |
1243 | this.logDebug('Getting Godot version');
1244 | const { stdout } = await execAsync(`"${this.godotPath}" --version`);
1245 | return {
1246 | content: [
1247 | {
1248 | type: 'text',
1249 | text: stdout.trim(),
1250 | },
1251 | ],
1252 | };
1253 | } catch (error: unknown) {
1254 | const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1255 | return this.createErrorResponse(
1256 | `Failed to get Godot version: ${errorMessage}`,
1257 | [
1258 | 'Ensure Godot is installed correctly',
1259 | 'Check if the GODOT_PATH environment variable is set correctly',
1260 | ]
1261 | );
1262 | }
1263 | }
1264 |
1265 | /**
1266 | * Handle the list_projects tool
1267 | */
1268 | private async handleListProjects(args: any) {
1269 | // Normalize parameters to camelCase
1270 | args = this.normalizeParameters(args);
1271 |
1272 | if (!args.directory) {
1273 | return this.createErrorResponse(
1274 | 'Directory is required',
1275 | ['Provide a valid directory path to search for Godot projects']
1276 | );
1277 | }
1278 |
1279 | if (!this.validatePath(args.directory)) {
1280 | return this.createErrorResponse(
1281 | 'Invalid directory path',
1282 | ['Provide a valid path without ".." or other potentially unsafe characters']
1283 | );
1284 | }
1285 |
1286 | try {
1287 | this.logDebug(`Listing Godot projects in directory: ${args.directory}`);
1288 | if (!existsSync(args.directory)) {
1289 | return this.createErrorResponse(
1290 | `Directory does not exist: ${args.directory}`,
1291 | ['Provide a valid directory path that exists on the system']
1292 | );
1293 | }
1294 |
1295 | const recursive = args.recursive === true;
1296 | const projects = this.findGodotProjects(args.directory, recursive);
1297 |
1298 | return {
1299 | content: [
1300 | {
1301 | type: 'text',
1302 | text: JSON.stringify(projects, null, 2),
1303 | },
1304 | ],
1305 | };
1306 | } catch (error: any) {
1307 | return this.createErrorResponse(
1308 | `Failed to list projects: ${error?.message || 'Unknown error'}`,
1309 | [
1310 | 'Ensure the directory exists and is accessible',
1311 | 'Check if you have permission to read the directory',
1312 | ]
1313 | );
1314 | }
1315 | }
1316 |
1317 | /**
1318 | * Get the structure of a Godot project asynchronously by counting files recursively
1319 | * @param projectPath Path to the Godot project
1320 | * @returns Promise resolving to an object with counts of scenes, scripts, assets, and other files
1321 | */
1322 | private getProjectStructureAsync(projectPath: string): Promise<any> {
1323 | return new Promise((resolve) => {
1324 | try {
1325 | const structure = {
1326 | scenes: 0,
1327 | scripts: 0,
1328 | assets: 0,
1329 | other: 0,
1330 | };
1331 |
1332 | const scanDirectory = (currentPath: string) => {
1333 | const entries = readdirSync(currentPath, { withFileTypes: true });
1334 |
1335 | for (const entry of entries) {
1336 | const entryPath = join(currentPath, entry.name);
1337 |
1338 | // Skip hidden files and directories
1339 | if (entry.name.startsWith('.')) {
1340 | continue;
1341 | }
1342 |
1343 | if (entry.isDirectory()) {
1344 | // Recursively scan subdirectories
1345 | scanDirectory(entryPath);
1346 | } else if (entry.isFile()) {
1347 | // Count file by extension
1348 | const ext = entry.name.split('.').pop()?.toLowerCase();
1349 |
1350 | if (ext === 'tscn') {
1351 | structure.scenes++;
1352 | } else if (ext === 'gd' || ext === 'gdscript' || ext === 'cs') {
1353 | structure.scripts++;
1354 | } else if (['png', 'jpg', 'jpeg', 'webp', 'svg', 'ttf', 'wav', 'mp3', 'ogg'].includes(ext || '')) {
1355 | structure.assets++;
1356 | } else {
1357 | structure.other++;
1358 | }
1359 | }
1360 | }
1361 | };
1362 |
1363 | // Start scanning from the project root
1364 | scanDirectory(projectPath);
1365 | resolve(structure);
1366 | } catch (error) {
1367 | this.logDebug(`Error getting project structure asynchronously: ${error}`);
1368 | resolve({
1369 | error: 'Failed to get project structure',
1370 | scenes: 0,
1371 | scripts: 0,
1372 | assets: 0,
1373 | other: 0
1374 | });
1375 | }
1376 | });
1377 | }
1378 |
1379 | /**
1380 | * Handle the get_project_info tool
1381 | */
1382 | private async handleGetProjectInfo(args: any) {
1383 | // Normalize parameters to camelCase
1384 | args = this.normalizeParameters(args);
1385 |
1386 | if (!args.projectPath) {
1387 | return this.createErrorResponse(
1388 | 'Project path is required',
1389 | ['Provide a valid path to a Godot project directory']
1390 | );
1391 | }
1392 |
1393 | if (!this.validatePath(args.projectPath)) {
1394 | return this.createErrorResponse(
1395 | 'Invalid project path',
1396 | ['Provide a valid path without ".." or other potentially unsafe characters']
1397 | );
1398 | }
1399 |
1400 | try {
1401 | // Ensure godotPath is set
1402 | if (!this.godotPath) {
1403 | await this.detectGodotPath();
1404 | if (!this.godotPath) {
1405 | return this.createErrorResponse(
1406 | 'Could not find a valid Godot executable path',
1407 | [
1408 | 'Ensure Godot is installed correctly',
1409 | 'Set GODOT_PATH environment variable to specify the correct path',
1410 | ]
1411 | );
1412 | }
1413 | }
1414 |
1415 | // Check if the project directory exists and contains a project.godot file
1416 | const projectFile = join(args.projectPath, 'project.godot');
1417 | if (!existsSync(projectFile)) {
1418 | return this.createErrorResponse(
1419 | `Not a valid Godot project: ${args.projectPath}`,
1420 | [
1421 | 'Ensure the path points to a directory containing a project.godot file',
1422 | 'Use list_projects to find valid Godot projects',
1423 | ]
1424 | );
1425 | }
1426 |
1427 | this.logDebug(`Getting project info for: ${args.projectPath}`);
1428 |
1429 | // Get Godot version
1430 | const execOptions = { timeout: 10000 }; // 10 second timeout
1431 | const { stdout } = await execAsync(`"${this.godotPath}" --version`, execOptions);
1432 |
1433 | // Get project structure using the recursive method
1434 | const projectStructure = await this.getProjectStructureAsync(args.projectPath);
1435 |
1436 | // Extract project name from project.godot file
1437 | let projectName = basename(args.projectPath);
1438 | try {
1439 | const fs = require('fs');
1440 | const projectFileContent = fs.readFileSync(projectFile, 'utf8');
1441 | const configNameMatch = projectFileContent.match(/config\/name="([^"]+)"/);
1442 | if (configNameMatch && configNameMatch[1]) {
1443 | projectName = configNameMatch[1];
1444 | this.logDebug(`Found project name in config: ${projectName}`);
1445 | }
1446 | } catch (error) {
1447 | this.logDebug(`Error reading project file: ${error}`);
1448 | // Continue with default project name if extraction fails
1449 | }
1450 |
1451 | return {
1452 | content: [
1453 | {
1454 | type: 'text',
1455 | text: JSON.stringify(
1456 | {
1457 | name: projectName,
1458 | path: args.projectPath,
1459 | godotVersion: stdout.trim(),
1460 | structure: projectStructure,
1461 | },
1462 | null,
1463 | 2
1464 | ),
1465 | },
1466 | ],
1467 | };
1468 | } catch (error: any) {
1469 | return this.createErrorResponse(
1470 | `Failed to get project info: ${error?.message || 'Unknown error'}`,
1471 | [
1472 | 'Ensure Godot is installed correctly',
1473 | 'Check if the GODOT_PATH environment variable is set correctly',
1474 | 'Verify the project path is accessible',
1475 | ]
1476 | );
1477 | }
1478 | }
1479 |
1480 | /**
1481 | * Handle the create_scene tool
1482 | */
1483 | private async handleCreateScene(args: any) {
1484 | // Normalize parameters to camelCase
1485 | args = this.normalizeParameters(args);
1486 |
1487 | if (!args.projectPath || !args.scenePath) {
1488 | return this.createErrorResponse(
1489 | 'Project path and scene path are required',
1490 | ['Provide valid paths for both the project and the scene']
1491 | );
1492 | }
1493 |
1494 | if (!this.validatePath(args.projectPath) || !this.validatePath(args.scenePath)) {
1495 | return this.createErrorResponse(
1496 | 'Invalid path',
1497 | ['Provide valid paths without ".." or other potentially unsafe characters']
1498 | );
1499 | }
1500 |
1501 | try {
1502 | // Check if the project directory exists and contains a project.godot file
1503 | const projectFile = join(args.projectPath, 'project.godot');
1504 | if (!existsSync(projectFile)) {
1505 | return this.createErrorResponse(
1506 | `Not a valid Godot project: ${args.projectPath}`,
1507 | [
1508 | 'Ensure the path points to a directory containing a project.godot file',
1509 | 'Use list_projects to find valid Godot projects',
1510 | ]
1511 | );
1512 | }
1513 |
1514 | // Prepare parameters for the operation (already in camelCase)
1515 | const params = {
1516 | scenePath: args.scenePath,
1517 | rootNodeType: args.rootNodeType || 'Node2D',
1518 | };
1519 |
1520 | // Execute the operation
1521 | const { stdout, stderr } = await this.executeOperation('create_scene', params, args.projectPath);
1522 |
1523 | if (stderr && stderr.includes('Failed to')) {
1524 | return this.createErrorResponse(
1525 | `Failed to create scene: ${stderr}`,
1526 | [
1527 | 'Check if the root node type is valid',
1528 | 'Ensure you have write permissions to the scene path',
1529 | 'Verify the scene path is valid',
1530 | ]
1531 | );
1532 | }
1533 |
1534 | return {
1535 | content: [
1536 | {
1537 | type: 'text',
1538 | text: `Scene created successfully at: ${args.scenePath}\n\nOutput: ${stdout}`,
1539 | },
1540 | ],
1541 | };
1542 | } catch (error: any) {
1543 | return this.createErrorResponse(
1544 | `Failed to create scene: ${error?.message || 'Unknown error'}`,
1545 | [
1546 | 'Ensure Godot is installed correctly',
1547 | 'Check if the GODOT_PATH environment variable is set correctly',
1548 | 'Verify the project path is accessible',
1549 | ]
1550 | );
1551 | }
1552 | }
1553 |
1554 | /**
1555 | * Handle the add_node tool
1556 | */
1557 | private async handleAddNode(args: any) {
1558 | // Normalize parameters to camelCase
1559 | args = this.normalizeParameters(args);
1560 |
1561 | if (!args.projectPath || !args.scenePath || !args.nodeType || !args.nodeName) {
1562 | return this.createErrorResponse(
1563 | 'Missing required parameters',
1564 | ['Provide projectPath, scenePath, nodeType, and nodeName']
1565 | );
1566 | }
1567 |
1568 | if (!this.validatePath(args.projectPath) || !this.validatePath(args.scenePath)) {
1569 | return this.createErrorResponse(
1570 | 'Invalid path',
1571 | ['Provide valid paths without ".." or other potentially unsafe characters']
1572 | );
1573 | }
1574 |
1575 | try {
1576 | // Check if the project directory exists and contains a project.godot file
1577 | const projectFile = join(args.projectPath, 'project.godot');
1578 | if (!existsSync(projectFile)) {
1579 | return this.createErrorResponse(
1580 | `Not a valid Godot project: ${args.projectPath}`,
1581 | [
1582 | 'Ensure the path points to a directory containing a project.godot file',
1583 | 'Use list_projects to find valid Godot projects',
1584 | ]
1585 | );
1586 | }
1587 |
1588 | // Check if the scene file exists
1589 | const scenePath = join(args.projectPath, args.scenePath);
1590 | if (!existsSync(scenePath)) {
1591 | return this.createErrorResponse(
1592 | `Scene file does not exist: ${args.scenePath}`,
1593 | [
1594 | 'Ensure the scene path is correct',
1595 | 'Use create_scene to create a new scene first',
1596 | ]
1597 | );
1598 | }
1599 |
1600 | // Prepare parameters for the operation (already in camelCase)
1601 | const params: any = {
1602 | scenePath: args.scenePath,
1603 | nodeType: args.nodeType,
1604 | nodeName: args.nodeName,
1605 | };
1606 |
1607 | // Add optional parameters
1608 | if (args.parentNodePath) {
1609 | params.parentNodePath = args.parentNodePath;
1610 | }
1611 |
1612 | if (args.properties) {
1613 | params.properties = args.properties;
1614 | }
1615 |
1616 | // Execute the operation
1617 | const { stdout, stderr } = await this.executeOperation('add_node', params, args.projectPath);
1618 |
1619 | if (stderr && stderr.includes('Failed to')) {
1620 | return this.createErrorResponse(
1621 | `Failed to add node: ${stderr}`,
1622 | [
1623 | 'Check if the node type is valid',
1624 | 'Ensure the parent node path exists',
1625 | 'Verify the scene file is valid',
1626 | ]
1627 | );
1628 | }
1629 |
1630 | return {
1631 | content: [
1632 | {
1633 | type: 'text',
1634 | text: `Node '${args.nodeName}' of type '${args.nodeType}' added successfully to '${args.scenePath}'.\n\nOutput: ${stdout}`,
1635 | },
1636 | ],
1637 | };
1638 | } catch (error: any) {
1639 | return this.createErrorResponse(
1640 | `Failed to add node: ${error?.message || 'Unknown error'}`,
1641 | [
1642 | 'Ensure Godot is installed correctly',
1643 | 'Check if the GODOT_PATH environment variable is set correctly',
1644 | 'Verify the project path is accessible',
1645 | ]
1646 | );
1647 | }
1648 | }
1649 |
1650 | /**
1651 | * Handle the load_sprite tool
1652 | */
1653 | private async handleLoadSprite(args: any) {
1654 | // Normalize parameters to camelCase
1655 | args = this.normalizeParameters(args);
1656 |
1657 | if (!args.projectPath || !args.scenePath || !args.nodePath || !args.texturePath) {
1658 | return this.createErrorResponse(
1659 | 'Missing required parameters',
1660 | ['Provide projectPath, scenePath, nodePath, and texturePath']
1661 | );
1662 | }
1663 |
1664 | if (
1665 | !this.validatePath(args.projectPath) ||
1666 | !this.validatePath(args.scenePath) ||
1667 | !this.validatePath(args.nodePath) ||
1668 | !this.validatePath(args.texturePath)
1669 | ) {
1670 | return this.createErrorResponse(
1671 | 'Invalid path',
1672 | ['Provide valid paths without ".." or other potentially unsafe characters']
1673 | );
1674 | }
1675 |
1676 | try {
1677 | // Check if the project directory exists and contains a project.godot file
1678 | const projectFile = join(args.projectPath, 'project.godot');
1679 | if (!existsSync(projectFile)) {
1680 | return this.createErrorResponse(
1681 | `Not a valid Godot project: ${args.projectPath}`,
1682 | [
1683 | 'Ensure the path points to a directory containing a project.godot file',
1684 | 'Use list_projects to find valid Godot projects',
1685 | ]
1686 | );
1687 | }
1688 |
1689 | // Check if the scene file exists
1690 | const scenePath = join(args.projectPath, args.scenePath);
1691 | if (!existsSync(scenePath)) {
1692 | return this.createErrorResponse(
1693 | `Scene file does not exist: ${args.scenePath}`,
1694 | [
1695 | 'Ensure the scene path is correct',
1696 | 'Use create_scene to create a new scene first',
1697 | ]
1698 | );
1699 | }
1700 |
1701 | // Check if the texture file exists
1702 | const texturePath = join(args.projectPath, args.texturePath);
1703 | if (!existsSync(texturePath)) {
1704 | return this.createErrorResponse(
1705 | `Texture file does not exist: ${args.texturePath}`,
1706 | [
1707 | 'Ensure the texture path is correct',
1708 | 'Upload or create the texture file first',
1709 | ]
1710 | );
1711 | }
1712 |
1713 | // Prepare parameters for the operation (already in camelCase)
1714 | const params = {
1715 | scenePath: args.scenePath,
1716 | nodePath: args.nodePath,
1717 | texturePath: args.texturePath,
1718 | };
1719 |
1720 | // Execute the operation
1721 | const { stdout, stderr } = await this.executeOperation('load_sprite', params, args.projectPath);
1722 |
1723 | if (stderr && stderr.includes('Failed to')) {
1724 | return this.createErrorResponse(
1725 | `Failed to load sprite: ${stderr}`,
1726 | [
1727 | 'Check if the node path is correct',
1728 | 'Ensure the node is a Sprite2D, Sprite3D, or TextureRect',
1729 | 'Verify the texture file is a valid image format',
1730 | ]
1731 | );
1732 | }
1733 |
1734 | return {
1735 | content: [
1736 | {
1737 | type: 'text',
1738 | text: `Sprite loaded successfully with texture: ${args.texturePath}\n\nOutput: ${stdout}`,
1739 | },
1740 | ],
1741 | };
1742 | } catch (error: any) {
1743 | return this.createErrorResponse(
1744 | `Failed to load sprite: ${error?.message || 'Unknown error'}`,
1745 | [
1746 | 'Ensure Godot is installed correctly',
1747 | 'Check if the GODOT_PATH environment variable is set correctly',
1748 | 'Verify the project path is accessible',
1749 | ]
1750 | );
1751 | }
1752 | }
1753 |
1754 | /**
1755 | * Handle the export_mesh_library tool
1756 | */
1757 | private async handleExportMeshLibrary(args: any) {
1758 | // Normalize parameters to camelCase
1759 | args = this.normalizeParameters(args);
1760 |
1761 | if (!args.projectPath || !args.scenePath || !args.outputPath) {
1762 | return this.createErrorResponse(
1763 | 'Missing required parameters',
1764 | ['Provide projectPath, scenePath, and outputPath']
1765 | );
1766 | }
1767 |
1768 | if (
1769 | !this.validatePath(args.projectPath) ||
1770 | !this.validatePath(args.scenePath) ||
1771 | !this.validatePath(args.outputPath)
1772 | ) {
1773 | return this.createErrorResponse(
1774 | 'Invalid path',
1775 | ['Provide valid paths without ".." or other potentially unsafe characters']
1776 | );
1777 | }
1778 |
1779 | try {
1780 | // Check if the project directory exists and contains a project.godot file
1781 | const projectFile = join(args.projectPath, 'project.godot');
1782 | if (!existsSync(projectFile)) {
1783 | return this.createErrorResponse(
1784 | `Not a valid Godot project: ${args.projectPath}`,
1785 | [
1786 | 'Ensure the path points to a directory containing a project.godot file',
1787 | 'Use list_projects to find valid Godot projects',
1788 | ]
1789 | );
1790 | }
1791 |
1792 | // Check if the scene file exists
1793 | const scenePath = join(args.projectPath, args.scenePath);
1794 | if (!existsSync(scenePath)) {
1795 | return this.createErrorResponse(
1796 | `Scene file does not exist: ${args.scenePath}`,
1797 | [
1798 | 'Ensure the scene path is correct',
1799 | 'Use create_scene to create a new scene first',
1800 | ]
1801 | );
1802 | }
1803 |
1804 | // Prepare parameters for the operation (already in camelCase)
1805 | const params: any = {
1806 | scenePath: args.scenePath,
1807 | outputPath: args.outputPath,
1808 | };
1809 |
1810 | // Add optional parameters
1811 | if (args.meshItemNames && Array.isArray(args.meshItemNames)) {
1812 | params.meshItemNames = args.meshItemNames;
1813 | }
1814 |
1815 | // Execute the operation
1816 | const { stdout, stderr } = await this.executeOperation('export_mesh_library', params, args.projectPath);
1817 |
1818 | if (stderr && stderr.includes('Failed to')) {
1819 | return this.createErrorResponse(
1820 | `Failed to export mesh library: ${stderr}`,
1821 | [
1822 | 'Check if the scene contains valid 3D meshes',
1823 | 'Ensure the output path is valid',
1824 | 'Verify the scene file is valid',
1825 | ]
1826 | );
1827 | }
1828 |
1829 | return {
1830 | content: [
1831 | {
1832 | type: 'text',
1833 | text: `MeshLibrary exported successfully to: ${args.outputPath}\n\nOutput: ${stdout}`,
1834 | },
1835 | ],
1836 | };
1837 | } catch (error: any) {
1838 | return this.createErrorResponse(
1839 | `Failed to export mesh library: ${error?.message || 'Unknown error'}`,
1840 | [
1841 | 'Ensure Godot is installed correctly',
1842 | 'Check if the GODOT_PATH environment variable is set correctly',
1843 | 'Verify the project path is accessible',
1844 | ]
1845 | );
1846 | }
1847 | }
1848 |
1849 | /**
1850 | * Handle the save_scene tool
1851 | */
1852 | private async handleSaveScene(args: any) {
1853 | // Normalize parameters to camelCase
1854 | args = this.normalizeParameters(args);
1855 |
1856 | if (!args.projectPath || !args.scenePath) {
1857 | return this.createErrorResponse(
1858 | 'Missing required parameters',
1859 | ['Provide projectPath and scenePath']
1860 | );
1861 | }
1862 |
1863 | if (!this.validatePath(args.projectPath) || !this.validatePath(args.scenePath)) {
1864 | return this.createErrorResponse(
1865 | 'Invalid path',
1866 | ['Provide valid paths without ".." or other potentially unsafe characters']
1867 | );
1868 | }
1869 |
1870 | // If newPath is provided, validate it
1871 | if (args.newPath && !this.validatePath(args.newPath)) {
1872 | return this.createErrorResponse(
1873 | 'Invalid new path',
1874 | ['Provide a valid new path without ".." or other potentially unsafe characters']
1875 | );
1876 | }
1877 |
1878 | try {
1879 | // Check if the project directory exists and contains a project.godot file
1880 | const projectFile = join(args.projectPath, 'project.godot');
1881 | if (!existsSync(projectFile)) {
1882 | return this.createErrorResponse(
1883 | `Not a valid Godot project: ${args.projectPath}`,
1884 | [
1885 | 'Ensure the path points to a directory containing a project.godot file',
1886 | 'Use list_projects to find valid Godot projects',
1887 | ]
1888 | );
1889 | }
1890 |
1891 | // Check if the scene file exists
1892 | const scenePath = join(args.projectPath, args.scenePath);
1893 | if (!existsSync(scenePath)) {
1894 | return this.createErrorResponse(
1895 | `Scene file does not exist: ${args.scenePath}`,
1896 | [
1897 | 'Ensure the scene path is correct',
1898 | 'Use create_scene to create a new scene first',
1899 | ]
1900 | );
1901 | }
1902 |
1903 | // Prepare parameters for the operation (already in camelCase)
1904 | const params: any = {
1905 | scenePath: args.scenePath,
1906 | };
1907 |
1908 | // Add optional parameters
1909 | if (args.newPath) {
1910 | params.newPath = args.newPath;
1911 | }
1912 |
1913 | // Execute the operation
1914 | const { stdout, stderr } = await this.executeOperation('save_scene', params, args.projectPath);
1915 |
1916 | if (stderr && stderr.includes('Failed to')) {
1917 | return this.createErrorResponse(
1918 | `Failed to save scene: ${stderr}`,
1919 | [
1920 | 'Check if the scene file is valid',
1921 | 'Ensure you have write permissions to the output path',
1922 | 'Verify the scene can be properly packed',
1923 | ]
1924 | );
1925 | }
1926 |
1927 | const savePath = args.newPath || args.scenePath;
1928 | return {
1929 | content: [
1930 | {
1931 | type: 'text',
1932 | text: `Scene saved successfully to: ${savePath}\n\nOutput: ${stdout}`,
1933 | },
1934 | ],
1935 | };
1936 | } catch (error: any) {
1937 | return this.createErrorResponse(
1938 | `Failed to save scene: ${error?.message || 'Unknown error'}`,
1939 | [
1940 | 'Ensure Godot is installed correctly',
1941 | 'Check if the GODOT_PATH environment variable is set correctly',
1942 | 'Verify the project path is accessible',
1943 | ]
1944 | );
1945 | }
1946 | }
1947 |
1948 | /**
1949 | * Handle the get_uid tool
1950 | */
1951 | private async handleGetUid(args: any) {
1952 | // Normalize parameters to camelCase
1953 | args = this.normalizeParameters(args);
1954 |
1955 | if (!args.projectPath || !args.filePath) {
1956 | return this.createErrorResponse(
1957 | 'Missing required parameters',
1958 | ['Provide projectPath and filePath']
1959 | );
1960 | }
1961 |
1962 | if (!this.validatePath(args.projectPath) || !this.validatePath(args.filePath)) {
1963 | return this.createErrorResponse(
1964 | 'Invalid path',
1965 | ['Provide valid paths without ".." or other potentially unsafe characters']
1966 | );
1967 | }
1968 |
1969 | try {
1970 | // Ensure godotPath is set
1971 | if (!this.godotPath) {
1972 | await this.detectGodotPath();
1973 | if (!this.godotPath) {
1974 | return this.createErrorResponse(
1975 | 'Could not find a valid Godot executable path',
1976 | [
1977 | 'Ensure Godot is installed correctly',
1978 | 'Set GODOT_PATH environment variable to specify the correct path',
1979 | ]
1980 | );
1981 | }
1982 | }
1983 |
1984 | // Check if the project directory exists and contains a project.godot file
1985 | const projectFile = join(args.projectPath, 'project.godot');
1986 | if (!existsSync(projectFile)) {
1987 | return this.createErrorResponse(
1988 | `Not a valid Godot project: ${args.projectPath}`,
1989 | [
1990 | 'Ensure the path points to a directory containing a project.godot file',
1991 | 'Use list_projects to find valid Godot projects',
1992 | ]
1993 | );
1994 | }
1995 |
1996 | // Check if the file exists
1997 | const filePath = join(args.projectPath, args.filePath);
1998 | if (!existsSync(filePath)) {
1999 | return this.createErrorResponse(
2000 | `File does not exist: ${args.filePath}`,
2001 | ['Ensure the file path is correct']
2002 | );
2003 | }
2004 |
2005 | // Get Godot version to check if UIDs are supported
2006 | const { stdout: versionOutput } = await execAsync(`"${this.godotPath}" --version`);
2007 | const version = versionOutput.trim();
2008 |
2009 | if (!this.isGodot44OrLater(version)) {
2010 | return this.createErrorResponse(
2011 | `UIDs are only supported in Godot 4.4 or later. Current version: ${version}`,
2012 | [
2013 | 'Upgrade to Godot 4.4 or later to use UIDs',
2014 | 'Use resource paths instead of UIDs for this version of Godot',
2015 | ]
2016 | );
2017 | }
2018 |
2019 | // Prepare parameters for the operation (already in camelCase)
2020 | const params = {
2021 | filePath: args.filePath,
2022 | };
2023 |
2024 | // Execute the operation
2025 | const { stdout, stderr } = await this.executeOperation('get_uid', params, args.projectPath);
2026 |
2027 | if (stderr && stderr.includes('Failed to')) {
2028 | return this.createErrorResponse(
2029 | `Failed to get UID: ${stderr}`,
2030 | [
2031 | 'Check if the file is a valid Godot resource',
2032 | 'Ensure the file path is correct',
2033 | ]
2034 | );
2035 | }
2036 |
2037 | return {
2038 | content: [
2039 | {
2040 | type: 'text',
2041 | text: `UID for ${args.filePath}: ${stdout.trim()}`,
2042 | },
2043 | ],
2044 | };
2045 | } catch (error: any) {
2046 | return this.createErrorResponse(
2047 | `Failed to get UID: ${error?.message || 'Unknown error'}`,
2048 | [
2049 | 'Ensure Godot is installed correctly',
2050 | 'Check if the GODOT_PATH environment variable is set correctly',
2051 | 'Verify the project path is accessible',
2052 | ]
2053 | );
2054 | }
2055 | }
2056 |
2057 | /**
2058 | * Handle the update_project_uids tool
2059 | */
2060 | private async handleUpdateProjectUids(args: any) {
2061 | // Normalize parameters to camelCase
2062 | args = this.normalizeParameters(args);
2063 |
2064 | if (!args.projectPath) {
2065 | return this.createErrorResponse(
2066 | 'Project path is required',
2067 | ['Provide a valid path to a Godot project directory']
2068 | );
2069 | }
2070 |
2071 | if (!this.validatePath(args.projectPath)) {
2072 | return this.createErrorResponse(
2073 | 'Invalid project path',
2074 | ['Provide a valid path without ".." or other potentially unsafe characters']
2075 | );
2076 | }
2077 |
2078 | try {
2079 | // Ensure godotPath is set
2080 | if (!this.godotPath) {
2081 | await this.detectGodotPath();
2082 | if (!this.godotPath) {
2083 | return this.createErrorResponse(
2084 | 'Could not find a valid Godot executable path',
2085 | [
2086 | 'Ensure Godot is installed correctly',
2087 | 'Set GODOT_PATH environment variable to specify the correct path',
2088 | ]
2089 | );
2090 | }
2091 | }
2092 |
2093 | // Check if the project directory exists and contains a project.godot file
2094 | const projectFile = join(args.projectPath, 'project.godot');
2095 | if (!existsSync(projectFile)) {
2096 | return this.createErrorResponse(
2097 | `Not a valid Godot project: ${args.projectPath}`,
2098 | [
2099 | 'Ensure the path points to a directory containing a project.godot file',
2100 | 'Use list_projects to find valid Godot projects',
2101 | ]
2102 | );
2103 | }
2104 |
2105 | // Get Godot version to check if UIDs are supported
2106 | const { stdout: versionOutput } = await execAsync(`"${this.godotPath}" --version`);
2107 | const version = versionOutput.trim();
2108 |
2109 | if (!this.isGodot44OrLater(version)) {
2110 | return this.createErrorResponse(
2111 | `UIDs are only supported in Godot 4.4 or later. Current version: ${version}`,
2112 | [
2113 | 'Upgrade to Godot 4.4 or later to use UIDs',
2114 | 'Use resource paths instead of UIDs for this version of Godot',
2115 | ]
2116 | );
2117 | }
2118 |
2119 | // Prepare parameters for the operation (already in camelCase)
2120 | const params = {
2121 | projectPath: args.projectPath,
2122 | };
2123 |
2124 | // Execute the operation
2125 | const { stdout, stderr } = await this.executeOperation('resave_resources', params, args.projectPath);
2126 |
2127 | if (stderr && stderr.includes('Failed to')) {
2128 | return this.createErrorResponse(
2129 | `Failed to update project UIDs: ${stderr}`,
2130 | [
2131 | 'Check if the project is valid',
2132 | 'Ensure you have write permissions to the project directory',
2133 | ]
2134 | );
2135 | }
2136 |
2137 | return {
2138 | content: [
2139 | {
2140 | type: 'text',
2141 | text: `Project UIDs updated successfully.\n\nOutput: ${stdout}`,
2142 | },
2143 | ],
2144 | };
2145 | } catch (error: any) {
2146 | return this.createErrorResponse(
2147 | `Failed to update project UIDs: ${error?.message || 'Unknown error'}`,
2148 | [
2149 | 'Ensure Godot is installed correctly',
2150 | 'Check if the GODOT_PATH environment variable is set correctly',
2151 | 'Verify the project path is accessible',
2152 | ]
2153 | );
2154 | }
2155 | }
2156 |
2157 | /**
2158 | * Run the MCP server
2159 | */
2160 | async run() {
2161 | try {
2162 | // Detect Godot path before starting the server
2163 | await this.detectGodotPath();
2164 |
2165 | if (!this.godotPath) {
2166 | console.error('[SERVER] Failed to find a valid Godot executable path');
2167 | console.error('[SERVER] Please set GODOT_PATH environment variable or provide a valid path');
2168 | process.exit(1);
2169 | }
2170 |
2171 | // Check if the path is valid
2172 | const isValid = await this.isValidGodotPath(this.godotPath);
2173 |
2174 | if (!isValid) {
2175 | if (this.strictPathValidation) {
2176 | // In strict mode, exit if the path is invalid
2177 | console.error(`[SERVER] Invalid Godot path: ${this.godotPath}`);
2178 | console.error('[SERVER] Please set a valid GODOT_PATH environment variable or provide a valid path');
2179 | process.exit(1);
2180 | } else {
2181 | // In compatibility mode, warn but continue with the default path
2182 | console.warn(`[SERVER] Warning: Using potentially invalid Godot path: ${this.godotPath}`);
2183 | console.warn('[SERVER] This may cause issues when executing Godot commands');
2184 | console.warn('[SERVER] This fallback behavior will be removed in a future version. Set strictPathValidation: true to opt-in to the new behavior.');
2185 | }
2186 | }
2187 |
2188 | console.log(`[SERVER] Using Godot at: ${this.godotPath}`);
2189 |
2190 | const transport = new StdioServerTransport();
2191 | await this.server.connect(transport);
2192 | console.error('Godot MCP server running on stdio');
2193 | } catch (error: unknown) {
2194 | const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2195 | console.error('[SERVER] Failed to start:', errorMessage);
2196 | process.exit(1);
2197 | }
2198 | }
2199 | }
2200 |
2201 | // Create and run the server
2202 | const server = new GodotServer();
2203 | server.run().catch((error: unknown) => {
2204 | const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2205 | console.error('Failed to run server:', errorMessage);
2206 | process.exit(1);
2207 | });
2208 |
```