diff --git a/.gitignore b/.gitignore index bd7fd82c1..16e1c514b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,8 @@ Thumbs.db # Coverage directory coverage/ + +# Claude Code +.claude/ +.subtask/ +CLAUDE.md diff --git a/package-lock.json b/package-lock.json index 3334689b4..c900fba8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "0.6.0", - "axios": "^1.7.9", "fs-extra": "^11.2.0" }, "bin": { @@ -42,23 +41,6 @@ "undici-types": "~6.19.2" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -68,31 +50,6 @@ "node": ">= 0.8" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -102,15 +59,6 @@ "node": ">= 0.6" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -120,101 +68,6 @@ "node": ">= 0.8" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -229,109 +82,12 @@ "node": ">=14.14" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -378,42 +134,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/raw-body": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", diff --git a/package.json b/package.json index 098ec76fb..222a38cc1 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "0.6.0", - "axios": "^1.7.9", "fs-extra": "^11.2.0" }, "devDependencies": { diff --git a/scripts/build.js b/scripts/build.js index bbb915425..29f2340de 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -13,14 +13,22 @@ fs.chmodSync(path.join(__dirname, '..', 'build', 'index.js'), '755'); try { // Ensure the build/scripts directory exists fs.ensureDirSync(path.join(__dirname, '..', 'build', 'scripts')); - + // Copy the godot_operations.gd file fs.copyFileSync( path.join(__dirname, '..', 'src', 'scripts', 'godot_operations.gd'), path.join(__dirname, '..', 'build', 'scripts', 'godot_operations.gd') ); - + console.log('Successfully copied godot_operations.gd to build/scripts'); + + // Copy the screenshot_manager.gd file + fs.copyFileSync( + path.join(__dirname, '..', 'src', 'scripts', 'screenshot_manager.gd'), + path.join(__dirname, '..', 'build', 'scripts', 'screenshot_manager.gd') + ); + + console.log('Successfully copied screenshot_manager.gd to build/scripts'); } catch (error) { console.error('Error copying scripts:', error); process.exit(1); diff --git a/src/index.ts b/src/index.ts index 23e13831a..fa033a852 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import { fileURLToPath } from 'url'; import { join, dirname, basename, normalize } from 'path'; -import { existsSync, readdirSync, mkdirSync } from 'fs'; +import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, copyFileSync } from 'fs'; import { spawn } from 'child_process'; import { promisify } from 'util'; import { exec } from 'child_process'; @@ -67,6 +67,7 @@ class GodotServer { private activeProcess: GodotProcess | null = null; private godotPath: string | null = null; private operationsScriptPath: string; + private screenshotManagerScriptPath: string; private validatedPaths: Map = new Map(); private strictPathValidation: boolean = false; @@ -136,6 +137,10 @@ class GodotServer { this.operationsScriptPath = join(__dirname, 'scripts', 'godot_operations.gd'); if (debugMode) console.error(`[DEBUG] Operations script path: ${this.operationsScriptPath}`); + // Set the path to the screenshot manager script + this.screenshotManagerScriptPath = join(__dirname, 'scripts', 'screenshot_manager.gd'); + if (debugMode) console.error(`[DEBUG] Screenshot manager script path: ${this.screenshotManagerScriptPath}`); + // Initialize the MCP server this.server = new Server( { @@ -923,6 +928,38 @@ class GodotServer { required: ['projectPath'], }, }, + { + name: 'take_screenshot', + description: 'Take a screenshot of the running Godot project window. Returns the screenshot as a base64-encoded image or saves it to a file. Requires the ScreenshotManager autoload to be installed (use setup_screenshot_manager first). Wait for the game to fully load before calling this tool, otherwise it may timeout.', + inputSchema: { + type: 'object', + properties: { + projectPath: { + type: 'string', + description: 'Path to the Godot project directory (used to determine save location if filePath not specified)', + }, + filePath: { + type: 'string', + description: 'Optional: Absolute path to save the screenshot. If not provided, returns base64 image data.', + }, + }, + required: [], + }, + }, + { + name: 'setup_screenshot_manager', + description: 'Install the ScreenshotManager autoload in a Godot project to enable screenshot capture. This copies the screenshot_manager.gd script to the project and configures it as an autoload.', + inputSchema: { + type: 'object', + properties: { + projectPath: { + type: 'string', + description: 'Path to the Godot project directory', + }, + }, + required: ['projectPath'], + }, + }, ], })); @@ -958,6 +995,10 @@ class GodotServer { return await this.handleGetUid(request.params.arguments); case 'update_project_uids': return await this.handleUpdateProjectUids(request.params.arguments); + case 'take_screenshot': + return await this.handleTakeScreenshot(request.params.arguments); + case 'setup_screenshot_manager': + return await this.handleSetupScreenshotManager(request.params.arguments); default: throw new McpError( ErrorCode.MethodNotFound, @@ -2153,6 +2194,262 @@ class GodotServer { } } + /** + * Handle the take_screenshot tool + * Takes a screenshot using Godot's native viewport capture + * Requires the ScreenshotManager autoload to be present in the project + */ + private async handleTakeScreenshot(args: any) { + // Normalize parameters to camelCase + args = this.normalizeParameters(args); + + try { + this.logDebug('Taking screenshot using Godot native capture...'); + + // Check if a project is running + if (!this.activeProcess) { + return this.createErrorResponse( + 'No Godot project is currently running', + [ + 'Start a project first using run_project', + 'The project must have the ScreenshotManager autoload installed', + ] + ); + } + + // Get project name from project.godot to find user:// directory + let projectName = 'Unknown Project'; + const projectPath = args.projectPath || process.cwd(); + const projectGodotPath = join(projectPath, 'project.godot'); + + if (existsSync(projectGodotPath)) { + const projectContent = readFileSync(projectGodotPath, 'utf-8'); + const nameMatch = projectContent.match(/config\/name="([^"]+)"/); + if (nameMatch) { + projectName = nameMatch[1]; + } + } + + // Determine user:// directory based on OS + let userDataDir: string; + if (process.platform === 'win32') { + userDataDir = join(process.env.APPDATA || '', 'Godot', 'app_userdata', projectName); + } else if (process.platform === 'darwin') { + userDataDir = join(process.env.HOME || '', 'Library', 'Application Support', 'Godot', 'app_userdata', projectName); + } else { + userDataDir = join(process.env.HOME || '', '.local', 'share', 'godot', 'app_userdata', projectName); + } + + this.logDebug(`User data directory: ${userDataDir}`); + + // Ensure user data directory exists + if (!existsSync(userDataDir)) { + mkdirSync(userDataDir, { recursive: true }); + } + + const requestFile = join(userDataDir, 'mcp_screenshot_request.txt'); + const outputFile = join(userDataDir, 'mcp_screenshot.png'); + + // Remove any existing screenshot + if (existsSync(outputFile)) { + unlinkSync(outputFile); + } + + // Create the screenshot request file + writeFileSync(requestFile, 'take_screenshot'); + this.logDebug(`Screenshot request created: ${requestFile}`); + + // Wait for the screenshot to be taken (poll for up to 5 seconds) + const maxWaitTime = 5000; + const pollInterval = 100; + let waited = 0; + + while (waited < maxWaitTime) { + await new Promise(resolve => setTimeout(resolve, pollInterval)); + waited += pollInterval; + + if (existsSync(outputFile)) { + this.logDebug('Screenshot file found!'); + break; + } + } + + if (!existsSync(outputFile)) { + return this.createErrorResponse( + 'Screenshot request timed out', + [ + 'Make sure the ScreenshotManager autoload is installed in your project', + 'Run the setup_screenshot_manager tool first to install it automatically', + 'Or manually add to your project.godot under [autoload]:', + 'ScreenshotManager="*res://addons/godot_mcp/screenshot_manager.gd"', + 'The project must be running for screenshots to work', + ] + ); + } + + // Read the screenshot + const imgBuffer = readFileSync(outputFile); + this.logDebug(`Screenshot read: ${imgBuffer.length} bytes`); + + // If a specific file path is requested, copy there + if (args.filePath) { + const savePath = normalize(args.filePath); + writeFileSync(savePath, imgBuffer); + this.logDebug(`Screenshot copied to: ${savePath}`); + + return { + content: [ + { + type: 'text', + text: `Screenshot saved successfully to: ${savePath}`, + }, + ], + }; + } + + // Return as base64 image + const base64Image = imgBuffer.toString('base64'); + + return { + content: [ + { + type: 'image', + data: base64Image, + mimeType: 'image/png', + }, + ], + }; + } catch (error: any) { + return this.createErrorResponse( + `Failed to take screenshot: ${error?.message || 'Unknown error'}`, + [ + 'Ensure a Godot project is running (use run_project first)', + 'Make sure the ScreenshotManager autoload is installed (use setup_screenshot_manager tool)', + 'Check if the project has proper write permissions', + ] + ); + } + } + + /** + * Handle the setup_screenshot_manager tool + * Installs the ScreenshotManager autoload script into a Godot project + */ + private async handleSetupScreenshotManager(args: any) { + // Normalize parameters to camelCase + args = this.normalizeParameters(args); + + if (!args.projectPath) { + return this.createErrorResponse( + 'Project path is required', + ['Provide a valid path to a Godot project directory'] + ); + } + + if (!this.validatePath(args.projectPath)) { + return this.createErrorResponse( + 'Invalid project path', + ['Provide a valid path without ".." or other potentially unsafe characters'] + ); + } + + try { + // Check if the project directory exists and contains a project.godot file + const projectGodotPath = join(args.projectPath, 'project.godot'); + if (!existsSync(projectGodotPath)) { + 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 source script exists + if (!existsSync(this.screenshotManagerScriptPath)) { + return this.createErrorResponse( + 'Screenshot manager script not found in MCP server bundle', + [ + 'Reinstall the godot-mcp package', + 'Ensure the build process completed successfully', + ] + ); + } + + // Create the target directory: res://addons/godot_mcp/ + const addonsDir = join(args.projectPath, 'addons'); + const godotMcpDir = join(addonsDir, 'godot_mcp'); + + if (!existsSync(addonsDir)) { + mkdirSync(addonsDir); + this.logDebug(`Created addons directory: ${addonsDir}`); + } + + if (!existsSync(godotMcpDir)) { + mkdirSync(godotMcpDir); + this.logDebug(`Created godot_mcp directory: ${godotMcpDir}`); + } + + // Copy the screenshot manager script + const targetScriptPath = join(godotMcpDir, 'screenshot_manager.gd'); + copyFileSync(this.screenshotManagerScriptPath, targetScriptPath); + this.logDebug(`Copied screenshot_manager.gd to: ${targetScriptPath}`); + + // Update project.godot to add the autoload + let projectContent = readFileSync(projectGodotPath, 'utf-8'); + const autoloadEntry = 'ScreenshotManager="*res://addons/godot_mcp/screenshot_manager.gd"'; + + // Check if autoload section exists + if (projectContent.includes('[autoload]')) { + // Check if ScreenshotManager is already configured + if (projectContent.includes('ScreenshotManager=')) { + // Update existing entry + projectContent = projectContent.replace( + /ScreenshotManager="[^"]*"/, + autoloadEntry + ); + this.logDebug('Updated existing ScreenshotManager autoload entry'); + } else { + // Add new entry after [autoload] section header + projectContent = projectContent.replace( + '[autoload]', + `[autoload]\n\n${autoloadEntry}` + ); + this.logDebug('Added ScreenshotManager to existing [autoload] section'); + } + } else { + // Add new autoload section at the end of the file + projectContent += `\n[autoload]\n\n${autoloadEntry}\n`; + this.logDebug('Created new [autoload] section with ScreenshotManager'); + } + + // Write the updated project.godot + writeFileSync(projectGodotPath, projectContent); + this.logDebug('Updated project.godot with ScreenshotManager autoload'); + + return { + content: [ + { + type: 'text', + text: `ScreenshotManager installed successfully!\n\n` + + `Script copied to: res://addons/godot_mcp/screenshot_manager.gd\n` + + `Autoload configured in project.godot\n\n` + + `Note: If the project is currently running in the editor, you may need to reload it for the autoload to take effect.`, + }, + ], + }; + } catch (error: any) { + return this.createErrorResponse( + `Failed to setup screenshot manager: ${error?.message || 'Unknown error'}`, + [ + 'Ensure you have write permissions to the project directory', + 'Check if the project.godot file is not locked by another process', + ] + ); + } + } + /** * Run the MCP server */ diff --git a/src/scripts/screenshot_manager.gd b/src/scripts/screenshot_manager.gd new file mode 100644 index 000000000..b3de77e5f --- /dev/null +++ b/src/scripts/screenshot_manager.gd @@ -0,0 +1,47 @@ +extends Node +## ScreenshotManager - MCP Screenshot Capture Autoload +## +## This autoload script enables the Godot MCP server to capture screenshots +## of the running game. It polls for screenshot requests via a file-based IPC +## mechanism and captures the viewport when requested. +## +## Installation: +## 1. Copy this script to your project (e.g., res://addons/godot_mcp/screenshot_manager.gd) +## 2. Add to Project Settings > Autoload with name "ScreenshotManager" and enable it +## Or add to project.godot: ScreenshotManager="*res://addons/godot_mcp/screenshot_manager.gd" + +var check_interval := 0.1 # Check every 100ms +var timer := 0.0 + + +func _process(delta: float) -> void: + timer += delta + if timer >= check_interval: + timer = 0.0 + _check_for_screenshot_request() + + +func _check_for_screenshot_request() -> void: + var request_path := "user://mcp_screenshot_request.txt" + if FileAccess.file_exists(request_path): + _take_screenshot() + # Delete the request file after processing + var global_path := ProjectSettings.globalize_path(request_path) + DirAccess.remove_absolute(global_path) + + +func _take_screenshot() -> void: + # Wait for the frame to be fully rendered + await RenderingServer.frame_post_draw + + # Capture the viewport + var img := get_viewport().get_texture().get_image() + + # Save to the user:// directory where MCP server expects it + var output_path := "user://mcp_screenshot.png" + var error := img.save_png(output_path) + + if error != OK: + push_error("ScreenshotManager: Failed to save screenshot, error code: %d" % error) + else: + print("ScreenshotManager: Screenshot saved to %s" % ProjectSettings.globalize_path(output_path))