This is page 1 of 3. Use http://codebase.md/zabaglione/mcp-server-unity?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .claude │ └── settings.local.json ├── .gitignore ├── build-bundle.js ├── build-final-dxt.sh ├── BUILD.md ├── CHANGELOG.md ├── CLAUDE.md ├── CONTRIBUTING.md ├── create-bundled-dxt.sh ├── docs │ ├── API.md │ └── ARCHITECTURE.md ├── generate-embedded-scripts.cjs ├── LICENSE ├── manifest.json ├── package-lock.json ├── package.json ├── README-ja.md ├── README.md ├── src │ ├── adapters │ │ └── unity-http-adapter.ts │ ├── embedded-scripts.ts │ ├── services │ │ └── unity-bridge-deploy-service.ts │ ├── simple-index.ts │ ├── tools │ │ └── unity-mcp-tools.ts │ └── unity-scripts │ ├── UnityHttpServer.cs │ └── UnityMCPServerWindow.cs ├── TECHNICAL.md ├── tests │ ├── integration │ │ └── simple-integration.test.ts │ ├── unit │ │ ├── adapters │ │ │ └── unity-http-adapter.test.ts │ │ ├── templates │ │ │ └── shaders │ │ │ └── shader-templates.test.ts │ │ └── tools │ │ └── unity-mcp-tools.test.ts │ └── unity │ └── UnityHttpServerTests.cs ├── tsconfig.json ├── tsconfig.test.json ├── unity-mcp-server.bundle.js └── vitest.config.ts ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Node.js 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build output 8 | build/ 9 | dist/ 10 | 11 | # IDE 12 | .idea/ 13 | .vscode/ 14 | *.swp 15 | *.swo 16 | .DS_Store 17 | 18 | # Environment 19 | .env 20 | .env.local 21 | .env.*.local 22 | 23 | # Logs 24 | logs/ 25 | *.log 26 | 27 | # OS files 28 | Thumbs.db 29 | Desktop.ini 30 | 31 | # fot AI 32 | .github/ 33 | 34 | # Extension build artifacts 35 | extension-package/ 36 | *.dxt 37 | 38 | # Test output directories 39 | test-*-output/ 40 | 41 | # Legacy scripts 42 | install-*.js 43 | reinstall-*.js 44 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Unity MCP Server 2 | 3 | Unity MCP Server lets Claude work with your Unity projects! Create scripts, manage shaders, organize folders - all through natural conversation with Claude. 4 | 5 | [日本語版 README はこちら](README-ja.md) | [English](README.md) 6 | 7 | ## 🎮 What Can You Do? 8 | 9 | Talk to Claude to: 10 | - **Create Unity Scripts**: "Create a PlayerController script with jump functionality" 11 | - **Manage Shaders**: "Create a toon shader for my character" 12 | - **Organize Projects**: "Create a folder structure for my RPG game" 13 | - **Get Project Info**: "What render pipeline is my project using?" 14 | 15 | ## 🚀 Quick Start (Recommended: Claude Desktop Extension) 16 | 17 | ### Option 1: Install via Claude Desktop Extension (Easiest) 18 | 19 | 1. **Download the Extension** 20 | - Go to [Latest Release](https://github.com/zabaglione/mcp-server-unity/releases/latest) 21 | - Download `unity-mcp-server.dxt` (42KB) 22 | 23 | 2. **Install in Claude Desktop** 24 | - Open Claude Desktop 25 | - Go to Extensions 26 | - Click "Install from file" 27 | - Select the downloaded `unity-mcp-server.dxt` 28 | 29 | 3. **Start Using!** 30 | - Open any Unity project (2019.4 or newer) 31 | - Install Newtonsoft JSON package in Unity: 32 | - Open Window → Package Manager 33 | - Click the "+" button and select "Add package by name..." 34 | - Enter: `com.unity.nuget.newtonsoft-json` 35 | - Click "Add" 36 | - Ask Claude: "Setup Unity MCP in my project at /path/to/project" 37 | - Claude will install everything automatically! 38 | 39 | ### Option 2: Manual Installation (For developers) 40 | 41 | <details> 42 | <summary>Click to see manual installation steps</summary> 43 | 44 | 1. Clone and build: 45 | ```bash 46 | git clone https://github.com/zabaglione/mcp-server-unity.git 47 | cd mcp-server-unity 48 | npm install 49 | npm run build 50 | ``` 51 | 52 | 2. Configure Claude Desktop: 53 | ```json 54 | { 55 | "mcpServers": { 56 | "unity": { 57 | "command": "node", 58 | "args": ["/path/to/mcp-server-unity/build/simple-index.js"] 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | </details> 65 | 66 | ## 📝 How to Use 67 | 68 | Once installed, just talk to Claude naturally: 69 | 70 | ### Creating Scripts 71 | ``` 72 | You: "Create a PlayerHealth script that handles damage and healing" 73 | Claude: I'll create a PlayerHealth script for you... 74 | ``` 75 | 76 | ### Creating Shaders 77 | ``` 78 | You: "I need a water shader with wave animation" 79 | Claude: I'll create a water shader with wave animation... 80 | ``` 81 | 82 | ### Organizing Your Project 83 | ``` 84 | You: "Set up a folder structure for a platformer game" 85 | Claude: I'll create an organized folder structure for your platformer... 86 | ``` 87 | 88 | ### Checking Project Info 89 | ``` 90 | You: "What Unity version and render pipeline am I using?" 91 | Claude: Let me check your project information... 92 | ``` 93 | 94 | ## 🎯 Features 95 | 96 | - ✅ **Smart Script Creation** - Claude understands Unity patterns and creates proper MonoBehaviours 97 | - ✅ **Shader Support** - Works with Built-in, URP, and HDRP render pipelines 98 | - ✅ **Project Organization** - Create, move, and rename folders to keep projects tidy 99 | - ✅ **Auto Setup** - Claude automatically sets up the Unity integration when needed 100 | - ✅ **Safe Operations** - All changes are made safely with proper Unity asset handling 101 | 102 | ## 🛠️ Troubleshooting 103 | 104 | ### "Unity server not responding" 105 | 1. Make sure Unity Editor is open 106 | 2. Check Window → Unity MCP Server in Unity 107 | 3. Click "Start Server" if it's not running 108 | 109 | ### "Can't find my project" 110 | - Tell Claude the exact path: "My Unity project is at C:/Projects/MyGame" 111 | - Make sure it's a valid Unity project with an Assets folder 112 | 113 | ### Need Help? 114 | - Ask Claude: "Help me troubleshoot Unity MCP" 115 | - Check [Issues](https://github.com/zabaglione/mcp-server-unity/issues) 116 | - See [Technical Documentation](TECHNICAL.md) for advanced details 117 | 118 | ## 🎮 Unity Version Support 119 | 120 | - **Unity 2019.4+** - Full support 121 | - **Unity 6 (6000.0+)** - Recommended for best experience 122 | - Works on Windows, macOS, and Linux 123 | 124 | ## 📈 Latest Updates (v3.1.1) 125 | 126 | - ✅ Fixed render pipeline detection (now correctly identifies Built-in, URP, HDRP) 127 | - ✅ Resolved AssetDatabase synchronization errors 128 | - ✅ Improved file management and Unity integration stability 129 | 130 | ## 🤝 Contributing 131 | 132 | Want to help improve Unity MCP Server? Check out our [Contributing Guide](CONTRIBUTING.md)! 133 | 134 | ## 📝 License 135 | 136 | MIT License - see [LICENSE](LICENSE) 137 | 138 | ## 🙏 Acknowledgments 139 | 140 | - [Anthropic](https://anthropic.com) for Claude and MCP 141 | - [Unity Technologies](https://unity.com) for the amazing game engine 142 | - All our contributors and users! 143 | 144 | --- 145 | 146 | **Ready to supercharge your Unity development with Claude?** [Download the extension now!](https://github.com/zabaglione/mcp-server-unity/releases/latest) ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown 1 | # Contributing to Unity MCP Server 2 | 3 | Thank you for your interest in contributing to Unity MCP Server! This document provides guidelines and instructions for contributing. 4 | 5 | ## Development Setup 6 | 7 | ### Prerequisites 8 | 9 | 1. Node.js 18.x or higher 10 | 2. npm or yarn 11 | 3. Git 12 | 4. A Unity installation (for testing) 13 | 5. Claude Desktop (for integration testing) 14 | 15 | ### Setting Up the Development Environment 16 | 17 | 1. Fork and clone the repository: 18 | ```bash 19 | git clone https://github.com/zabaglione/mcp-server-unity.git 20 | cd mcp-server-unity 21 | ``` 22 | 23 | 2. Install dependencies: 24 | ```bash 25 | npm install 26 | ``` 27 | 28 | 3. Build the project: 29 | ```bash 30 | npm run build 31 | ``` 32 | 33 | 4. For development with auto-rebuild: 34 | ```bash 35 | npm run dev 36 | ``` 37 | 38 | ## Code Style Guidelines 39 | 40 | ### TypeScript Conventions 41 | 42 | - Use strict TypeScript settings (already configured in tsconfig.json) 43 | - Always specify types explicitly for function parameters and return values 44 | - Use interfaces for complex object types 45 | - Prefer `const` over `let` when variables won't be reassigned 46 | 47 | ### Code Organization 48 | 49 | - Keep related functionality together 50 | - Each tool should have clear error handling 51 | - Use descriptive variable and function names 52 | - Add JSDoc comments for complex functions 53 | 54 | ### Error Handling 55 | 56 | - Always validate inputs before processing 57 | - Provide clear, actionable error messages 58 | - Use MCP's error types appropriately 59 | - Never expose sensitive file paths in error messages 60 | 61 | ## Testing 62 | 63 | ### Manual Testing 64 | 65 | 1. Configure Claude Desktop to use your development build 66 | 2. Test each tool with various inputs: 67 | - Valid inputs 68 | - Invalid inputs 69 | - Edge cases 70 | - Missing optional parameters 71 | 72 | ### Testing Checklist 73 | 74 | - [ ] Set Unity project with valid path 75 | - [ ] Set Unity project with invalid path 76 | - [ ] Create scripts with various content 77 | - [ ] Read existing and non-existing scripts 78 | - [ ] List scripts in projects with 0, 1, and many scripts 79 | - [ ] Create scenes and materials 80 | - [ ] Test all asset type filters 81 | - [ ] Test build command (if Unity is available) 82 | 83 | ## Pull Request Process 84 | 85 | ### Before Submitting 86 | 87 | 1. Ensure your code builds without errors: 88 | ```bash 89 | npm run build 90 | ``` 91 | 92 | 2. Test your changes thoroughly 93 | 3. Update documentation if needed 94 | 4. Add yourself to the contributors list (if first contribution) 95 | 96 | ### PR Guidelines 97 | 98 | 1. **Title**: Use a clear, descriptive title 99 | - Good: "Add support for prefab creation" 100 | - Bad: "Update code" 101 | 102 | 2. **Description**: Include: 103 | - What changes were made 104 | - Why the changes were necessary 105 | - Any breaking changes 106 | - Testing performed 107 | 108 | 3. **Scope**: Keep PRs focused 109 | - One feature or fix per PR 110 | - Separate refactoring from feature additions 111 | 112 | ### Review Process 113 | 114 | - PRs require at least one review 115 | - Address all feedback constructively 116 | - Update based on feedback and re-request review 117 | - Squash commits if requested 118 | 119 | ## Adding New Tools 120 | 121 | When adding a new Unity-related tool: 122 | 123 | 1. Add tool definition in `setupHandlers()` 124 | 2. Implement the tool handler method 125 | 3. Add appropriate error handling 126 | 4. Update README.md with usage examples 127 | 5. Test thoroughly with Claude Desktop 128 | 129 | ### Tool Implementation Template 130 | 131 | ```typescript 132 | private async toolName(param1: string, param2?: string): Promise<any> { 133 | // Validate Unity project is set 134 | if (!this.unityProject) { 135 | throw new Error('Unity project not set. Use set_unity_project first.'); 136 | } 137 | 138 | // Validate inputs 139 | if (!param1) { 140 | throw new Error('param1 is required'); 141 | } 142 | 143 | try { 144 | // Tool implementation 145 | 146 | return { 147 | content: [ 148 | { 149 | type: 'text', 150 | text: `Success message with details`, 151 | }, 152 | ], 153 | }; 154 | } catch (error) { 155 | throw new Error(`Tool operation failed: ${error}`); 156 | } 157 | } 158 | ``` 159 | 160 | ## Reporting Issues 161 | 162 | ### Bug Reports 163 | 164 | Include: 165 | - Unity version 166 | - Node.js version 167 | - OS and version 168 | - Steps to reproduce 169 | - Expected vs actual behavior 170 | - Error messages/logs 171 | 172 | ### Feature Requests 173 | 174 | Include: 175 | - Use case description 176 | - Proposed implementation (if any) 177 | - Unity version compatibility requirements 178 | - Potential impact on existing features 179 | 180 | ## Community 181 | 182 | - Be respectful and constructive 183 | - Help others when possible 184 | - Share your Unity MCP use cases 185 | - Suggest improvements 186 | 187 | ## Release Process 188 | 189 | 1. Update version in package.json 190 | 2. Update CHANGELOG.md 191 | 3. Create release PR 192 | 4. After merge, tag release 193 | 5. Publish release notes 194 | 195 | Thank you for contributing to Unity MCP Server! ``` -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- ```markdown 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | # Unity MCP Server - Project Knowledge Base 6 | 7 | ## Project Overview 8 | Unity MCP Server is a Model Context Protocol (MCP) server that bridges AI assistants (like Claude) with Unity game development. It supports both legacy file-based operations (v2.x) and direct Unity Editor integration (v3.0.0 for Unity 6000+). 9 | 10 | ## Commands 11 | 12 | ### Build and Development 13 | - `npm run build` - Compile TypeScript to JavaScript 14 | - `npm run dev` - Watch mode for development 15 | - `npm start` - Start MCP server (stdio mode for Claude Desktop) 16 | - `npm run start:http` - Start HTTP API server (default port 3000) 17 | - `npm run clean` - Clean build artifacts 18 | 19 | ### Optimized Mode 20 | - `npm run start:optimized` - MCP server with streaming for large files 21 | - `npm run start:http:optimized` - HTTP server with streaming 22 | 23 | ### Testing 24 | - `npm test` - Run all tests 25 | - `npm run test:unit` - Unit tests only 26 | - `npm run test:integration` - Integration tests only 27 | - `npm run test:e2e` - End-to-end tests 28 | - `npm run test:coverage` - Generate coverage report 29 | - `npm run test:manual` - Interactive manual test runner 30 | - `npm run test:performance` - Run performance benchmarks 31 | - `npm run test:watch` - Watch mode for tests 32 | - `npm run test:legacy` - Old integration test 33 | - `npm run test:direct` - Direct TypeScript test runner 34 | 35 | ### No Linting/Formatting 36 | - No ESLint or Prettier configured - maintain existing code style 37 | 38 | ## Architecture Evolution 39 | 40 | ### v3.0.0 - Unity Bridge Architecture (Unity 6000+ only) 41 | - **Direct Unity API Integration**: No file system manipulation 42 | - **Unity Bridge Client**: Named Pipes/Domain Sockets communication 43 | - **Simplified API**: Focus on script and folder operations only 44 | - **Real-time Events**: Connection, compilation, project changes 45 | - **Entry Points**: `src/index.ts`, `src/unity6-mcp-server.ts` 46 | - **Unity Script**: `src/unity-scripts/MCPBridge.cs` (place in Unity project) 47 | 48 | ### v2.x - Service-Based Architecture (Legacy, Unity 2019.4+) 49 | All services extend from `BaseService` and follow a consistent pattern: 50 | - **ProjectService**: Unity project validation and setup 51 | - **ScriptService**: C# script creation and management 52 | - **AssetService**: Unity asset reading and listing 53 | - **BuildService**: Multi-platform build automation 54 | - **ShaderService**: Shader creation for Built-in/URP/HDRP 55 | - **MaterialService**: Material creation and property management 56 | - **EditorScriptService**: Editor extensions (windows, inspectors) 57 | - **CodeAnalysisService**: Code diff, namespace management, duplicate detection 58 | - **CompilationService**: Real-time compilation error monitoring 59 | - **UnityRefreshService**: Asset database refresh with batch operations 60 | - **UnityDiagnosticsService**: Editor log analysis and error tracking 61 | - **UIToolkitService**: UXML/USS file creation and management 62 | 63 | ### Key Design Patterns 64 | 65 | 1. **Service Container Pattern** (v2.x) 66 | - All services registered in `ServicesContainer` 67 | - Dependency injection for service dependencies 68 | - Factory pattern for service instantiation 69 | 70 | 2. **Template-based Code Generation** 71 | - Templates in `src/templates/` for all generated code 72 | - Supports shader variants (Built-in, URP, HDRP) 73 | - Namespace auto-detection based on file location 74 | 75 | 3. **Meta File Management** (v2.x critical) 76 | - Automatic .meta file generation with consistent GUIDs 77 | - GUID preservation for shader/material updates 78 | - Prevents Unity reference breakage 79 | 80 | 4. **Render Pipeline Detection** 81 | - Auto-detects Built-in, URP, or HDRP from project packages 82 | - Adjusts shader/material creation accordingly 83 | 84 | ## Development Guidelines 85 | 86 | ### Error Handling 87 | - All services use custom error types from `src/errors/` 88 | - Detailed error messages with actionable suggestions 89 | - Validation before operations (project path, Unity version) 90 | 91 | ### File Operations 92 | - Always use absolute paths 93 | - Create parent directories automatically 94 | - Generate .meta files for all Unity assets (v2.x) 95 | - Respect Unity's folder structure conventions 96 | 97 | ### Code Style 98 | - TypeScript with strict type checking 99 | - ES modules with .js extensions in imports 100 | - Async/await for all I/O operations 101 | - Comprehensive logging with context 102 | 103 | ### Testing 104 | - Jest framework with TypeScript support 105 | - Virtual Unity project utility for test environments (`tests/utils/virtualUnityProject.ts`) 106 | - Snapshot testing for generated content validation 107 | - Performance benchmarks exported to JSON 108 | - Coverage thresholds: 80% lines, 70% branches/functions 109 | - Test structure mirrors source structure (e.g., `src/services/foo.ts` → `tests/unit/services/foo.test.ts`) 110 | 111 | ## Unity Integration Points 112 | 113 | ### Project Structure Expected 114 | ``` 115 | UnityProject/ 116 | ├── Assets/ 117 | │ ├── Scripts/ 118 | │ ├── Materials/ 119 | │ ├── Shaders/ 120 | │ └── Editor/ 121 | │ └── MCP/ 122 | │ └── MCPBridge.cs # v3.0 Unity Bridge script 123 | ├── Packages/ 124 | │ └── manifest.json (render pipeline detection) 125 | ├── Library/ 126 | │ ├── Bee/fullprofile.json (compilation errors) 127 | │ └── Logs/ (Unity console logs) 128 | └── ProjectSettings/ 129 | ``` 130 | 131 | ### Compilation Monitoring 132 | - Watches `Library/Bee/fullprofile.json` for errors 133 | - Parses Unity console logs from `Library/Logs/` 134 | - Real-time feedback on script compilation 135 | 136 | ### Asset Database Refresh (v2.x) 137 | - Triggers Unity refresh via EditorApplication 138 | - Supports batch operations to minimize refreshes 139 | - Handles both immediate and deferred refresh modes 140 | 141 | ## Common Workflows 142 | 143 | ### v3.0 Unity Bridge Workflow 144 | 1. Ensure Unity 6000+ with MCPBridge.cs installed 145 | 2. Unity automatically starts bridge on project open 146 | 3. Use script/folder operations via MCP tools 147 | 4. Bridge handles all Unity API calls directly 148 | 149 | ### Material Shader Updates (v2.x) 150 | 1. Read current material properties 151 | 2. Find target shader GUID 152 | 3. Update material preserving properties 153 | 4. Maintain material GUID for references 154 | 155 | ### Script Creation 156 | 1. Detect namespace from file path 157 | 2. Apply project conventions 158 | 3. Generate with proper using statements 159 | 4. Create accompanying .meta file (v2.x only) 160 | 161 | ### Build Automation (v2.x) 162 | 1. Validate project and target platform 163 | 2. Configure build settings 164 | 3. Execute build with error handling 165 | 4. Report build results and logs 166 | 167 | ## Performance Considerations 168 | - Batch operations for multiple files 169 | - Minimize Unity refreshes 170 | - Cache render pipeline detection 171 | - Efficient file system operations 172 | 173 | ## Security Notes 174 | - Path validation to prevent directory traversal 175 | - No execution of arbitrary Unity code 176 | - Safe template rendering 177 | - Input sanitization for all operations 178 | 179 | ## Critical Implementation Details 180 | 181 | ### Unity Asset Refresh (v2.x) 182 | - **CRITICAL**: Always trigger Unity refresh after file operations using `UnityRefreshService` 183 | - Unity won't recognize new/modified assets without refresh 184 | - Batch operations supported to minimize refresh calls 185 | - Both immediate and deferred refresh modes available 186 | 187 | ### Meta File Generation (v2.x) 188 | - Every Unity asset MUST have a corresponding .meta file 189 | - GUIDs must be consistent to prevent reference breakage 190 | - When updating shaders/materials, preserve existing GUIDs 191 | - Meta files generated automatically by all asset creation services 192 | 193 | ### Service Dependencies (v2.x) 194 | - Services can depend on each other (inject via constructor) 195 | - Example: `MaterialService` depends on `ShaderService` 196 | - All services registered in `ServicesContainer` 197 | - Use `ServiceFactory` to create properly wired instances 198 | 199 | ### Template System 200 | - All code generation uses templates from `src/templates/` 201 | - Templates support placeholders: `{{NAMESPACE}}`, `{{CLASS_NAME}}`, etc. 202 | - Shader templates vary by render pipeline (builtin/urp/hdrp) 203 | - UI Toolkit templates for windows, documents, and components 204 | 205 | ### Large File Support 206 | - Automatic streaming for files larger than 10MB 207 | - Maximum file size limit: 1GB 208 | - Services automatically use streaming for read/write operations 209 | - HTTP API supports up to 1GB request bodies 210 | - Implemented in: ScriptService, ShaderService, MaterialService, UIToolkitService 211 | 212 | ### Unity Bridge Protocol (v3.0) 213 | - JSON-RPC style messages over Named Pipes/Domain Sockets 214 | - Event types: connection, compilation, projectChanged 215 | - Request timeout: 6 minutes (configurable) 216 | - Automatic reconnection on Unity restart 217 | - Unity-side implementation in MCPBridge.cs 218 | 219 | ## Environment Variables 220 | - `UNITY_MCP_LOG_LEVEL`: Logging level (debug, info, warn, error) 221 | - `UNITY_MCP_TIMEOUT`: Request timeout in milliseconds 222 | - `USE_OPTIMIZED_SERVICES`: Enable streaming for large files 223 | - `PORT`: HTTP server port (default 3000) 224 | 225 | ## Version Compatibility 226 | - **v2.x**: Unity 2019.4+, full service coverage, file-based operations 227 | - **v3.0**: Unity 6000+ only, simplified API, direct Unity integration ``` -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "allowImportingTsExtensions": false, 7 | "noEmit": true 8 | }, 9 | "include": ["tests/**/*", "src/**/*"], 10 | "ts-node": { 11 | "esm": true, 12 | "experimentalSpecifierResolution": "node" 13 | } 14 | } ``` -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'node', 7 | coverage: { 8 | provider: 'v8', 9 | reporter: ['text', 'json', 'html'], 10 | exclude: [ 11 | 'node_modules/', 12 | 'tests/', 13 | 'build/', 14 | '**/*.test.ts', 15 | '**/*.config.ts' 16 | ] 17 | } 18 | }, 19 | resolve: { 20 | extensions: ['.ts', '.js', '.json'] 21 | } 22 | }); ``` -------------------------------------------------------------------------------- /create-bundled-dxt.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Clean up 4 | rm -rf dxt-bundled 5 | mkdir -p dxt-bundled 6 | 7 | # Copy manifest.json and modify entry point 8 | cp manifest.json dxt-bundled/ 9 | sed -i '' 's|"entry_point": "server/simple-index.js"|"entry_point": "unity-mcp-server.bundle.js"|g' dxt-bundled/manifest.json 10 | sed -i '' 's|"args": \["${__dirname}/server/simple-index.js"\]|"args": ["${__dirname}/unity-mcp-server.bundle.js"]|g' dxt-bundled/manifest.json 11 | 12 | # Copy bundle 13 | cp unity-mcp-server.bundle.js dxt-bundled/ 14 | 15 | # Create ZIP file 16 | cd dxt-bundled 17 | zip -r ../unity-mcp-server.dxt * -x "*.DS_Store" -x "__MACOSX/*" 18 | cd .. 19 | 20 | # Clean up 21 | rm -rf dxt-bundled 22 | 23 | echo "Created unity-mcp-server.dxt" ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "lib": ["ES2020"], 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": false, 15 | "outDir": "./build", 16 | "rootDir": "./src", 17 | "declaration": true, 18 | "declarationMap": true, 19 | "sourceMap": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noImplicitReturns": true, 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": ["src/**/*"], 26 | "exclude": ["node_modules", "build", "dist"] 27 | } ``` -------------------------------------------------------------------------------- /build-bundle.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | import * as esbuild from 'esbuild'; 4 | import { readFileSync, writeFileSync } from 'fs'; 5 | import { fileURLToPath } from 'url'; 6 | import { dirname, join } from 'path'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = dirname(__filename); 10 | 11 | // Build the bundle 12 | await esbuild.build({ 13 | entryPoints: ['build/simple-index.js'], 14 | bundle: true, 15 | platform: 'node', 16 | target: 'node18', 17 | outfile: 'unity-mcp-server.bundle.js', 18 | format: 'esm', 19 | // No banner needed - causes conflicts 20 | external: [ 21 | 'crypto', 22 | 'fs', 23 | 'path', 24 | 'url', 25 | 'util', 26 | 'stream', 27 | 'os', 28 | 'events', 29 | 'http', 30 | 'https', 31 | 'net', 32 | 'child_process', 33 | 'readline', 34 | 'zlib', 35 | 'buffer', 36 | 'string_decoder', 37 | 'querystring', 38 | 'assert', 39 | 'tty', 40 | 'dgram', 41 | 'dns', 42 | 'v8', 43 | 'vm', 44 | 'worker_threads', 45 | 'perf_hooks' 46 | ], 47 | minify: false, 48 | sourcemap: false, 49 | metafile: true 50 | }); 51 | 52 | console.log('Bundle created: unity-mcp-server.bundle.js'); ``` -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- ```markdown 1 | # Building Unity MCP Server DXT Package 2 | 3 | ## Quick Build 4 | 5 | To build the complete DXT package for Claude Desktop: 6 | 7 | ```bash 8 | npm run build:dxt 9 | ``` 10 | 11 | This will create `unity-mcp-server.dxt` which can be installed in Claude Desktop. 12 | 13 | ## Manual Build Steps 14 | 15 | If you need to build manually: 16 | 17 | 1. **Build TypeScript:** 18 | ```bash 19 | npm run build 20 | ``` 21 | 22 | 2. **Create JavaScript bundle:** 23 | ```bash 24 | node build-bundle.js 25 | ``` 26 | 27 | 3. **Create DXT package:** 28 | ```bash 29 | ./create-bundled-dxt.sh 30 | ``` 31 | 32 | ## Installation 33 | 34 | 1. Build the DXT package using `npm run build:dxt` 35 | 2. Install `unity-mcp-server.dxt` in Claude Desktop 36 | 3. The server will automatically start when Claude Desktop loads 37 | 38 | ## What's Included 39 | 40 | The DXT package contains: 41 | - **manifest.json** - Extension metadata 42 | - **unity-mcp-server.bundle.js** - Complete bundled server with embedded Unity scripts 43 | 44 | All Unity C# scripts are embedded directly in the JavaScript bundle, eliminating file dependencies. 45 | 46 | ## Clean Build 47 | 48 | To start fresh: 49 | 50 | ```bash 51 | npm run clean 52 | npm run build:dxt 53 | ``` ``` -------------------------------------------------------------------------------- /build-final-dxt.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Unity MCP Server - Final DXT Builder 4 | # This script builds the complete Unity MCP Server DXT package 5 | 6 | set -e 7 | 8 | echo "Building Unity MCP Server DXT package..." 9 | 10 | # Clean previous builds 11 | echo "Cleaning previous builds..." 12 | rm -f unity-mcp-server.bundle.js 13 | rm -f unity-mcp-server.dxt 14 | 15 | # Generate embedded scripts from Unity source files 16 | echo "Generating embedded scripts..." 17 | node generate-embedded-scripts.cjs 18 | 19 | # Build TypeScript 20 | echo "Building TypeScript..." 21 | npm run build 22 | 23 | # Create bundle 24 | echo "Creating JavaScript bundle..." 25 | node build-bundle.js 26 | 27 | # Create DXT package 28 | echo "Creating DXT package..." 29 | ./create-bundled-dxt.sh 30 | 31 | # Verify final package 32 | if [ -f "unity-mcp-server.dxt" ]; then 33 | echo "✓ Successfully created unity-mcp-server.dxt" 34 | echo " Size: $(ls -lh unity-mcp-server.dxt | awk '{print $5}')" 35 | echo " Contents:" 36 | unzip -l unity-mcp-server.dxt | grep -E '\.(json|js)$' | awk '{print " " $4}' 37 | else 38 | echo "✗ Failed to create unity-mcp-server.dxt" 39 | exit 1 40 | fi 41 | 42 | echo "DXT package build complete!" 43 | echo "Install the package: unity-mcp-server.dxt" ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "unity-mcp-server", 3 | "version": "3.1.1", 4 | "description": "Unity MCP Server - Simple HTTP-based Unity Editor integration for AI assistants", 5 | "type": "module", 6 | "main": "build/simple-index.js", 7 | "bin": { 8 | "unity-mcp-server": "./build/simple-index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "dev": "tsc --watch", 13 | "start": "node build/simple-index.js", 14 | "clean": "rm -rf build", 15 | "prepare": "npm run build", 16 | "build:dxt": "./build-final-dxt.sh", 17 | "test": "vitest", 18 | "test:watch": "vitest --watch", 19 | "test:coverage": "vitest run --coverage", 20 | "test:unit": "vitest run tests/unit", 21 | "test:integration": "vitest run tests/integration" 22 | }, 23 | "keywords": [ 24 | "mcp", 25 | "mcp-server", 26 | "model-context-protocol", 27 | "unity", 28 | "unity3d", 29 | "unity-editor", 30 | "gamedev", 31 | "game-development", 32 | "claude", 33 | "ai", 34 | "ai-tools" 35 | ], 36 | "author": "zabaglione", 37 | "license": "MIT", 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/zabaglione/mcp-server-unity.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/zabaglione/mcp-server-unity/issues" 44 | }, 45 | "homepage": "https://github.com/zabaglione/mcp-server-unity#readme", 46 | "dependencies": { 47 | "@modelcontextprotocol/sdk": "^0.5.0" 48 | }, 49 | "devDependencies": { 50 | "@types/node": "^20.0.0", 51 | "@vitest/coverage-v8": "^3.2.4", 52 | "esbuild": "^0.25.6", 53 | "typescript": "^5.0.0", 54 | "vitest": "^3.2.4" 55 | }, 56 | "engines": { 57 | "node": ">=16.0.0" 58 | } 59 | } 60 | ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "dxt_version": "1.0", 3 | "name": "unity-mcp-server", 4 | "version": "3.1.0", 5 | "description": "Unity MCP Server - Enable AI assistants to interact with Unity projects", 6 | "author": { 7 | "name": "zabaglione", 8 | "url": "https://github.com/zabaglione" 9 | }, 10 | "server": { 11 | "type": "node", 12 | "entry_point": "server/simple-index.js", 13 | "mcp_config": { 14 | "command": "node", 15 | "args": ["${__dirname}/server/simple-index.js"] 16 | } 17 | }, 18 | "tools": [ 19 | { 20 | "name": "project_info", 21 | "description": "Get Unity project information" 22 | }, 23 | { 24 | "name": "project_status", 25 | "description": "Check Unity connection status" 26 | }, 27 | { 28 | "name": "setup_unity_bridge", 29 | "description": "Install/update Unity MCP scripts" 30 | }, 31 | { 32 | "name": "script_create", 33 | "description": "Create a new C# script" 34 | }, 35 | { 36 | "name": "script_read", 37 | "description": "Read script contents" 38 | }, 39 | { 40 | "name": "script_apply_diff", 41 | "description": "Apply unified diff to update scripts" 42 | }, 43 | { 44 | "name": "script_delete", 45 | "description": "Delete a script" 46 | }, 47 | { 48 | "name": "shader_create", 49 | "description": "Create a new shader" 50 | }, 51 | { 52 | "name": "shader_read", 53 | "description": "Read shader contents" 54 | }, 55 | { 56 | "name": "shader_delete", 57 | "description": "Delete a shader" 58 | }, 59 | { 60 | "name": "folder_create", 61 | "description": "Create a new folder" 62 | }, 63 | { 64 | "name": "folder_rename", 65 | "description": "Rename a folder" 66 | }, 67 | { 68 | "name": "folder_move", 69 | "description": "Move a folder to new location" 70 | }, 71 | { 72 | "name": "folder_delete", 73 | "description": "Delete a folder recursively" 74 | }, 75 | { 76 | "name": "folder_list", 77 | "description": "List folder contents" 78 | } 79 | ] 80 | } ``` -------------------------------------------------------------------------------- /src/simple-index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 4 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 5 | import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; 6 | import { UnityMcpTools } from './tools/unity-mcp-tools.js'; 7 | 8 | /** 9 | * Simple Unity MCP Server 10 | * Provides Unity Editor integration through MCP protocol 11 | */ 12 | class UnityMcpServer { 13 | private server: Server; 14 | private tools: UnityMcpTools; 15 | 16 | constructor() { 17 | this.tools = new UnityMcpTools(); 18 | this.server = new Server( 19 | { 20 | name: 'unity-mcp-server', 21 | version: '1.0.0', 22 | }, 23 | { 24 | capabilities: { 25 | tools: {}, 26 | }, 27 | } 28 | ); 29 | 30 | this.setupHandlers(); 31 | } 32 | 33 | private setupHandlers() { 34 | // Handle tool listing 35 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 36 | tools: this.tools.getTools(), 37 | })); 38 | 39 | // Handle tool execution 40 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 41 | return this.tools.executeTool(request.params.name, request.params.arguments || {}); 42 | }); 43 | } 44 | 45 | async run() { 46 | try { 47 | console.error('[Unity MCP] Starting server...'); 48 | const transport = new StdioServerTransport(); 49 | 50 | console.error('[Unity MCP] Connecting to transport...'); 51 | await this.server.connect(transport); 52 | console.error('[Unity MCP] Server connected successfully'); 53 | 54 | // Keep the process alive - this is critical! 55 | process.stdin.resume(); 56 | 57 | process.on('SIGINT', () => { 58 | console.error('[Unity MCP] Received SIGINT, shutting down...'); 59 | process.exit(0); 60 | }); 61 | 62 | process.on('SIGTERM', () => { 63 | console.error('[Unity MCP] Received SIGTERM, shutting down...'); 64 | process.exit(0); 65 | }); 66 | 67 | // Log that we're ready 68 | console.error('[Unity MCP] Server is ready and listening'); 69 | } catch (error) { 70 | console.error('[Unity MCP] Failed to start server:', error); 71 | throw error; 72 | } 73 | } 74 | } 75 | 76 | // Main entry point 77 | const server = new UnityMcpServer(); 78 | server.run().catch((error) => { 79 | console.error('[Unity MCP] Fatal error:', error); 80 | process.exit(1); 81 | }); 82 | 83 | // Handle uncaught errors 84 | process.on('uncaughtException', (error) => { 85 | console.error('[Unity MCP] Uncaught exception:', error); 86 | process.exit(1); 87 | }); 88 | 89 | process.on('unhandledRejection', (reason, promise) => { 90 | console.error('[Unity MCP] Unhandled rejection at:', promise, 'reason:', reason); 91 | process.exit(1); 92 | }); ``` -------------------------------------------------------------------------------- /generate-embedded-scripts.cjs: -------------------------------------------------------------------------------- ``` 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | 4 | async function generateEmbeddedScripts() { 5 | const scriptsDir = path.join(__dirname, 'src', 'unity-scripts'); 6 | const outputPath = path.join(__dirname, 'src', 'embedded-scripts.ts'); 7 | 8 | try { 9 | console.log('Generating embedded-scripts.ts from Unity source files...'); 10 | 11 | // Define scripts to embed 12 | const scriptsToEmbed = [ 13 | { 14 | fileName: 'UnityHttpServer.cs', 15 | version: '1.1.0', 16 | sourcePath: path.join(scriptsDir, 'UnityHttpServer.cs') 17 | }, 18 | { 19 | fileName: 'UnityMCPServerWindow.cs', 20 | version: '1.0.0', 21 | sourcePath: path.join(scriptsDir, 'UnityMCPServerWindow.cs') 22 | } 23 | ]; 24 | 25 | // Read all script contents 26 | const embeddedScripts = []; 27 | 28 | for (const script of scriptsToEmbed) { 29 | try { 30 | const content = await fs.readFile(script.sourcePath, 'utf-8'); 31 | 32 | // Escape content for JavaScript template literal 33 | const escapedContent = content 34 | .replace(/\\/g, '\\\\') 35 | .replace(/`/g, '\\`') 36 | .replace(/\$\{/g, '\\${'); 37 | 38 | embeddedScripts.push({ 39 | fileName: script.fileName, 40 | version: script.version, 41 | content: escapedContent 42 | }); 43 | 44 | console.log(`✓ Embedded ${script.fileName} (${content.length} chars)`); 45 | } catch (error) { 46 | console.error(`✗ Failed to read ${script.fileName}: ${error.message}`); 47 | throw error; 48 | } 49 | } 50 | 51 | // Generate TypeScript content 52 | const tsContent = `import * as fs from 'fs/promises'; 53 | import * as path from 'path'; 54 | 55 | export interface EmbeddedScript { 56 | fileName: string; 57 | content: string; 58 | version: string; 59 | } 60 | 61 | /** 62 | * Static embedded scripts provider 63 | * Generated at build time from Unity source files 64 | */ 65 | export class EmbeddedScriptsProvider { 66 | private scripts: Map<string, EmbeddedScript> = new Map(); 67 | 68 | constructor() { 69 | this.initializeScripts(); 70 | } 71 | 72 | private initializeScripts() { 73 | ${embeddedScripts.map(script => ` // ${script.fileName} content 74 | this.scripts.set('${script.fileName}', { 75 | fileName: '${script.fileName}', 76 | version: '${script.version}', 77 | content: \`${script.content}\` 78 | });`).join('\n\n')} 79 | } 80 | 81 | /** 82 | * Get script by filename 83 | */ 84 | async getScript(fileName: string): Promise<EmbeddedScript | null> { 85 | return this.scripts.get(fileName) || null; 86 | } 87 | 88 | /** 89 | * Get script synchronously 90 | */ 91 | getScriptSync(fileName: string): EmbeddedScript | null { 92 | return this.scripts.get(fileName) || null; 93 | } 94 | 95 | /** 96 | * Write script to file with proper UTF-8 BOM for Unity compatibility 97 | */ 98 | async writeScriptToFile(fileName: string, targetPath: string): Promise<void> { 99 | const script = await this.getScript(fileName); 100 | if (!script) { 101 | throw new Error(\`Script not found: \${fileName}\`); 102 | } 103 | 104 | // Ensure target directory exists 105 | await fs.mkdir(path.dirname(targetPath), { recursive: true }); 106 | 107 | // Write with UTF-8 BOM for Unity compatibility 108 | const utf8BOM = Buffer.from([0xEF, 0xBB, 0xBF]); 109 | const contentBuffer = Buffer.from(script.content, 'utf8'); 110 | const finalBuffer = Buffer.concat([utf8BOM, contentBuffer]); 111 | 112 | await fs.writeFile(targetPath, finalBuffer); 113 | } 114 | 115 | /** 116 | * Get all available script names 117 | */ 118 | getAvailableScripts(): string[] { 119 | return Array.from(this.scripts.keys()); 120 | } 121 | 122 | /** 123 | * Get script version 124 | */ 125 | getScriptVersion(fileName: string): string | null { 126 | const script = this.scripts.get(fileName); 127 | return script?.version || null; 128 | } 129 | }`; 130 | 131 | // Write the generated file 132 | await fs.writeFile(outputPath, tsContent, 'utf-8'); 133 | 134 | console.log(`✓ Generated embedded-scripts.ts with ${embeddedScripts.length} scripts`); 135 | console.log(` Output: ${outputPath}`); 136 | 137 | } catch (error) { 138 | console.error('Failed to generate embedded scripts:', error.message); 139 | process.exit(1); 140 | } 141 | } 142 | 143 | generateEmbeddedScripts(); ``` -------------------------------------------------------------------------------- /tests/integration/simple-integration.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeAll, afterAll, test } from 'vitest'; 2 | import { spawn } from 'child_process'; 3 | import { UnityHttpAdapter } from '../../src/adapters/unity-http-adapter.js'; 4 | 5 | describe('Unity MCP Integration Tests', () => { 6 | let adapter: UnityHttpAdapter; 7 | 8 | beforeAll(() => { 9 | adapter = new UnityHttpAdapter(); 10 | }); 11 | 12 | describe('Connection', () => { 13 | it('should check connection status', async () => { 14 | // This test will pass if Unity is running with the HTTP server 15 | // or fail gracefully if not 16 | const connected = await adapter.isConnected(); 17 | console.log('Unity connection status:', connected); 18 | expect(typeof connected).toBe('boolean'); 19 | }); 20 | }); 21 | 22 | describe('Script Operations (if connected)', () => { 23 | it.skipIf(async () => !(await adapter.isConnected()))('should create, read, and delete a script', async () => { 24 | // Test will only run if connected 25 | 26 | // Create 27 | const createResult = await adapter.createScript( 28 | 'IntegrationTestScript', 29 | 'public class IntegrationTestScript : MonoBehaviour { }', 30 | 'Assets/Scripts/Tests' 31 | ); 32 | expect(createResult.path).toContain('IntegrationTestScript.cs'); 33 | expect(createResult.guid).toBeTruthy(); 34 | 35 | // Read 36 | const readResult = await adapter.readScript(createResult.path); 37 | expect(readResult.content).toContain('IntegrationTestScript'); 38 | expect(readResult.path).toBe(createResult.path); 39 | 40 | // Delete 41 | const deleteResult = await adapter.deleteScript(createResult.path); 42 | expect(deleteResult.message).toContain('successfully'); 43 | }); 44 | }); 45 | 46 | describe('Shader Operations (if connected)', () => { 47 | it.skipIf(async () => !(await adapter.isConnected()))('should create, read, and delete a shader', async () => { 48 | 49 | // Create 50 | const createResult = await adapter.createShader( 51 | 'IntegrationTestShader', 52 | 'Shader "Custom/IntegrationTest" { SubShader { Pass { } } }', 53 | 'Assets/Shaders/Tests' 54 | ); 55 | expect(createResult.path).toContain('IntegrationTestShader.shader'); 56 | 57 | // Read 58 | const readResult = await adapter.readShader(createResult.path); 59 | expect(readResult.content).toContain('IntegrationTest'); 60 | 61 | // Delete 62 | const deleteResult = await adapter.deleteShader(createResult.path); 63 | expect(deleteResult.message).toContain('successfully'); 64 | }); 65 | }); 66 | 67 | describe('Project Operations (if connected)', () => { 68 | it.skipIf(async () => !(await adapter.isConnected()))('should get project info', async () => { 69 | 70 | const info = await adapter.getProjectInfo(); 71 | expect(info.projectPath).toBeTruthy(); 72 | expect(info.unityVersion).toMatch(/\d{4}\.\d+\.\d+/); 73 | expect(info.platform).toBeTruthy(); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('MCP Server Integration', () => { 79 | let mcpProcess: any; 80 | 81 | beforeAll(async () => { 82 | // Start MCP server 83 | mcpProcess = spawn('node', ['build/simple-index.js'], { 84 | stdio: ['pipe', 'pipe', 'pipe'] 85 | }); 86 | 87 | // Wait for server to start 88 | await new Promise(resolve => setTimeout(resolve, 1000)); 89 | }); 90 | 91 | afterAll(() => { 92 | mcpProcess?.kill(); 93 | }); 94 | 95 | it('should start without errors', () => { 96 | expect(mcpProcess.pid).toBeTruthy(); 97 | }); 98 | 99 | it('should respond to MCP protocol', async () => { 100 | // Send initialize request 101 | const request = { 102 | jsonrpc: '2.0', 103 | id: 1, 104 | method: 'initialize', 105 | params: { 106 | protocolVersion: '2024-11-05', 107 | capabilities: {} 108 | } 109 | }; 110 | 111 | mcpProcess.stdin.write(JSON.stringify(request) + '\n'); 112 | 113 | // Wait for response with timeout 114 | const response = await new Promise((resolve, reject) => { 115 | const timeout = setTimeout(() => { 116 | reject(new Error('Timeout waiting for response')); 117 | }, 5000); 118 | 119 | mcpProcess.stdout.once('data', (data: Buffer) => { 120 | clearTimeout(timeout); 121 | const lines = data.toString().split('\n'); 122 | for (const line of lines) { 123 | if (line.trim()) { 124 | try { 125 | const parsed = JSON.parse(line); 126 | resolve(parsed); 127 | break; 128 | } catch { 129 | // Continue if not JSON 130 | } 131 | } 132 | } 133 | }); 134 | 135 | mcpProcess.stderr.once('data', (data: Buffer) => { 136 | console.error('MCP server error:', data.toString()); 137 | }); 138 | }); 139 | 140 | console.log('MCP response:', response); 141 | expect(response).toBeDefined(); 142 | expect((response as any).jsonrpc).toBe('2.0'); 143 | }); 144 | }); ``` -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- ```markdown 1 | # Unity MCP Server API Reference 2 | 3 | ## Overview 4 | 5 | Unity MCP Server provides a simple HTTP-based API for Unity Editor integration. All operations are performed through MCP tools that communicate with Unity via HTTP. 6 | 7 | ## MCP Tools 8 | 9 | ### Script Operations 10 | 11 | #### `script_create` 12 | Create a new C# script in the Unity project. 13 | 14 | **Parameters:** 15 | - `fileName` (string, required): Name of the script file (without .cs extension) 16 | - `content` (string, optional): Script content. If not provided, creates an empty MonoBehaviour 17 | - `folder` (string, optional): Target folder path. Defaults to "Assets/Scripts" 18 | 19 | **Example:** 20 | ```json 21 | { 22 | "fileName": "PlayerController", 23 | "content": "public class PlayerController : MonoBehaviour { }", 24 | "folder": "Assets/Scripts/Player" 25 | } 26 | ``` 27 | 28 | #### `script_read` 29 | Read the contents of an existing C# script. 30 | 31 | **Parameters:** 32 | - `path` (string, required): Path to the script file 33 | 34 | **Example:** 35 | ```json 36 | { 37 | "path": "Assets/Scripts/PlayerController.cs" 38 | } 39 | ``` 40 | 41 | #### `script_delete` 42 | Delete a C# script from the Unity project. 43 | 44 | **Parameters:** 45 | - `path` (string, required): Path to the script file 46 | 47 | **Example:** 48 | ```json 49 | { 50 | "path": "Assets/Scripts/PlayerController.cs" 51 | } 52 | ``` 53 | 54 | ### Shader Operations 55 | 56 | #### `shader_create` 57 | Create a new shader file in the Unity project. 58 | 59 | **Parameters:** 60 | - `name` (string, required): Name of the shader (without .shader extension) 61 | - `content` (string, optional): Shader content. If not provided, creates a default shader 62 | - `folder` (string, optional): Target folder path. Defaults to "Assets/Shaders" 63 | 64 | **Example:** 65 | ```json 66 | { 67 | "name": "CustomShader", 68 | "content": "Shader \"Custom/MyShader\" { SubShader { Pass { } } }", 69 | "folder": "Assets/Shaders/Custom" 70 | } 71 | ``` 72 | 73 | #### `shader_read` 74 | Read the contents of an existing shader file. 75 | 76 | **Parameters:** 77 | - `path` (string, required): Path to the shader file 78 | 79 | **Example:** 80 | ```json 81 | { 82 | "path": "Assets/Shaders/CustomShader.shader" 83 | } 84 | ``` 85 | 86 | #### `shader_delete` 87 | Delete a shader file from the Unity project. 88 | 89 | **Parameters:** 90 | - `path` (string, required): Path to the shader file 91 | 92 | **Example:** 93 | ```json 94 | { 95 | "path": "Assets/Shaders/CustomShader.shader" 96 | } 97 | ``` 98 | 99 | ### Project Operations 100 | 101 | #### `project_info` 102 | Get information about the current Unity project. 103 | 104 | **Parameters:** None 105 | 106 | **Returns:** 107 | - `projectPath`: Path to the Unity project 108 | - `unityVersion`: Version of Unity being used 109 | - `platform`: Current build platform 110 | - `isPlaying`: Whether Unity is in play mode 111 | 112 | **Example Response:** 113 | ```json 114 | { 115 | "projectPath": "/Users/user/MyUnityProject", 116 | "unityVersion": "2022.3.0f1", 117 | "platform": "StandaloneOSX", 118 | "isPlaying": false 119 | } 120 | ``` 121 | 122 | #### `project_status` 123 | Check the connection status between MCP server and Unity. 124 | 125 | **Parameters:** None 126 | 127 | **Returns:** 128 | - Connection status message 129 | - Project path (if connected) 130 | 131 | ## HTTP API (Unity Side) 132 | 133 | The Unity HTTP server listens on port 3001 and provides the following endpoints: 134 | 135 | ### Base URL 136 | ``` 137 | http://localhost:3001 138 | ``` 139 | 140 | ### Endpoints 141 | 142 | #### `GET /ping` 143 | Health check endpoint. 144 | 145 | **Response:** 146 | ```json 147 | { 148 | "status": "ok", 149 | "timestamp": "2024-01-10T12:00:00Z" 150 | } 151 | ``` 152 | 153 | #### `POST /script/create` 154 | Create a new C# script. 155 | 156 | **Request Body:** 157 | ```json 158 | { 159 | "fileName": "string", 160 | "content": "string", 161 | "folder": "string" 162 | } 163 | ``` 164 | 165 | #### `POST /script/read` 166 | Read script contents. 167 | 168 | **Request Body:** 169 | ```json 170 | { 171 | "path": "string" 172 | } 173 | ``` 174 | 175 | #### `POST /script/delete` 176 | Delete a script. 177 | 178 | **Request Body:** 179 | ```json 180 | { 181 | "path": "string" 182 | } 183 | ``` 184 | 185 | #### `POST /shader/create` 186 | Create a new shader. 187 | 188 | **Request Body:** 189 | ```json 190 | { 191 | "name": "string", 192 | "content": "string", 193 | "folder": "string" 194 | } 195 | ``` 196 | 197 | #### `POST /shader/read` 198 | Read shader contents. 199 | 200 | **Request Body:** 201 | ```json 202 | { 203 | "path": "string" 204 | } 205 | ``` 206 | 207 | #### `POST /shader/delete` 208 | Delete a shader. 209 | 210 | **Request Body:** 211 | ```json 212 | { 213 | "path": "string" 214 | } 215 | ``` 216 | 217 | #### `GET /project/info` 218 | Get project information. 219 | 220 | **Response:** 221 | ```json 222 | { 223 | "projectPath": "string", 224 | "unityVersion": "string", 225 | "platform": "string", 226 | "isPlaying": boolean 227 | } 228 | ``` 229 | 230 | ## Error Handling 231 | 232 | All errors are returned with appropriate HTTP status codes and error messages: 233 | 234 | - `200 OK`: Success 235 | - `400 Bad Request`: Invalid parameters 236 | - `404 Not Found`: Resource not found 237 | - `500 Internal Server Error`: Unity operation failed 238 | 239 | Error response format: 240 | ```json 241 | { 242 | "error": "Error message describing what went wrong" 243 | } 244 | ``` 245 | 246 | ## Usage with Claude 247 | 248 | When using with Claude Desktop, the tools are automatically available after configuration. You can use natural language to interact with Unity: 249 | 250 | ``` 251 | "Create a new PlayerController script in the Scripts folder" 252 | "Read the contents of the GameManager script" 253 | "Delete the old TestScript" 254 | "Create a new water shader" 255 | "Show me the project information" 256 | ``` 257 | 258 | Claude will translate these requests into the appropriate tool calls. ``` -------------------------------------------------------------------------------- /src/adapters/unity-http-adapter.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Using Node.js 18+ built-in fetch 2 | 3 | export interface UnityHttpAdapterOptions { 4 | url?: string; 5 | timeout?: number; 6 | } 7 | 8 | export interface UnityResponse { 9 | success: boolean; 10 | result?: any; 11 | error?: string; 12 | } 13 | 14 | export interface FolderEntry { 15 | path: string; 16 | name: string; 17 | type: 'file' | 'folder'; 18 | extension?: string; 19 | guid: string; 20 | } 21 | 22 | /** 23 | * HTTP adapter for Unity MCP Server 24 | * Provides a clean interface to communicate with Unity HTTP server 25 | */ 26 | export class UnityHttpAdapter { 27 | private url: string; 28 | private timeout: number; 29 | 30 | constructor(options: UnityHttpAdapterOptions = {}) { 31 | this.url = options.url || 'http://localhost:23457/'; 32 | this.timeout = options.timeout || 15000; 33 | } 34 | 35 | /** 36 | * Call a method on the Unity server 37 | */ 38 | async call(method: string, params: Record<string, any> = {}): Promise<any> { 39 | const startTime = Date.now(); 40 | console.error(`[Unity HTTP] Calling method: ${method}`); 41 | 42 | const maxRetries = 3; 43 | let lastError: any; 44 | 45 | for (let retry = 0; retry < maxRetries; retry++) { 46 | if (retry > 0) { 47 | console.error(`[Unity HTTP] Retry ${retry}/${maxRetries - 1} for method: ${method}`); 48 | await new Promise(resolve => setTimeout(resolve, 1000 * retry)); // Exponential backoff 49 | } 50 | 51 | try { 52 | const controller = new AbortController(); 53 | const timeoutId = setTimeout(() => { 54 | console.error(`[Unity HTTP] Request timeout after ${this.timeout}ms for method: ${method}`); 55 | controller.abort(); 56 | }, this.timeout); 57 | 58 | const response = await fetch(this.url, { 59 | method: 'POST', 60 | headers: { 61 | 'Content-Type': 'application/json; charset=utf-8', 62 | 'Accept': 'application/json; charset=utf-8' 63 | }, 64 | body: JSON.stringify({ method, ...params }), 65 | signal: controller.signal 66 | }); 67 | 68 | clearTimeout(timeoutId); 69 | 70 | const elapsed = Date.now() - startTime; 71 | console.error(`[Unity HTTP] Response received in ${elapsed}ms for method: ${method}`); 72 | 73 | const result = await response.json() as UnityResponse; 74 | 75 | if (!result.success) { 76 | throw new Error(result.error || 'Unknown error'); 77 | } 78 | 79 | return result.result; 80 | 81 | } catch (error: any) { 82 | lastError = error; 83 | 84 | if (error.name === 'AbortError') { 85 | lastError = new Error('Request timeout'); 86 | } else if (error.message?.includes('ECONNREFUSED')) { 87 | lastError = new Error('Unity HTTP server is not running'); 88 | } else if (error.message?.includes('Failed to fetch')) { 89 | lastError = new Error('Failed to connect to Unity HTTP server'); 90 | } 91 | 92 | console.error(`[Unity HTTP] Error on attempt ${retry + 1}: ${lastError.message}`); 93 | 94 | // Don't retry on certain errors 95 | if (error.message?.includes('Method not found')) { 96 | throw error; 97 | } 98 | } 99 | } 100 | 101 | // All retries failed 102 | throw lastError || new Error('Unknown error after retries'); 103 | } 104 | 105 | /** 106 | * Check if Unity server is connected 107 | */ 108 | async isConnected(): Promise<boolean> { 109 | try { 110 | await this.call('ping'); 111 | return true; 112 | } catch { 113 | return false; 114 | } 115 | } 116 | 117 | // Script operations 118 | async createScript(fileName: string, content?: string, folder?: string): Promise<any> { 119 | return this.call('script/create', { fileName, content, folder }); 120 | } 121 | 122 | async readScript(path: string): Promise<any> { 123 | return this.call('script/read', { path }); 124 | } 125 | 126 | async deleteScript(path: string): Promise<any> { 127 | return this.call('script/delete', { path }); 128 | } 129 | 130 | async applyDiff(path: string, diff: string, options?: any): Promise<any> { 131 | return this.call('script/applyDiff', { path, diff, options }); 132 | } 133 | 134 | // Shader operations 135 | async createShader(name: string, content?: string, folder?: string): Promise<any> { 136 | return this.call('shader/create', { name, content, folder }); 137 | } 138 | 139 | async readShader(path: string): Promise<any> { 140 | return this.call('shader/read', { path }); 141 | } 142 | 143 | async deleteShader(path: string): Promise<any> { 144 | return this.call('shader/delete', { path }); 145 | } 146 | 147 | // Project operations 148 | async getProjectInfo(): Promise<any> { 149 | return this.call('project/info'); 150 | } 151 | 152 | // Folder operations 153 | async createFolder(path: string): Promise<{ path: string; guid: string }> { 154 | return this.call('folder/create', { path }); 155 | } 156 | 157 | async renameFolder(oldPath: string, newName: string): Promise<{ oldPath: string; newPath: string; guid: string }> { 158 | return this.call('folder/rename', { oldPath, newName }); 159 | } 160 | 161 | async moveFolder(sourcePath: string, targetPath: string): Promise<{ sourcePath: string; targetPath: string; guid: string }> { 162 | return this.call('folder/move', { sourcePath, targetPath }); 163 | } 164 | 165 | async deleteFolder(path: string, recursive: boolean = true): Promise<{ path: string }> { 166 | return this.call('folder/delete', { path, recursive }); 167 | } 168 | 169 | async listFolder(path?: string, recursive: boolean = false): Promise<{ path: string; entries: FolderEntry[] }> { 170 | return this.call('folder/list', { path, recursive }); 171 | } 172 | } ``` -------------------------------------------------------------------------------- /tests/unit/templates/shaders/shader-templates.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Shader template tests 3 | */ 4 | 5 | import { getBuiltInShaderTemplate } from '../../../../src/templates/shaders/builtin-shader.js'; 6 | import { getURPShaderTemplate } from '../../../../src/templates/shaders/urp-shader.js'; 7 | import { getHDRPShaderTemplate } from '../../../../src/templates/shaders/hdrp-shader.js'; 8 | 9 | describe('Shader Templates', () => { 10 | describe('Built-in Shader Template', () => { 11 | it('should generate valid built-in shader', () => { 12 | const shader = getBuiltInShaderTemplate('TestShader'); 13 | 14 | expect(shader).toContain('Shader "Custom/TestShader"'); 15 | expect(shader).toContain('Properties'); 16 | expect(shader).toContain('_MainTex'); 17 | expect(shader).toContain('_Color'); 18 | expect(shader).toContain('SubShader'); 19 | expect(shader).toContain('CGPROGRAM'); 20 | expect(shader).toContain('#pragma surface surf Standard'); 21 | expect(shader).toContain('struct Input'); 22 | expect(shader).toContain('void surf'); 23 | expect(shader).toContain('ENDCG'); 24 | expect(shader).toContain('FallBack "Diffuse"'); 25 | }); 26 | 27 | it('should use shader name in declaration', () => { 28 | const shader = getBuiltInShaderTemplate('MyCustomShader'); 29 | expect(shader).toContain('Shader "Custom/MyCustomShader"'); 30 | }); 31 | 32 | it('should include metallic and smoothness properties', () => { 33 | const shader = getBuiltInShaderTemplate('TestShader'); 34 | expect(shader).toContain('_Metallic'); 35 | expect(shader).toContain('_Glossiness'); // Built-in uses _Glossiness 36 | }); 37 | }); 38 | 39 | describe('URP Shader Template', () => { 40 | it('should generate valid URP shader', () => { 41 | const shader = getURPShaderTemplate('TestShader'); 42 | 43 | expect(shader).toContain('Shader "Universal Render Pipeline/Custom/TestShader"'); 44 | expect(shader).toContain('Properties'); 45 | expect(shader).toContain('_BaseMap'); 46 | expect(shader).toContain('_BaseColor'); 47 | expect(shader).toContain('SubShader'); 48 | expect(shader).toContain('Tags'); 49 | expect(shader).toContain('"RenderType"="Opaque"'); 50 | expect(shader).toContain('"RenderPipeline" = "UniversalPipeline"'); // Fixed spacing 51 | expect(shader).toContain('HLSLPROGRAM'); 52 | expect(shader).toContain('#pragma vertex vert'); 53 | expect(shader).toContain('#pragma fragment frag'); 54 | expect(shader).toContain('ENDHLSL'); 55 | }); 56 | 57 | it('should use shader name in declaration', () => { 58 | const shader = getURPShaderTemplate('MyURPShader'); 59 | expect(shader).toContain('Shader "Universal Render Pipeline/Custom/MyURPShader"'); 60 | }); 61 | 62 | it('should include URP-specific includes', () => { 63 | const shader = getURPShaderTemplate('TestShader'); 64 | expect(shader).toContain('Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl'); 65 | expect(shader).toContain('Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl'); 66 | }); 67 | 68 | it('should have Forward pass', () => { 69 | const shader = getURPShaderTemplate('TestShader'); 70 | expect(shader).toContain('Name "ForwardLit"'); 71 | expect(shader).toContain('"LightMode" = "UniversalForward"'); // Fixed spacing 72 | }); 73 | }); 74 | 75 | describe('HDRP Shader Template', () => { 76 | it('should generate valid HDRP shader', () => { 77 | const shader = getHDRPShaderTemplate('TestShader'); 78 | 79 | expect(shader).toContain('Shader "HDRP/Custom/TestShader"'); 80 | expect(shader).toContain('Properties'); 81 | expect(shader).toContain('_BaseColorMap'); 82 | expect(shader).toContain('_BaseColor'); 83 | expect(shader).toContain('_Metallic'); 84 | expect(shader).toContain('_Smoothness'); 85 | expect(shader).toContain('SubShader'); 86 | expect(shader).toContain('HLSLPROGRAM'); 87 | expect(shader).toContain('#pragma vertex Vert'); 88 | expect(shader).toContain('#pragma fragment Frag'); 89 | expect(shader).toContain('ENDHLSL'); 90 | }); 91 | 92 | it('should use shader name in declaration', () => { 93 | const shader = getHDRPShaderTemplate('MyHDRPShader'); 94 | expect(shader).toContain('Shader "HDRP/Custom/MyHDRPShader"'); 95 | }); 96 | 97 | it('should include HDRP-specific includes', () => { 98 | const shader = getHDRPShaderTemplate('TestShader'); 99 | expect(shader).toContain('Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl'); 100 | expect(shader).toContain('Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl'); 101 | expect(shader).toContain('Packages/com.unity.render-pipelines.high-definition/Runtime/Material/Material.hlsl'); 102 | }); 103 | 104 | it('should have proper HDRP tags', () => { 105 | const shader = getHDRPShaderTemplate('TestShader'); 106 | expect(shader).toContain('"RenderPipeline"="HDRenderPipeline"'); 107 | expect(shader).toContain('"Queue"="Geometry"'); 108 | }); 109 | 110 | it('should have ForwardOnly and ShadowCaster passes', () => { 111 | const shader = getHDRPShaderTemplate('TestShader'); 112 | expect(shader).toContain('Name "ForwardOnly"'); 113 | expect(shader).toContain('"LightMode" = "ForwardOnly"'); 114 | expect(shader).toContain('Name "ShadowCaster"'); 115 | expect(shader).toContain('"LightMode" = "ShadowCaster"'); 116 | }); 117 | 118 | it('should have fallback to HDRP/Lit', () => { 119 | const shader = getHDRPShaderTemplate('TestShader'); 120 | expect(shader).toContain('FallBack "HDRP/Lit"'); 121 | }); 122 | }); 123 | }); ``` -------------------------------------------------------------------------------- /src/unity-scripts/UnityMCPServerWindow.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using UnityEngine; 3 | using UnityEditor; 4 | 5 | namespace UnityMCP 6 | { 7 | /// <summary> 8 | /// Unity MCP Server control window 9 | /// </summary> 10 | public class UnityMCPServerWindow : EditorWindow 11 | { 12 | // Version information (should match UnityHttpServer) 13 | private const string SCRIPT_VERSION = "1.1.0"; 14 | 15 | private int serverPort = 23457; 16 | private bool isServerRunning = false; 17 | private string serverStatus = "Stopped"; 18 | private string lastError = ""; 19 | 20 | [MenuItem("Window/Unity MCP Server")] 21 | public static void ShowWindow() 22 | { 23 | GetWindow<UnityMCPServerWindow>("Unity MCP Server"); 24 | } 25 | 26 | void OnEnable() 27 | { 28 | // Load saved settings 29 | serverPort = EditorPrefs.GetInt("UnityMCP.ServerPort", 23457); 30 | UpdateStatus(); 31 | } 32 | 33 | void OnDisable() 34 | { 35 | // Save settings 36 | EditorPrefs.SetInt("UnityMCP.ServerPort", serverPort); 37 | } 38 | 39 | void OnGUI() 40 | { 41 | GUILayout.Label("Unity MCP Server Control", EditorStyles.boldLabel); 42 | GUILayout.Label($"Version: {SCRIPT_VERSION}", EditorStyles.miniLabel); 43 | 44 | EditorGUILayout.Space(); 45 | 46 | // Server Status 47 | EditorGUILayout.BeginHorizontal(); 48 | GUILayout.Label("Status:", GUILayout.Width(60)); 49 | var statusColor = isServerRunning ? Color.green : Color.red; 50 | var originalColor = GUI.color; 51 | GUI.color = statusColor; 52 | GUILayout.Label(serverStatus, EditorStyles.boldLabel); 53 | GUI.color = originalColor; 54 | EditorGUILayout.EndHorizontal(); 55 | 56 | EditorGUILayout.Space(); 57 | 58 | // Port Configuration 59 | EditorGUILayout.BeginHorizontal(); 60 | GUILayout.Label("Port:", GUILayout.Width(60)); 61 | var newPort = EditorGUILayout.IntField(serverPort); 62 | if (newPort != serverPort && newPort > 0 && newPort <= 65535) 63 | { 64 | serverPort = newPort; 65 | EditorPrefs.SetInt("UnityMCP.ServerPort", serverPort); 66 | } 67 | EditorGUILayout.EndHorizontal(); 68 | 69 | // Port validation 70 | if (serverPort < 1024) 71 | { 72 | EditorGUILayout.HelpBox("Warning: Ports below 1024 may require administrator privileges.", MessageType.Warning); 73 | } 74 | 75 | EditorGUILayout.Space(); 76 | 77 | // Control Buttons 78 | EditorGUILayout.BeginHorizontal(); 79 | 80 | GUI.enabled = !isServerRunning; 81 | if (GUILayout.Button("Start Server", GUILayout.Height(30))) 82 | { 83 | StartServer(); 84 | } 85 | 86 | GUI.enabled = isServerRunning; 87 | if (GUILayout.Button("Stop Server", GUILayout.Height(30))) 88 | { 89 | StopServer(); 90 | } 91 | 92 | GUI.enabled = true; 93 | EditorGUILayout.EndHorizontal(); 94 | 95 | EditorGUILayout.Space(); 96 | 97 | // Connection Info 98 | if (isServerRunning) 99 | { 100 | EditorGUILayout.BeginVertical(EditorStyles.helpBox); 101 | GUILayout.Label("Connection Information", EditorStyles.boldLabel); 102 | EditorGUILayout.SelectableLabel($"http://localhost:{serverPort}/"); 103 | EditorGUILayout.EndVertical(); 104 | } 105 | 106 | // Error Display 107 | if (!string.IsNullOrEmpty(lastError)) 108 | { 109 | EditorGUILayout.Space(); 110 | EditorGUILayout.HelpBox(lastError, MessageType.Error); 111 | if (GUILayout.Button("Clear Error")) 112 | { 113 | lastError = ""; 114 | } 115 | } 116 | 117 | EditorGUILayout.Space(); 118 | 119 | // Instructions 120 | EditorGUILayout.BeginVertical(EditorStyles.helpBox); 121 | GUILayout.Label("Instructions", EditorStyles.boldLabel); 122 | GUILayout.Label("1. Configure the port (default: 23457)"); 123 | GUILayout.Label("2. Click 'Start Server' to begin"); 124 | GUILayout.Label("3. Use the MCP client to connect"); 125 | GUILayout.Label("4. Click 'Stop Server' when done"); 126 | EditorGUILayout.EndVertical(); 127 | } 128 | 129 | void StartServer() 130 | { 131 | try 132 | { 133 | UnityHttpServer.Start(serverPort); 134 | UpdateStatus(); 135 | lastError = ""; 136 | Debug.Log($"[UnityMCP] Server started on port {serverPort}"); 137 | } 138 | catch (Exception e) 139 | { 140 | lastError = $"Failed to start server: {e.Message}"; 141 | Debug.LogError($"[UnityMCP] {lastError}"); 142 | } 143 | } 144 | 145 | void StopServer() 146 | { 147 | try 148 | { 149 | UnityHttpServer.Shutdown(); 150 | UpdateStatus(); 151 | lastError = ""; 152 | Debug.Log("[UnityMCP] Server stopped"); 153 | } 154 | catch (Exception e) 155 | { 156 | lastError = $"Failed to stop server: {e.Message}"; 157 | Debug.LogError($"[UnityMCP] {lastError}"); 158 | } 159 | } 160 | 161 | void UpdateStatus() 162 | { 163 | isServerRunning = UnityHttpServer.IsRunning; 164 | serverStatus = isServerRunning ? $"Running on port {UnityHttpServer.CurrentPort}" : "Stopped"; 165 | Repaint(); 166 | } 167 | 168 | void Update() 169 | { 170 | // Update status periodically 171 | UpdateStatus(); 172 | } 173 | } 174 | } ``` -------------------------------------------------------------------------------- /tests/unit/tools/unity-mcp-tools.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach, vi } from 'vitest'; 2 | import { UnityMcpTools } from '../../../src/tools/unity-mcp-tools.js'; 3 | import { UnityHttpAdapter } from '../../../src/adapters/unity-http-adapter.js'; 4 | 5 | // Mock the adapter 6 | vi.mock('../../../src/adapters/unity-http-adapter.js'); 7 | 8 | describe('UnityMcpTools', () => { 9 | let tools: UnityMcpTools; 10 | let mockAdapter: any; 11 | 12 | beforeEach(() => { 13 | mockAdapter = { 14 | isConnected: vi.fn().mockResolvedValue(true), 15 | createScript: vi.fn(), 16 | readScript: vi.fn(), 17 | deleteScript: vi.fn(), 18 | createShader: vi.fn(), 19 | readShader: vi.fn(), 20 | deleteShader: vi.fn(), 21 | getProjectInfo: vi.fn() 22 | }; 23 | 24 | vi.mocked(UnityHttpAdapter).mockImplementation(() => mockAdapter); 25 | tools = new UnityMcpTools(); 26 | }); 27 | 28 | describe('getTools', () => { 29 | it('should return all available tools', () => { 30 | const toolList = tools.getTools(); 31 | 32 | expect(toolList).toHaveLength(8); // 3 script + 3 shader + 2 project tools 33 | expect(toolList.map(t => t.name)).toContain('script_create'); 34 | expect(toolList.map(t => t.name)).toContain('shader_create'); 35 | expect(toolList.map(t => t.name)).toContain('project_info'); 36 | }); 37 | 38 | it('should have proper input schemas', () => { 39 | const toolList = tools.getTools(); 40 | const scriptCreate = toolList.find(t => t.name === 'script_create'); 41 | 42 | expect(scriptCreate?.inputSchema).toMatchObject({ 43 | type: 'object', 44 | properties: { 45 | fileName: { type: 'string' }, 46 | content: { type: 'string' }, 47 | folder: { type: 'string' } 48 | }, 49 | required: ['fileName'] 50 | }); 51 | }); 52 | }); 53 | 54 | describe('executeTool', () => { 55 | describe('script_create', () => { 56 | it('should create script with all parameters', async () => { 57 | // Arrange 58 | mockAdapter.createScript.mockResolvedValue({ 59 | path: 'Assets/Scripts/Test.cs', 60 | guid: 'test-guid' 61 | }); 62 | 63 | // Act 64 | const result = await tools.executeTool('script_create', { 65 | fileName: 'Test', 66 | content: 'public class Test {}', 67 | folder: 'Assets/Scripts' 68 | }); 69 | 70 | // Assert 71 | expect(mockAdapter.createScript).toHaveBeenCalledWith( 72 | 'Test', 73 | 'public class Test {}', 74 | 'Assets/Scripts' 75 | ); 76 | expect(result).toMatchObject({ 77 | content: [{ 78 | type: 'text', 79 | text: expect.stringContaining('Script created successfully') 80 | }] 81 | }); 82 | }); 83 | 84 | it('should handle missing fileName', async () => { 85 | // Act 86 | const result = await tools.executeTool('script_create', {}); 87 | 88 | // Assert 89 | expect(result.content[0].text).toContain('Error: fileName is required'); 90 | }); 91 | }); 92 | 93 | describe('script_read', () => { 94 | it('should read script and return content', async () => { 95 | // Arrange 96 | mockAdapter.readScript.mockResolvedValue({ 97 | path: 'Assets/Scripts/Test.cs', 98 | content: 'public class Test {}', 99 | guid: 'test-guid' 100 | }); 101 | 102 | // Act 103 | const result = await tools.executeTool('script_read', { 104 | path: 'Assets/Scripts/Test.cs' 105 | }); 106 | 107 | // Assert 108 | expect(result.content[0].text).toContain('public class Test {}'); 109 | }); 110 | }); 111 | 112 | describe('script_delete', () => { 113 | it('should delete script', async () => { 114 | // Arrange 115 | mockAdapter.deleteScript.mockResolvedValue({ 116 | message: 'Script deleted successfully' 117 | }); 118 | 119 | // Act 120 | const result = await tools.executeTool('script_delete', { 121 | path: 'Assets/Scripts/Test.cs' 122 | }); 123 | 124 | // Assert 125 | expect(result.content[0].text).toContain('Script deleted successfully'); 126 | }); 127 | }); 128 | 129 | describe('shader_create', () => { 130 | it('should create shader', async () => { 131 | // Arrange 132 | mockAdapter.createShader.mockResolvedValue({ 133 | path: 'Assets/Shaders/Test.shader', 134 | guid: 'shader-guid' 135 | }); 136 | 137 | // Act 138 | const result = await tools.executeTool('shader_create', { 139 | name: 'Test', 140 | content: 'Shader "Custom/Test" {}', 141 | folder: 'Assets/Shaders' 142 | }); 143 | 144 | // Assert 145 | expect(mockAdapter.createShader).toHaveBeenCalledWith( 146 | 'Test', 147 | 'Shader "Custom/Test" {}', 148 | 'Assets/Shaders' 149 | ); 150 | expect(result.content[0].text).toContain('Shader created successfully'); 151 | }); 152 | }); 153 | 154 | describe('project_info', () => { 155 | it('should return project information', async () => { 156 | // Arrange 157 | mockAdapter.getProjectInfo.mockResolvedValue({ 158 | projectPath: '/Users/test/UnityProject', 159 | unityVersion: '2022.3.0f1', 160 | platform: 'StandaloneOSX', 161 | isPlaying: false 162 | }); 163 | 164 | // Act 165 | const result = await tools.executeTool('project_info', {}); 166 | 167 | // Assert 168 | expect(result.content[0].text).toContain('Unity Project Information'); 169 | expect(result.content[0].text).toContain('2022.3.0f1'); 170 | }); 171 | }); 172 | 173 | describe('project_status', () => { 174 | it('should return connected status', async () => { 175 | // Arrange 176 | mockAdapter.isConnected.mockResolvedValue(true); 177 | mockAdapter.getProjectInfo.mockResolvedValue({ 178 | projectPath: '/Users/test/UnityProject' 179 | }); 180 | 181 | // Act 182 | const result = await tools.executeTool('project_status', {}); 183 | 184 | // Assert 185 | expect(result.content[0].text).toContain('Unity server is connected'); 186 | }); 187 | 188 | it('should return disconnected status', async () => { 189 | // Arrange 190 | mockAdapter.isConnected.mockResolvedValue(false); 191 | 192 | // Act 193 | const result = await tools.executeTool('project_status', {}); 194 | 195 | // Assert 196 | expect(result.content[0].text).toContain('Unity server is not connected'); 197 | }); 198 | }); 199 | 200 | it('should throw error for unknown tool', async () => { 201 | // Act 202 | const result = await tools.executeTool('unknown_tool', {}); 203 | 204 | // Assert 205 | expect(result.content[0].text).toContain('Error: Unknown tool: unknown_tool'); 206 | }); 207 | }); 208 | }); ``` -------------------------------------------------------------------------------- /tests/unity/UnityHttpServerTests.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Net.Http; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using NUnit.Framework; 6 | using Newtonsoft.Json; 7 | using UnityEngine; 8 | using UnityEngine.TestTools; 9 | 10 | namespace UnityMCP.Tests 11 | { 12 | [TestFixture] 13 | public class UnityHttpServerTests 14 | { 15 | private const string BASE_URL = "http://localhost:23457"; 16 | private HttpClient httpClient; 17 | 18 | [SetUp] 19 | public void Setup() 20 | { 21 | httpClient = new HttpClient(); 22 | httpClient.Timeout = TimeSpan.FromSeconds(5); 23 | } 24 | 25 | [TearDown] 26 | public void TearDown() 27 | { 28 | httpClient?.Dispose(); 29 | } 30 | 31 | [Test] 32 | public async Task Server_ShouldRespondToPing() 33 | { 34 | // Arrange 35 | var request = new { method = "ping" }; 36 | var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); 37 | 38 | // Act 39 | var response = await httpClient.PostAsync($"{BASE_URL}/", content); 40 | var responseBody = await response.Content.ReadAsStringAsync(); 41 | var result = JsonConvert.DeserializeObject<dynamic>(responseBody); 42 | 43 | // Assert 44 | Assert.IsTrue(response.IsSuccessStatusCode); 45 | Assert.AreEqual("ok", result.status.ToString()); 46 | } 47 | 48 | [Test] 49 | public async Task CreateScript_ShouldReturnSuccessWithValidParams() 50 | { 51 | // Arrange 52 | var request = new 53 | { 54 | method = "script/create", 55 | fileName = "TestScript", 56 | content = "public class TestScript : MonoBehaviour { }", 57 | folder = "Assets/Scripts/Test" 58 | }; 59 | var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); 60 | 61 | // Act 62 | var response = await httpClient.PostAsync($"{BASE_URL}/", content); 63 | var responseBody = await response.Content.ReadAsStringAsync(); 64 | var result = JsonConvert.DeserializeObject<dynamic>(responseBody); 65 | 66 | // Assert 67 | Assert.IsTrue(response.IsSuccessStatusCode); 68 | Assert.IsTrue(result.success); 69 | Assert.IsNotNull(result.path); 70 | Assert.IsNotNull(result.guid); 71 | } 72 | 73 | [Test] 74 | public async Task CreateScript_ShouldReturnErrorWithMissingFileName() 75 | { 76 | // Arrange 77 | var request = new 78 | { 79 | method = "script/create", 80 | content = "public class TestScript : MonoBehaviour { }" 81 | }; 82 | var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); 83 | 84 | // Act 85 | var response = await httpClient.PostAsync($"{BASE_URL}/", content); 86 | var responseBody = await response.Content.ReadAsStringAsync(); 87 | var result = JsonConvert.DeserializeObject<dynamic>(responseBody); 88 | 89 | // Assert 90 | Assert.AreEqual(400, (int)response.StatusCode); 91 | Assert.IsFalse(result.success); 92 | Assert.IsNotNull(result.error); 93 | } 94 | 95 | [Test] 96 | public async Task ReadScript_ShouldReturnContentForExistingFile() 97 | { 98 | // Arrange - First create a script 99 | var createRequest = new 100 | { 101 | method = "script/create", 102 | fileName = "ReadTestScript", 103 | content = "// Test content", 104 | folder = "Assets/Scripts/Test" 105 | }; 106 | await httpClient.PostAsync($"{BASE_URL}/", 107 | new StringContent(JsonConvert.SerializeObject(createRequest), Encoding.UTF8, "application/json")); 108 | 109 | // Act - Read the script 110 | var readRequest = new 111 | { 112 | method = "script/read", 113 | path = "Assets/Scripts/Test/ReadTestScript.cs" 114 | }; 115 | var response = await httpClient.PostAsync($"{BASE_URL}/", 116 | new StringContent(JsonConvert.SerializeObject(readRequest), Encoding.UTF8, "application/json")); 117 | var responseBody = await response.Content.ReadAsStringAsync(); 118 | var result = JsonConvert.DeserializeObject<dynamic>(responseBody); 119 | 120 | // Assert 121 | Assert.IsTrue(response.IsSuccessStatusCode); 122 | Assert.IsTrue(result.success); 123 | Assert.AreEqual("// Test content", result.content.ToString()); 124 | } 125 | 126 | [Test] 127 | public async Task CreateShader_ShouldReturnSuccessWithValidParams() 128 | { 129 | // Arrange 130 | var request = new 131 | { 132 | method = "shader/create", 133 | name = "TestShader", 134 | content = "Shader \"Custom/TestShader\" { }", 135 | folder = "Assets/Shaders" 136 | }; 137 | var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); 138 | 139 | // Act 140 | var response = await httpClient.PostAsync($"{BASE_URL}/", content); 141 | var responseBody = await response.Content.ReadAsStringAsync(); 142 | var result = JsonConvert.DeserializeObject<dynamic>(responseBody); 143 | 144 | // Assert 145 | Assert.IsTrue(response.IsSuccessStatusCode); 146 | Assert.IsTrue(result.success); 147 | Assert.IsNotNull(result.path); 148 | Assert.IsNotNull(result.guid); 149 | } 150 | 151 | [Test] 152 | public async Task GetProjectInfo_ShouldReturnProjectDetails() 153 | { 154 | // Arrange 155 | var request = new { method = "project/info" }; 156 | var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); 157 | 158 | // Act 159 | var response = await httpClient.PostAsync($"{BASE_URL}/", content); 160 | var responseBody = await response.Content.ReadAsStringAsync(); 161 | var result = JsonConvert.DeserializeObject<dynamic>(responseBody); 162 | 163 | // Assert 164 | Assert.IsTrue(response.IsSuccessStatusCode); 165 | Assert.IsNotNull(result.projectPath); 166 | Assert.IsNotNull(result.unityVersion); 167 | } 168 | 169 | [Test] 170 | public async Task InvalidMethod_ShouldReturn404() 171 | { 172 | // Arrange 173 | var request = new { method = "invalid/method" }; 174 | var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); 175 | 176 | // Act 177 | var response = await httpClient.PostAsync($"{BASE_URL}/", content); 178 | 179 | // Assert 180 | Assert.AreEqual(404, (int)response.StatusCode); 181 | } 182 | } 183 | } ``` -------------------------------------------------------------------------------- /src/services/unity-bridge-deploy-service.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as fs from 'fs/promises'; 2 | import * as path from 'path'; 3 | import { EmbeddedScriptsProvider } from '../embedded-scripts.js'; 4 | 5 | interface DeploymentOptions { 6 | projectPath: string; 7 | forceUpdate?: boolean; 8 | } 9 | 10 | interface ScriptInfo { 11 | fileName: string; 12 | targetPath: string; 13 | version: string; 14 | } 15 | 16 | interface Logger { 17 | info(message: string): void; 18 | debug(message: string): void; 19 | error(message: string): void; 20 | } 21 | 22 | export class UnityBridgeDeployService { 23 | private logger: Logger = { 24 | info: (msg: string) => console.error(`[Unity MCP Deploy] ${msg}`), 25 | debug: (msg: string) => console.error(`[Unity MCP Deploy] DEBUG: ${msg}`), 26 | error: (msg: string) => console.error(`[Unity MCP Deploy] ERROR: ${msg}`) 27 | }; 28 | 29 | private scriptsProvider: EmbeddedScriptsProvider = new EmbeddedScriptsProvider(); 30 | 31 | private readonly SCRIPTS: ScriptInfo[] = [ 32 | { 33 | fileName: 'UnityHttpServer.cs', 34 | targetPath: 'Assets/Editor/MCP/UnityHttpServer.cs', 35 | version: '1.1.0' 36 | }, 37 | { 38 | fileName: 'UnityMCPServerWindow.cs', 39 | targetPath: 'Assets/Editor/MCP/UnityMCPServerWindow.cs', 40 | version: '1.1.0' 41 | } 42 | ]; 43 | 44 | async deployScripts(options: DeploymentOptions): Promise<void> { 45 | const { projectPath, forceUpdate = false } = options; 46 | 47 | // Validate Unity project 48 | const projectValidation = await this.validateUnityProject(projectPath); 49 | if (!projectValidation.isValid) { 50 | throw new Error(`Invalid Unity project: ${projectValidation.error}`); 51 | } 52 | 53 | // Create Editor/MCP directory if it doesn't exist 54 | const editorMCPPath = path.join(projectPath, 'Assets', 'Editor', 'MCP'); 55 | await fs.mkdir(editorMCPPath, { recursive: true }); 56 | 57 | // Deploy each script 58 | for (const script of this.SCRIPTS) { 59 | await this.deployScript(projectPath, script, forceUpdate); 60 | } 61 | 62 | this.logger.info('Unity MCP scripts deployed successfully'); 63 | } 64 | 65 | private async deployScript(projectPath: string, script: ScriptInfo, forceUpdate: boolean): Promise<void> { 66 | const targetPath = path.join(projectPath, script.targetPath); 67 | 68 | // Check if script exists and needs update 69 | const needsUpdate = await this.checkNeedsUpdate(targetPath, script.version, forceUpdate); 70 | 71 | if (needsUpdate) { 72 | // Get script from embedded provider (now async) 73 | const embeddedScript = await this.scriptsProvider.getScript(script.fileName); 74 | if (!embeddedScript) { 75 | throw new Error(`Embedded script not found: ${script.fileName}`); 76 | } 77 | 78 | this.logger.debug(`Using embedded script: ${script.fileName} (loaded from source)`); 79 | 80 | // Remove existing files if they exist (including .meta files) 81 | await this.removeExistingFiles(targetPath); 82 | 83 | // Write script using the embedded provider's method (handles UTF-8 BOM) 84 | await this.scriptsProvider.writeScriptToFile(script.fileName, targetPath); 85 | 86 | // Generate .meta file 87 | await this.generateMetaFile(targetPath); 88 | 89 | this.logger.info(`Deployed ${script.fileName} to ${script.targetPath}`); 90 | } else { 91 | this.logger.debug(`${script.fileName} is up to date`); 92 | } 93 | } 94 | 95 | private async checkNeedsUpdate(targetPath: string, currentVersion: string, forceUpdate: boolean): Promise<boolean> { 96 | if (forceUpdate) { 97 | return true; 98 | } 99 | 100 | try { 101 | const content = await fs.readFile(targetPath, 'utf8'); 102 | 103 | // Extract version from file 104 | const versionMatch = content.match(/SCRIPT_VERSION\s*=\s*"([^"]+)"/); 105 | if (versionMatch) { 106 | const installedVersion = versionMatch[1]; 107 | return this.compareVersions(currentVersion, installedVersion) > 0; 108 | } 109 | 110 | // If no version found, update needed 111 | return true; 112 | } catch (error) { 113 | // File doesn't exist, needs deployment 114 | return true; 115 | } 116 | } 117 | 118 | private compareVersions(version1: string, version2: string): number { 119 | const v1Parts = version1.split('.').map(Number); 120 | const v2Parts = version2.split('.').map(Number); 121 | 122 | for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { 123 | const v1Part = v1Parts[i] || 0; 124 | const v2Part = v2Parts[i] || 0; 125 | 126 | if (v1Part > v2Part) return 1; 127 | if (v1Part < v2Part) return -1; 128 | } 129 | 130 | return 0; 131 | } 132 | 133 | private async generateMetaFile(filePath: string): Promise<void> { 134 | const metaPath = filePath + '.meta'; 135 | 136 | // Check if meta file already exists 137 | try { 138 | await fs.access(metaPath); 139 | return; // Meta file exists, don't overwrite 140 | } catch { 141 | // Meta file doesn't exist, create it 142 | } 143 | 144 | const guid = this.generateGUID(); 145 | const metaContent = `fileFormatVersion: 2 146 | guid: ${guid} 147 | MonoImporter: 148 | externalObjects: {} 149 | serializedVersion: 2 150 | defaultReferences: [] 151 | executionOrder: 0 152 | icon: {instanceID: 0} 153 | userData: 154 | assetBundleName: 155 | assetBundleVariant: 156 | `; 157 | 158 | await fs.writeFile(metaPath, metaContent, 'utf8'); 159 | } 160 | 161 | private async removeExistingFiles(targetPath: string): Promise<void> { 162 | try { 163 | // Remove the script file if it exists 164 | await fs.unlink(targetPath); 165 | this.logger.debug(`Removed existing file: ${targetPath}`); 166 | } catch (error: any) { 167 | if (error.code !== 'ENOENT') { 168 | this.logger.debug(`Failed to remove file ${targetPath}: ${error.message}`); 169 | } 170 | } 171 | 172 | try { 173 | // Remove the .meta file if it exists 174 | const metaPath = targetPath + '.meta'; 175 | await fs.unlink(metaPath); 176 | this.logger.debug(`Removed existing meta file: ${metaPath}`); 177 | } catch (error: any) { 178 | if (error.code !== 'ENOENT') { 179 | this.logger.debug(`Failed to remove meta file ${targetPath}.meta: ${error.message}`); 180 | } 181 | } 182 | } 183 | 184 | private generateGUID(): string { 185 | // Generate a Unity-compatible GUID 186 | const hex = '0123456789abcdef'; 187 | let guid = ''; 188 | for (let i = 0; i < 32; i++) { 189 | guid += hex[Math.floor(Math.random() * 16)]; 190 | } 191 | return guid; 192 | } 193 | 194 | private async validateUnityProject(projectPath: string): Promise<{ isValid: boolean; error?: string }> { 195 | try { 196 | // Check if directory exists 197 | const stats = await fs.stat(projectPath); 198 | if (!stats.isDirectory()) { 199 | return { isValid: false, error: 'Path is not a directory' }; 200 | } 201 | 202 | // Check for Unity project structure 203 | const requiredDirs = ['Assets', 'ProjectSettings']; 204 | for (const dir of requiredDirs) { 205 | try { 206 | const dirPath = path.join(projectPath, dir); 207 | const dirStats = await fs.stat(dirPath); 208 | if (!dirStats.isDirectory()) { 209 | return { isValid: false, error: `Missing ${dir} directory` }; 210 | } 211 | } catch { 212 | return { isValid: false, error: `Missing ${dir} directory` }; 213 | } 214 | } 215 | 216 | return { isValid: true }; 217 | } catch (error: any) { 218 | return { isValid: false, error: error.message }; 219 | } 220 | } 221 | } ``` -------------------------------------------------------------------------------- /tests/unit/adapters/unity-http-adapter.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; 2 | import { UnityHttpAdapter } from '../../../src/adapters/unity-http-adapter.js'; 3 | 4 | // Mock global fetch 5 | const mockFetch = vi.fn(); 6 | global.fetch = mockFetch; 7 | 8 | describe('UnityHttpAdapter', () => { 9 | let adapter: UnityHttpAdapter; 10 | 11 | beforeEach(() => { 12 | adapter = new UnityHttpAdapter(); 13 | vi.clearAllMocks(); 14 | }); 15 | 16 | afterEach(() => { 17 | vi.restoreAllMocks(); 18 | }); 19 | 20 | describe('call', () => { 21 | it('should send POST request with correct format', async () => { 22 | // Arrange 23 | const mockResponse = { 24 | json: vi.fn().mockResolvedValue({ 25 | success: true, 26 | result: { status: 'ok' } 27 | }), 28 | ok: true, 29 | status: 200 30 | }; 31 | mockFetch.mockResolvedValue(mockResponse as any); 32 | 33 | // Act 34 | const result = await adapter.call('ping'); 35 | 36 | // Assert 37 | expect(mockFetch).toHaveBeenCalledWith( 38 | 'http://localhost:23457/', 39 | expect.objectContaining({ 40 | method: 'POST', 41 | headers: { 'Content-Type': 'application/json' }, 42 | body: JSON.stringify({ method: 'ping' }) 43 | }) 44 | ); 45 | expect(result).toEqual({ status: 'ok' }); 46 | }); 47 | 48 | it('should pass parameters correctly', async () => { 49 | // Arrange 50 | const mockResponse = { 51 | json: vi.fn().mockResolvedValue({ 52 | success: true, 53 | result: { path: 'Assets/Scripts/Test.cs' } 54 | }), 55 | ok: true, 56 | status: 200 57 | }; 58 | mockFetch.mockResolvedValue(mockResponse as any); 59 | 60 | const params = { 61 | fileName: 'Test', 62 | content: 'public class Test {}', 63 | folder: 'Assets/Scripts' 64 | }; 65 | 66 | // Act 67 | await adapter.call('script/create', params); 68 | 69 | // Assert 70 | expect(mockFetch).toHaveBeenCalledWith( 71 | 'http://localhost:23457/', 72 | expect.objectContaining({ 73 | body: JSON.stringify({ method: 'script/create', ...params }) 74 | }) 75 | ); 76 | }); 77 | 78 | it('should throw error when success is false', async () => { 79 | // Arrange 80 | const mockResponse = { 81 | json: vi.fn().mockResolvedValue({ 82 | success: false, 83 | error: 'File not found' 84 | }), 85 | ok: true, 86 | status: 404 87 | }; 88 | mockFetch.mockResolvedValue(mockResponse as any); 89 | 90 | // Act & Assert 91 | await expect(adapter.call('script/read', { path: 'missing.cs' })) 92 | .rejects.toThrow('File not found'); 93 | }); 94 | 95 | it('should handle connection refused error', async () => { 96 | // Arrange 97 | mockFetch.mockRejectedValue(new Error('ECONNREFUSED')); 98 | 99 | // Act & Assert 100 | await expect(adapter.call('ping')) 101 | .rejects.toThrow('Unity HTTP server is not running'); 102 | }); 103 | 104 | it('should handle timeout', async () => { 105 | // Arrange 106 | const abortError = new Error('The operation was aborted'); 107 | (abortError as any).name = 'AbortError'; 108 | mockFetch.mockRejectedValue(abortError); 109 | 110 | // Act & Assert 111 | await expect(adapter.call('ping')) 112 | .rejects.toThrow('Request timeout'); 113 | }); 114 | }); 115 | 116 | describe('isConnected', () => { 117 | it('should return true when ping succeeds', async () => { 118 | // Arrange 119 | const mockResponse = { 120 | json: vi.fn().mockResolvedValue({ 121 | success: true, 122 | result: { status: 'ok' } 123 | }), 124 | ok: true, 125 | status: 200 126 | }; 127 | mockFetch.mockResolvedValue(mockResponse as any); 128 | 129 | // Act 130 | const connected = await adapter.isConnected(); 131 | 132 | // Assert 133 | expect(connected).toBe(true); 134 | }); 135 | 136 | it('should return false when ping fails', async () => { 137 | // Arrange 138 | mockFetch.mockRejectedValue(new Error('Connection failed')); 139 | 140 | // Act 141 | const connected = await adapter.isConnected(); 142 | 143 | // Assert 144 | expect(connected).toBe(false); 145 | }); 146 | }); 147 | 148 | describe('script operations', () => { 149 | it('should create script with correct parameters', async () => { 150 | // Arrange 151 | const mockResponse = { 152 | json: vi.fn().mockResolvedValue({ 153 | success: true, 154 | result: { path: 'Assets/Scripts/Test.cs', guid: 'test-guid' } 155 | }), 156 | ok: true, 157 | status: 200 158 | }; 159 | mockFetch.mockResolvedValue(mockResponse as any); 160 | 161 | // Act 162 | const result = await adapter.createScript('Test', 'public class Test {}', 'Assets/Scripts'); 163 | 164 | // Assert 165 | expect(mockFetch).toHaveBeenCalledWith( 166 | 'http://localhost:23457/', 167 | expect.objectContaining({ 168 | body: JSON.stringify({ 169 | method: 'script/create', 170 | fileName: 'Test', 171 | content: 'public class Test {}', 172 | folder: 'Assets/Scripts' 173 | }) 174 | }) 175 | ); 176 | expect(result).toEqual({ path: 'Assets/Scripts/Test.cs', guid: 'test-guid' }); 177 | }); 178 | 179 | it('should read script content', async () => { 180 | // Arrange 181 | const mockResponse = { 182 | json: vi.fn().mockResolvedValue({ 183 | success: true, 184 | result: { 185 | path: 'Assets/Scripts/Test.cs', 186 | content: 'public class Test {}', 187 | guid: 'test-guid' 188 | } 189 | }), 190 | ok: true, 191 | status: 200 192 | }; 193 | mockFetch.mockResolvedValue(mockResponse as any); 194 | 195 | // Act 196 | const result = await adapter.readScript('Assets/Scripts/Test.cs'); 197 | 198 | // Assert 199 | expect(result.content).toBe('public class Test {}'); 200 | }); 201 | 202 | it('should delete script', async () => { 203 | // Arrange 204 | const mockResponse = { 205 | json: vi.fn().mockResolvedValue({ 206 | success: true, 207 | result: { message: 'Script deleted successfully' } 208 | }), 209 | ok: true, 210 | status: 200 211 | }; 212 | mockFetch.mockResolvedValue(mockResponse as any); 213 | 214 | // Act 215 | const result = await adapter.deleteScript('Assets/Scripts/Test.cs'); 216 | 217 | // Assert 218 | expect(result.message).toBe('Script deleted successfully'); 219 | }); 220 | }); 221 | 222 | describe('shader operations', () => { 223 | it('should create shader', async () => { 224 | // Arrange 225 | const mockResponse = { 226 | json: vi.fn().mockResolvedValue({ 227 | success: true, 228 | result: { path: 'Assets/Shaders/Test.shader', guid: 'shader-guid' } 229 | }), 230 | ok: true, 231 | status: 200 232 | }; 233 | mockFetch.mockResolvedValue(mockResponse as any); 234 | 235 | // Act 236 | const result = await adapter.createShader('Test', 'Shader "Custom/Test" {}', 'Assets/Shaders'); 237 | 238 | // Assert 239 | expect(result.path).toBe('Assets/Shaders/Test.shader'); 240 | }); 241 | }); 242 | 243 | describe('project operations', () => { 244 | it('should get project info', async () => { 245 | // Arrange 246 | const mockResponse = { 247 | json: vi.fn().mockResolvedValue({ 248 | success: true, 249 | result: { 250 | projectPath: '/Users/test/UnityProject', 251 | unityVersion: '2022.3.0f1', 252 | platform: 'StandaloneOSX' 253 | } 254 | }), 255 | ok: true, 256 | status: 200 257 | }; 258 | mockFetch.mockResolvedValue(mockResponse as any); 259 | 260 | // Act 261 | const result = await adapter.getProjectInfo(); 262 | 263 | // Assert 264 | expect(result.unityVersion).toBe('2022.3.0f1'); 265 | }); 266 | }); 267 | }); ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # Changelog 2 | 3 | All notable changes to Unity MCP Server will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [3.1.1] - 2025-07-10 9 | 10 | ### Fixed 11 | - **Render Pipeline Detection**: Fixed "Unknown" render pipeline issue 12 | - `project/info` now correctly runs on main thread for Unity API access 13 | - Enhanced render pipeline detection with GraphicsSettings.renderPipelineAsset 14 | - Added fallback to package detection when no render pipeline asset is configured 15 | - Improved debug logging for render pipeline detection troubleshooting 16 | - **AssetDatabase Synchronization**: Fixed "[Worker0] Import Error Code:(4)" errors 17 | - Added proper file cleanup before script deployment 18 | - Implemented removeExistingFiles method to delete both .cs and .meta files 19 | - Enhanced UnityBridgeDeployService with proper file lifecycle management 20 | - Eliminated modification time mismatches between SourceAssetDB and disk 21 | 22 | ### Changed 23 | - **Script Generation**: Unity scripts now generated dynamically from source files 24 | - `generate-embedded-scripts.cjs` creates embedded-scripts.ts from actual Unity C# files 25 | - Eliminates static embedded script content in favor of build-time generation 26 | - Ensures Unity scripts are always up-to-date with source modifications 27 | 28 | ## [3.1.0] - 2025-07-10 29 | 30 | ### Added 31 | - Folder operations support: 32 | - `folder_create` - Create new folders in Unity project 33 | - `folder_rename` - Rename existing folders 34 | - `folder_move` - Move folders to new locations 35 | - `folder_delete` - Delete folders recursively 36 | - `folder_list` - List folder contents with optional recursion 37 | - Automatic Unity MCP bridge script deployment: 38 | - `setup_unity_bridge` - Install/update Unity scripts automatically 39 | - Auto-deployment on first project info request 40 | - Version checking and auto-update support 41 | - Unity control window (Window > Unity MCP Server) for server management 42 | - Shader operations (simplified): 43 | - `shader_create` - Create new shaders with default template 44 | - `shader_read` - Read shader contents 45 | - `shader_delete` - Delete shaders 46 | - **DXT Package Build System**: 47 | - `npm run build:dxt` - One-command DXT package creation 48 | - Embedded Unity C# scripts for elimination of file dependencies 49 | - Unified build system with TypeScript compilation and bundling 50 | 51 | ### Changed 52 | - **DXT Package Optimization**: Reduced package size from 24MB to 41KB through embedded scripts 53 | - **Package Name**: Changed final DXT filename from `unity-mcp-server-bundled.dxt` to `unity-mcp-server.dxt` 54 | - **Build System**: Complete overhaul with automated bundling and packaging scripts 55 | - **Script Deployment**: Unity C# scripts now embedded directly in TypeScript bundle 56 | - Updated port from 3001 to 23457 for better conflict avoidance 57 | - Improved UTF-8 BOM handling for Unity compatibility 58 | - Simplified HTTP-based architecture for better reliability 59 | - Enhanced error messages and logging with [UnityMCP] prefix 60 | - Scripts now install to `Assets/Editor/MCP/` folder structure 61 | 62 | ### Fixed 63 | - **UTF-8 BOM Encoding**: Fixed BOM generation using proper byte array `[0xEF, 0xBB, 0xBF]` 64 | - **Module Resolution**: Eliminated all external file dependencies in DXT package 65 | - **Process Lifecycle**: Added `process.stdin.resume()` to prevent early server shutdown 66 | - Character encoding issues with UTF-8 BOM for Unity files 67 | - Script deployment path handling for various Unity project structures 68 | - Connection stability with retry logic 69 | 70 | ### Removed 71 | - Obsolete shader template files (builtin-shader.ts, hdrp-shader.ts, urp-shader.ts, index.ts) 72 | - External file dependencies in DXT package 73 | 74 | ### Documentation 75 | - Updated README files with correct repository name (mcp-server-unity) 76 | - Added comprehensive usage examples for all operations 77 | - Improved setup instructions with automatic and manual options 78 | - Added BUILD.md with complete build system documentation 79 | 80 | ## [3.0.0] - 2025-06-08 81 | 82 | ### Changed 83 | - Complete rewrite with simplified HTTP-based architecture 84 | - Removed complex service layer in favor of direct API implementation 85 | - Industry-standard diff processing for script updates 86 | - Streamlined to essential features only 87 | 88 | ### Added 89 | - Desktop Extension support with bundled configuration 90 | - Large file support (up to 1GB) with streaming 91 | - Diff-based script update system (`script_apply_diff`) 92 | - Comprehensive test coverage (100%) 93 | 94 | ### Removed 95 | - Legacy service-based architecture 96 | - Complex material and shader management 97 | - Compilation monitoring features 98 | - ProBuilder and package management 99 | 100 | ## [2.2.0] - 2025-06-06 101 | 102 | ### Added 103 | - Shader and material update features: 104 | - `asset_update_shader` - Update existing shader content with automatic temporary backup 105 | - `asset_read_shader` - Read shader file content (supports both code and ShaderGraph) 106 | - `asset_update_material` - Update entire material content with YAML validation 107 | - `asset_clone_material` - Clone material with a new name 108 | - `asset_list_materials` - List all materials in the project 109 | - Enhanced shader service: 110 | - Shader GUID caching for faster lookups 111 | - Shader name detection from file content 112 | - Support for finding shaders by file name or internal shader name 113 | - Temporary backup system: 114 | - Creates backup files only during update operations 115 | - Automatically cleans up backup files after success or failure 116 | - Restores original content on update failure 117 | 118 | ### Changed 119 | - Backup system now uses temporary files with automatic cleanup 120 | - Improved shader lookup to check both file names and shader declarations 121 | - Enhanced error handling with try-finally blocks for resource cleanup 122 | 123 | ### Fixed 124 | - Fixed shader-material GUID reference issues for custom shaders 125 | - Added UnityMetaGenerator for proper meta file creation 126 | - Improved material service to work with custom shader GUIDs 127 | 128 | ## [2.1.0] - 2025-06-06 129 | 130 | ### Added 131 | - Material management features: 132 | - `asset_update_material_shader` - Change material shaders dynamically 133 | - `asset_update_material_properties` - Update material colors, floats, textures, vectors 134 | - `asset_read_material` - Read and inspect material properties 135 | - `asset_batch_convert_materials` - Batch convert materials to different shaders 136 | - Script update functionality: 137 | - `asset_update_script` - Update existing script content 138 | - Code analysis tools: 139 | - `code_analyze_diff` - Get detailed diff between current and new content 140 | - `code_detect_duplicates` - Detect duplicate class names across project 141 | - `code_suggest_namespace` - Auto-suggest namespace based on file location 142 | - `code_apply_namespace` - Apply namespace to scripts automatically 143 | - Compilation monitoring: 144 | - `compile_get_errors` - Get detailed compilation errors with file context 145 | - `compile_get_status` - Check current compilation status 146 | - `compile_install_helper` - Install real-time compilation monitoring 147 | - Render pipeline detection: 148 | - Automatic detection of Built-in, URP, or HDRP 149 | - Material creation uses correct default shader for detected pipeline 150 | 151 | ### Changed 152 | - Material creation now detects render pipeline and uses appropriate shader 153 | - Improved YAML parsing to handle Unity's custom tags (!u!21 &2100000) 154 | - Enhanced project info to include render pipeline information 155 | 156 | ### Fixed 157 | - Fixed URP detection incorrectly showing as Built-in 158 | - Fixed YAML parsing errors for Unity material files 159 | - Fixed material shader GUID handling 160 | 161 | ### Removed 162 | - Removed ProBuilder service (non-functional) 163 | - Removed package management service (non-functional) 164 | - Removed backup functionality from all services 165 | 166 | ## [1.0.0] - 2025-06-02 167 | 168 | ### Added 169 | - Initial release of Unity MCP Server 170 | - Core MCP server implementation with stdio transport 171 | - Unity project management tools: 172 | - `set_unity_project` - Set and validate Unity project path 173 | - `create_script` - Create C# scripts with folder support 174 | - `read_script` - Read C# scripts with recursive search 175 | - `list_scripts` - List all scripts in the project 176 | - `create_scene` - Create Unity scene files with YAML template 177 | - `create_material` - Create Unity material files 178 | - `list_assets` - List and filter project assets 179 | - `project_info` - Get Unity version and project statistics 180 | - `build_project` - Build Unity projects via command line 181 | - Cross-platform support (Windows, macOS, Linux) 182 | - TypeScript implementation with strict typing 183 | - Comprehensive error handling and validation 184 | - Setup scripts for easy installation 185 | - GitHub Actions CI/CD pipeline 186 | - Full documentation and contribution guidelines 187 | 188 | ### Security 189 | - Path traversal protection 190 | - Input validation for all tools 191 | - Safe error messages without sensitive information 192 | 193 | [1.0.0]: https://github.com/zabaglione/mcp-server-unity/releases/tag/v1.0.0 ``` -------------------------------------------------------------------------------- /src/tools/unity-mcp-tools.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { UnityHttpAdapter } from '../adapters/unity-http-adapter.js'; 3 | import { UnityBridgeDeployService } from '../services/unity-bridge-deploy-service.js'; 4 | 5 | /** 6 | * Unity MCP Tools 7 | * Provides MCP tool definitions for Unity operations 8 | */ 9 | export class UnityMcpTools { 10 | private adapter: UnityHttpAdapter; 11 | private deployService: UnityBridgeDeployService; 12 | 13 | constructor() { 14 | const port = process.env.UNITY_MCP_PORT ? parseInt(process.env.UNITY_MCP_PORT) : 23457; 15 | const url = `http://localhost:${port}/`; 16 | console.error(`[Unity MCP] Connecting to Unity at ${url}`); 17 | 18 | this.adapter = new UnityHttpAdapter({ 19 | url, 20 | timeout: parseInt(process.env.UNITY_MCP_TIMEOUT || '120000') 21 | }); 22 | 23 | this.deployService = new UnityBridgeDeployService(); 24 | 25 | // Check connection on startup 26 | this.checkConnection(); 27 | } 28 | 29 | private async checkConnection() { 30 | try { 31 | const connected = await this.adapter.isConnected(); 32 | if (connected) { 33 | console.error('[Unity MCP] Successfully connected to Unity HTTP server'); 34 | } else { 35 | console.error('[Unity MCP] Unity HTTP server is not responding'); 36 | } 37 | } catch (error: any) { 38 | console.error(`[Unity MCP] Connection check failed: ${error.message}`); 39 | } 40 | } 41 | 42 | /** 43 | * Auto-deploy Unity MCP scripts if connected 44 | */ 45 | private async autoDeployScripts(): Promise<void> { 46 | try { 47 | const result = await this.adapter.getProjectInfo(); 48 | await this.deployService.deployScripts({ 49 | projectPath: result.projectPath, 50 | forceUpdate: false 51 | }); 52 | } catch (error: any) { 53 | console.error(`[Unity MCP] Failed to auto-deploy scripts: ${error.message}`); 54 | } 55 | } 56 | 57 | /** 58 | * Get all available tools 59 | */ 60 | getTools(): Tool[] { 61 | return [ 62 | // Script tools 63 | { 64 | name: 'script_create', 65 | description: 'Create a new C# script in Unity project', 66 | inputSchema: { 67 | type: 'object', 68 | properties: { 69 | fileName: { 70 | type: 'string', 71 | description: 'Name of the script file (without .cs extension)' 72 | }, 73 | content: { 74 | type: 'string', 75 | description: 'Script content (optional, will use template if not provided)' 76 | }, 77 | folder: { 78 | type: 'string', 79 | description: 'Target folder path (default: Assets/Scripts)' 80 | } 81 | }, 82 | required: ['fileName'] 83 | } 84 | }, 85 | { 86 | name: 'script_read', 87 | description: 'Read a C# script from Unity project', 88 | inputSchema: { 89 | type: 'object', 90 | properties: { 91 | path: { 92 | type: 'string', 93 | description: 'Path to the script file' 94 | } 95 | }, 96 | required: ['path'] 97 | } 98 | }, 99 | { 100 | name: 'script_delete', 101 | description: 'Delete a C# script from Unity project', 102 | inputSchema: { 103 | type: 'object', 104 | properties: { 105 | path: { 106 | type: 'string', 107 | description: 'Path to the script file' 108 | } 109 | }, 110 | required: ['path'] 111 | } 112 | }, 113 | { 114 | name: 'script_apply_diff', 115 | description: 'Apply a unified diff to a C# script', 116 | inputSchema: { 117 | type: 'object', 118 | properties: { 119 | path: { 120 | type: 'string', 121 | description: 'Path to the script file' 122 | }, 123 | diff: { 124 | type: 'string', 125 | description: 'Unified diff content to apply' 126 | }, 127 | options: { 128 | type: 'object', 129 | description: 'Optional diff application settings', 130 | properties: { 131 | dryRun: { 132 | type: 'boolean', 133 | description: 'Preview changes without applying (default: false)' 134 | } 135 | } 136 | } 137 | }, 138 | required: ['path', 'diff'] 139 | } 140 | }, 141 | 142 | // Shader tools 143 | { 144 | name: 'shader_create', 145 | description: 'Create a new shader in Unity project', 146 | inputSchema: { 147 | type: 'object', 148 | properties: { 149 | name: { 150 | type: 'string', 151 | description: 'Name of the shader (without .shader extension)' 152 | }, 153 | content: { 154 | type: 'string', 155 | description: 'Shader content (optional, will use template if not provided)' 156 | }, 157 | folder: { 158 | type: 'string', 159 | description: 'Target folder path (default: Assets/Shaders)' 160 | } 161 | }, 162 | required: ['name'] 163 | } 164 | }, 165 | { 166 | name: 'shader_read', 167 | description: 'Read a shader from Unity project', 168 | inputSchema: { 169 | type: 'object', 170 | properties: { 171 | path: { 172 | type: 'string', 173 | description: 'Path to the shader file' 174 | } 175 | }, 176 | required: ['path'] 177 | } 178 | }, 179 | { 180 | name: 'shader_delete', 181 | description: 'Delete a shader from Unity project', 182 | inputSchema: { 183 | type: 'object', 184 | properties: { 185 | path: { 186 | type: 'string', 187 | description: 'Path to the shader file' 188 | } 189 | }, 190 | required: ['path'] 191 | } 192 | }, 193 | 194 | // Project tools 195 | { 196 | name: 'project_info', 197 | description: 'Get comprehensive Unity project information including render pipeline details, project path, Unity version, and platform info', 198 | inputSchema: { 199 | type: 'object', 200 | properties: {} 201 | } 202 | }, 203 | { 204 | name: 'project_status', 205 | description: 'Check Unity MCP server connection status (simple connectivity test only)', 206 | inputSchema: { 207 | type: 'object', 208 | properties: {} 209 | } 210 | }, 211 | { 212 | name: 'setup_unity_bridge', 213 | description: 'Install/update Unity MCP bridge scripts to a Unity project (works even if Unity server is not running)', 214 | inputSchema: { 215 | type: 'object', 216 | properties: { 217 | projectPath: { 218 | type: 'string', 219 | description: 'Path to the Unity project' 220 | }, 221 | forceUpdate: { 222 | type: 'boolean', 223 | description: 'Force update even if scripts are up to date', 224 | default: false 225 | } 226 | }, 227 | required: ['projectPath'] 228 | } 229 | }, 230 | 231 | // Folder tools 232 | { 233 | name: 'folder_create', 234 | description: 'Create a new folder in Unity project', 235 | inputSchema: { 236 | type: 'object', 237 | properties: { 238 | path: { 239 | type: 'string', 240 | description: 'Path for the new folder (e.g., Assets/MyFolder)' 241 | } 242 | }, 243 | required: ['path'] 244 | } 245 | }, 246 | { 247 | name: 'folder_rename', 248 | description: 'Rename a folder in Unity project', 249 | inputSchema: { 250 | type: 'object', 251 | properties: { 252 | oldPath: { 253 | type: 'string', 254 | description: 'Current path of the folder' 255 | }, 256 | newName: { 257 | type: 'string', 258 | description: 'New name for the folder' 259 | } 260 | }, 261 | required: ['oldPath', 'newName'] 262 | } 263 | }, 264 | { 265 | name: 'folder_move', 266 | description: 'Move a folder to a new location in Unity project', 267 | inputSchema: { 268 | type: 'object', 269 | properties: { 270 | sourcePath: { 271 | type: 'string', 272 | description: 'Current path of the folder' 273 | }, 274 | targetPath: { 275 | type: 'string', 276 | description: 'Target path for the folder' 277 | } 278 | }, 279 | required: ['sourcePath', 'targetPath'] 280 | } 281 | }, 282 | { 283 | name: 'folder_delete', 284 | description: 'Delete a folder from Unity project', 285 | inputSchema: { 286 | type: 'object', 287 | properties: { 288 | path: { 289 | type: 'string', 290 | description: 'Path of the folder to delete' 291 | }, 292 | recursive: { 293 | type: 'boolean', 294 | description: 'Delete all contents recursively (default: true)' 295 | } 296 | }, 297 | required: ['path'] 298 | } 299 | }, 300 | { 301 | name: 'folder_list', 302 | description: 'List contents of a folder in Unity project', 303 | inputSchema: { 304 | type: 'object', 305 | properties: { 306 | path: { 307 | type: 'string', 308 | description: 'Path of the folder to list (default: Assets)' 309 | }, 310 | recursive: { 311 | type: 'boolean', 312 | description: 'List all subdirectories recursively (default: false)' 313 | } 314 | } 315 | } 316 | } 317 | ]; 318 | } 319 | 320 | /** 321 | * Execute a tool 322 | */ 323 | async executeTool(toolName: string, args: any): Promise<{ content: Array<{ type: string; text: string }> }> { 324 | try { 325 | switch (toolName) { 326 | // Script operations 327 | case 'script_create': { 328 | if (!args.fileName) { 329 | throw new Error('fileName is required'); 330 | } 331 | const result = await this.adapter.createScript(args.fileName, args.content, args.folder); 332 | return { 333 | content: [{ 334 | type: 'text', 335 | text: `Script created successfully:\nPath: ${result.path}\nGUID: ${result.guid}` 336 | }] 337 | }; 338 | } 339 | 340 | case 'script_read': { 341 | if (!args.path) { 342 | throw new Error('path is required'); 343 | } 344 | const result = await this.adapter.readScript(args.path); 345 | return { 346 | content: [{ 347 | type: 'text', 348 | text: `Script content from ${result.path}:\n\n${result.content}` 349 | }] 350 | }; 351 | } 352 | 353 | case 'script_delete': { 354 | if (!args.path) { 355 | throw new Error('path is required'); 356 | } 357 | await this.adapter.deleteScript(args.path); 358 | return { 359 | content: [{ 360 | type: 'text', 361 | text: `Script deleted successfully: ${args.path}` 362 | }] 363 | }; 364 | } 365 | 366 | case 'script_apply_diff': { 367 | if (!args.path || !args.diff) { 368 | throw new Error('path and diff are required'); 369 | } 370 | const result = await this.adapter.applyDiff(args.path, args.diff, args.options); 371 | return { 372 | content: [{ 373 | type: 'text', 374 | text: `Diff applied successfully:\nPath: ${result.path}\nLines added: ${result.linesAdded}\nLines removed: ${result.linesRemoved}` 375 | }] 376 | }; 377 | } 378 | 379 | // Shader operations 380 | case 'shader_create': { 381 | if (!args.name) { 382 | throw new Error('name is required'); 383 | } 384 | const result = await this.adapter.createShader(args.name, args.content, args.folder); 385 | return { 386 | content: [{ 387 | type: 'text', 388 | text: `Shader created successfully:\nPath: ${result.path}\nGUID: ${result.guid}` 389 | }] 390 | }; 391 | } 392 | 393 | case 'shader_read': { 394 | if (!args.path) { 395 | throw new Error('path is required'); 396 | } 397 | const result = await this.adapter.readShader(args.path); 398 | return { 399 | content: [{ 400 | type: 'text', 401 | text: `Shader content from ${result.path}:\n\n${result.content}` 402 | }] 403 | }; 404 | } 405 | 406 | case 'shader_delete': { 407 | if (!args.path) { 408 | throw new Error('path is required'); 409 | } 410 | await this.adapter.deleteShader(args.path); 411 | return { 412 | content: [{ 413 | type: 'text', 414 | text: `Shader deleted successfully: ${args.path}` 415 | }] 416 | }; 417 | } 418 | 419 | // Project operations 420 | case 'project_info': { 421 | const result = await this.adapter.getProjectInfo(); 422 | 423 | // Auto-deploy scripts if needed 424 | await this.autoDeployScripts(); 425 | 426 | return { 427 | content: [{ 428 | type: 'text', 429 | text: `Unity Project Information: 430 | Project Path: ${result.projectPath} 431 | Project Name: ${result.projectName || 'N/A'} 432 | Unity Version: ${result.unityVersion} 433 | Platform: ${result.platform} 434 | Is Playing: ${result.isPlaying} 435 | Render Pipeline: ${result.renderPipeline || 'Unknown'} 436 | Render Pipeline Version: ${result.renderPipelineVersion || 'N/A'}` 437 | }] 438 | }; 439 | } 440 | 441 | case 'project_status': { 442 | const connected = await this.adapter.isConnected(); 443 | const status = connected ? 'Unity server is connected and ready' : 'Unity server is not connected or not responding'; 444 | 445 | return { 446 | content: [{ 447 | type: 'text', 448 | text: status 449 | }] 450 | }; 451 | } 452 | 453 | case 'setup_unity_bridge': { 454 | const { projectPath, forceUpdate } = args; 455 | if (!projectPath) { 456 | throw new Error('projectPath is required'); 457 | } 458 | 459 | try { 460 | await this.deployService.deployScripts({ projectPath, forceUpdate }); 461 | return { 462 | content: [{ 463 | type: 'text', 464 | text: `Unity MCP bridge scripts installed successfully to:\n${projectPath}/Assets/Editor/MCP/\n\nPlease restart Unity Editor or open Window > Unity MCP Server to start the server.` 465 | }] 466 | }; 467 | } catch (error: any) { 468 | throw new Error(`Failed to install scripts: ${error.message}`); 469 | } 470 | } 471 | 472 | // Folder operations 473 | case 'folder_create': { 474 | if (!args.path) { 475 | throw new Error('path is required'); 476 | } 477 | const result = await this.adapter.createFolder(args.path); 478 | return { 479 | content: [{ 480 | type: 'text', 481 | text: `Folder created successfully:\nPath: ${result.path}\nGUID: ${result.guid}` 482 | }] 483 | }; 484 | } 485 | 486 | case 'folder_rename': { 487 | if (!args.oldPath || !args.newName) { 488 | throw new Error('oldPath and newName are required'); 489 | } 490 | const result = await this.adapter.renameFolder(args.oldPath, args.newName); 491 | return { 492 | content: [{ 493 | type: 'text', 494 | text: `Folder renamed successfully:\nOld Path: ${result.oldPath}\nNew Path: ${result.newPath}\nGUID: ${result.guid}` 495 | }] 496 | }; 497 | } 498 | 499 | case 'folder_move': { 500 | if (!args.sourcePath || !args.targetPath) { 501 | throw new Error('sourcePath and targetPath are required'); 502 | } 503 | const result = await this.adapter.moveFolder(args.sourcePath, args.targetPath); 504 | return { 505 | content: [{ 506 | type: 'text', 507 | text: `Folder moved successfully:\nFrom: ${result.sourcePath}\nTo: ${result.targetPath}\nGUID: ${result.guid}` 508 | }] 509 | }; 510 | } 511 | 512 | case 'folder_delete': { 513 | if (!args.path) { 514 | throw new Error('path is required'); 515 | } 516 | await this.adapter.deleteFolder(args.path, args.recursive); 517 | return { 518 | content: [{ 519 | type: 'text', 520 | text: `Folder deleted successfully: ${args.path}` 521 | }] 522 | }; 523 | } 524 | 525 | case 'folder_list': { 526 | const result = await this.adapter.listFolder(args.path, args.recursive); 527 | const entries = result.entries.map(e => { 528 | const prefix = e.type === 'folder' ? '📁' : '📄'; 529 | const info = e.type === 'file' ? ` (${e.extension})` : ''; 530 | return `${prefix} ${e.name}${info} - ${e.path}`; 531 | }).join('\n'); 532 | 533 | return { 534 | content: [{ 535 | type: 'text', 536 | text: `Contents of ${result.path}:\n\n${entries || '(empty)'}` 537 | }] 538 | }; 539 | } 540 | 541 | default: 542 | throw new Error(`Unknown tool: ${toolName}`); 543 | } 544 | } catch (error: any) { 545 | return { 546 | content: [{ 547 | type: 'text', 548 | text: `Error: ${error.message}` 549 | }] 550 | }; 551 | } 552 | } 553 | } ``` -------------------------------------------------------------------------------- /src/unity-scripts/UnityHttpServer.cs: -------------------------------------------------------------------------------- ```csharp 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Text; 7 | using System.Threading; 8 | using UnityEngine; 9 | using UnityEditor; 10 | using Newtonsoft.Json; 11 | using Newtonsoft.Json.Linq; 12 | 13 | namespace UnityMCP 14 | { 15 | [InitializeOnLoad] 16 | public static class UnityMCPInstaller 17 | { 18 | static UnityMCPInstaller() 19 | { 20 | CheckAndUpdateScripts(); 21 | } 22 | 23 | static void CheckAndUpdateScripts() 24 | { 25 | var installedVersion = EditorPrefs.GetString(UnityHttpServer.VERSION_META_KEY, "0.0.0"); 26 | if (installedVersion != UnityHttpServer.SCRIPT_VERSION) 27 | { 28 | Debug.Log($"[UnityMCP] Updating Unity MCP scripts from version {installedVersion} to {UnityHttpServer.SCRIPT_VERSION}"); 29 | // Version update logic will be handled by the MCP server 30 | EditorPrefs.SetString(UnityHttpServer.VERSION_META_KEY, UnityHttpServer.SCRIPT_VERSION); 31 | } 32 | } 33 | } 34 | /// <summary> 35 | /// Simple HTTP server for Unity MCP integration 36 | /// </summary> 37 | public static class UnityHttpServer 38 | { 39 | // Version information for auto-update 40 | public const string SCRIPT_VERSION = "1.1.0"; 41 | public const string VERSION_META_KEY = "UnityMCP.InstalledVersion"; 42 | 43 | // Configuration constants 44 | private const int DEFAULT_PORT = 23457; 45 | private const int REQUEST_TIMEOUT_MS = 120000; // 2 minutes 46 | private const int THREAD_JOIN_TIMEOUT_MS = 1000; // 1 second 47 | private const int ASSET_REFRESH_DELAY_MS = 500; // Wait after asset operations 48 | public const string SERVER_LOG_PREFIX = "[UnityMCP]"; 49 | private const string PREFS_PORT_KEY = "UnityMCP.ServerPort"; 50 | private const string PREFS_PORT_BEFORE_PLAY_KEY = "UnityMCP.ServerPortBeforePlay"; 51 | 52 | // File path constants 53 | private const string ASSETS_PREFIX = "Assets/"; 54 | private const int ASSETS_PREFIX_LENGTH = 7; 55 | private const string DEFAULT_SCRIPTS_FOLDER = "Assets/Scripts"; 56 | private const string DEFAULT_SHADERS_FOLDER = "Assets/Shaders"; 57 | private const string CS_EXTENSION = ".cs"; 58 | private const string SHADER_EXTENSION = ".shader"; 59 | 60 | private static HttpListener httpListener; 61 | private static Thread listenerThread; 62 | private static bool isRunning = false; 63 | 64 | // Request queue for serialization 65 | private static readonly Queue<Action> requestQueue = new Queue<Action>(); 66 | private static bool isProcessingRequest = false; 67 | private static int currentPort = DEFAULT_PORT; 68 | 69 | /// <summary> 70 | /// Gets whether the server is currently running 71 | /// </summary> 72 | public static bool IsRunning => isRunning; 73 | 74 | /// <summary> 75 | /// Gets the current port the server is running on 76 | /// </summary> 77 | public static int CurrentPort => currentPort; 78 | 79 | [InitializeOnLoad] 80 | static class AutoShutdown 81 | { 82 | static AutoShutdown() 83 | { 84 | EditorApplication.playModeStateChanged += OnPlayModeChanged; 85 | EditorApplication.quitting += Shutdown; 86 | 87 | // Handle script recompilation 88 | UnityEditor.Compilation.CompilationPipeline.compilationStarted += OnCompilationStarted; 89 | UnityEditor.Compilation.CompilationPipeline.compilationFinished += OnCompilationFinished; 90 | 91 | // Auto-start server on Unity startup 92 | EditorApplication.delayCall += () => { 93 | if (!isRunning) 94 | { 95 | var savedPort = EditorPrefs.GetInt(PREFS_PORT_KEY, DEFAULT_PORT); 96 | Debug.Log($"{SERVER_LOG_PREFIX} Auto-starting server on port {savedPort}"); 97 | Start(savedPort); 98 | } 99 | }; 100 | } 101 | 102 | static void OnCompilationStarted(object obj) 103 | { 104 | Debug.Log($"{SERVER_LOG_PREFIX} Compilation started - stopping server"); 105 | if (isRunning) 106 | { 107 | Shutdown(); 108 | } 109 | } 110 | 111 | static void OnCompilationFinished(object obj) 112 | { 113 | Debug.Log($"{SERVER_LOG_PREFIX} Compilation finished - auto-restarting server"); 114 | // Always auto-restart after compilation 115 | var savedPort = EditorPrefs.GetInt(PREFS_PORT_KEY, DEFAULT_PORT); 116 | EditorApplication.delayCall += () => Start(savedPort); 117 | } 118 | } 119 | 120 | /// <summary> 121 | /// Start the HTTP server on the specified port 122 | /// </summary> 123 | /// <param name="port">Port to listen on</param> 124 | public static void Start(int port = DEFAULT_PORT) 125 | { 126 | if (isRunning) 127 | { 128 | Debug.LogWarning($"{SERVER_LOG_PREFIX} Server is already running. Stop it first."); 129 | return; 130 | } 131 | 132 | currentPort = port; 133 | 134 | try 135 | { 136 | httpListener = new HttpListener(); 137 | httpListener.Prefixes.Add($"http://localhost:{currentPort}/"); 138 | httpListener.Start(); 139 | isRunning = true; 140 | 141 | listenerThread = new Thread(ListenLoop) 142 | { 143 | IsBackground = true, 144 | Name = "UnityMCPHttpListener" 145 | }; 146 | listenerThread.Start(); 147 | 148 | Debug.Log($"{SERVER_LOG_PREFIX} HTTP Server started on port {currentPort}"); 149 | } 150 | catch (Exception e) 151 | { 152 | isRunning = false; 153 | Debug.LogError($"{SERVER_LOG_PREFIX} Failed to start HTTP server: {e.Message}"); 154 | throw; 155 | } 156 | } 157 | 158 | /// <summary> 159 | /// Stop the HTTP server 160 | /// </summary> 161 | public static void Shutdown() 162 | { 163 | if (!isRunning) 164 | { 165 | Debug.LogWarning($"{SERVER_LOG_PREFIX} Server is not running."); 166 | return; 167 | } 168 | 169 | isRunning = false; 170 | 171 | try 172 | { 173 | httpListener?.Stop(); 174 | httpListener?.Close(); 175 | listenerThread?.Join(THREAD_JOIN_TIMEOUT_MS); 176 | Debug.Log($"{SERVER_LOG_PREFIX} HTTP Server stopped"); 177 | } 178 | catch (Exception e) 179 | { 180 | Debug.LogError($"{SERVER_LOG_PREFIX} Error during shutdown: {e.Message}"); 181 | } 182 | finally 183 | { 184 | httpListener = null; 185 | listenerThread = null; 186 | } 187 | } 188 | 189 | static void OnPlayModeChanged(PlayModeStateChange state) 190 | { 191 | // Stop server when entering play mode to avoid conflicts 192 | if (state == PlayModeStateChange.ExitingEditMode) 193 | { 194 | if (isRunning) 195 | { 196 | Debug.Log($"{SERVER_LOG_PREFIX} Stopping server due to play mode change"); 197 | EditorPrefs.SetInt(PREFS_PORT_BEFORE_PLAY_KEY, currentPort); 198 | Shutdown(); 199 | } 200 | } 201 | // Restart server when returning to edit mode 202 | else if (state == PlayModeStateChange.EnteredEditMode) 203 | { 204 | var savedPort = EditorPrefs.GetInt(PREFS_PORT_BEFORE_PLAY_KEY, DEFAULT_PORT); 205 | Debug.Log($"{SERVER_LOG_PREFIX} Restarting server after play mode on port {savedPort}"); 206 | EditorApplication.delayCall += () => Start(savedPort); 207 | } 208 | } 209 | 210 | static void ListenLoop() 211 | { 212 | while (isRunning) 213 | { 214 | try 215 | { 216 | var context = httpListener.GetContext(); 217 | ThreadPool.QueueUserWorkItem(_ => HandleRequest(context)); 218 | } 219 | catch (Exception e) 220 | { 221 | if (isRunning) 222 | Debug.LogError($"{SERVER_LOG_PREFIX} Listen error: {e.Message}"); 223 | } 224 | } 225 | } 226 | 227 | static void HandleRequest(HttpListenerContext context) 228 | { 229 | var request = context.Request; 230 | var response = context.Response; 231 | response.Headers.Add("Access-Control-Allow-Origin", "*"); 232 | 233 | try 234 | { 235 | if (request.HttpMethod != "POST") 236 | { 237 | SendResponse(response, 405, false, null, "Method not allowed"); 238 | return; 239 | } 240 | 241 | string requestBody; 242 | // Force UTF-8 encoding for request body 243 | using (var reader = new StreamReader(request.InputStream, Encoding.UTF8)) 244 | { 245 | requestBody = reader.ReadToEnd(); 246 | } 247 | 248 | var requestData = JObject.Parse(requestBody); 249 | var method = requestData["method"]?.ToString(); 250 | 251 | if (string.IsNullOrEmpty(method)) 252 | { 253 | SendResponse(response, 400, false, null, "Method is required"); 254 | return; 255 | } 256 | 257 | Debug.Log($"{SERVER_LOG_PREFIX} Processing request: {method}"); 258 | 259 | // Check if this request requires main thread 260 | bool requiresMainThread = RequiresMainThread(method); 261 | 262 | if (!requiresMainThread) 263 | { 264 | // Process directly on worker thread 265 | try 266 | { 267 | var result = ProcessRequestOnWorkerThread(method, requestData); 268 | SendResponse(response, 200, true, result, null); 269 | } 270 | catch (Exception e) 271 | { 272 | var statusCode = e is ArgumentException ? 400 : 500; 273 | SendResponse(response, statusCode, false, null, e.Message); 274 | } 275 | } 276 | else 277 | { 278 | // Execute on main thread for Unity API calls 279 | object result = null; 280 | Exception error = null; 281 | var resetEvent = new ManualResetEvent(false); 282 | 283 | EditorApplication.delayCall += () => 284 | { 285 | try 286 | { 287 | Debug.Log($"{SERVER_LOG_PREFIX} Processing on main thread: {method}"); 288 | result = ProcessRequest(method, requestData); 289 | Debug.Log($"{SERVER_LOG_PREFIX} Completed processing: {method}"); 290 | } 291 | catch (Exception e) 292 | { 293 | error = e; 294 | Debug.LogError($"{SERVER_LOG_PREFIX} Error processing {method}: {e.Message}"); 295 | } 296 | finally 297 | { 298 | resetEvent.Set(); 299 | } 300 | }; 301 | 302 | if (!resetEvent.WaitOne(REQUEST_TIMEOUT_MS)) 303 | { 304 | SendResponse(response, 504, false, null, "Request timeout - Unity may be busy or unfocused"); 305 | return; 306 | } 307 | 308 | if (error != null) 309 | { 310 | var statusCode = error is ArgumentException ? 400 : 500; 311 | SendResponse(response, statusCode, false, null, error.Message); 312 | return; 313 | } 314 | 315 | SendResponse(response, 200, true, result, null); 316 | } 317 | } 318 | catch (Exception e) 319 | { 320 | SendResponse(response, 400, false, null, $"Bad request: {e.Message}"); 321 | } 322 | } 323 | 324 | static bool RequiresMainThread(string method) 325 | { 326 | // These methods can run on worker thread 327 | switch (method) 328 | { 329 | case "ping": 330 | case "script/read": 331 | case "shader/read": 332 | return false; 333 | 334 | // project/info now requires Unity API for render pipeline detection 335 | // Creating, deleting files require Unity API (AssetDatabase) 336 | default: 337 | return true; 338 | } 339 | } 340 | 341 | static object ProcessRequestOnWorkerThread(string method, JObject request) 342 | { 343 | switch (method) 344 | { 345 | case "ping": 346 | return new { status = "ok", time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; 347 | 348 | case "project/info": 349 | // project/info requires Unity API for render pipeline detection 350 | throw new NotImplementedException("project/info requires main thread for render pipeline detection"); 351 | 352 | case "script/read": 353 | return ReadScriptOnWorkerThread(request); 354 | 355 | case "shader/read": 356 | return ReadShaderOnWorkerThread(request); 357 | 358 | // Folder operations (can run on worker thread) 359 | case "folder/create": 360 | return CreateFolderOnWorkerThread(request); 361 | case "folder/rename": 362 | return RenameFolderOnWorkerThread(request); 363 | case "folder/move": 364 | return MoveFolderOnWorkerThread(request); 365 | case "folder/delete": 366 | return DeleteFolderOnWorkerThread(request); 367 | case "folder/list": 368 | return ListFolderOnWorkerThread(request); 369 | 370 | default: 371 | throw new NotImplementedException($"Method not implemented for worker thread: {method}"); 372 | } 373 | } 374 | 375 | static object ReadScriptOnWorkerThread(JObject request) 376 | { 377 | var path = request["path"]?.ToString(); 378 | if (string.IsNullOrEmpty(path)) 379 | throw new ArgumentException("path is required"); 380 | 381 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 382 | if (!File.Exists(fullPath)) 383 | throw new FileNotFoundException($"File not found: {path}"); 384 | 385 | return new 386 | { 387 | path = path, 388 | content = File.ReadAllText(fullPath, new UTF8Encoding(true)), 389 | guid = "" // GUID requires AssetDatabase, skip in worker thread 390 | }; 391 | } 392 | 393 | static object ReadShaderOnWorkerThread(JObject request) 394 | { 395 | var path = request["path"]?.ToString(); 396 | if (string.IsNullOrEmpty(path)) 397 | throw new ArgumentException("path is required"); 398 | 399 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 400 | if (!File.Exists(fullPath)) 401 | throw new FileNotFoundException($"File not found: {path}"); 402 | 403 | return new 404 | { 405 | path = path, 406 | content = File.ReadAllText(fullPath, new UTF8Encoding(true)), 407 | guid = "" // GUID requires AssetDatabase, skip in worker thread 408 | }; 409 | } 410 | 411 | static object ProcessRequest(string method, JObject request) 412 | { 413 | switch (method) 414 | { 415 | case "ping": 416 | return new { status = "ok", time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; 417 | 418 | // Script operations 419 | case "script/create": 420 | return CreateScript(request); 421 | case "script/read": 422 | return ReadScript(request); 423 | case "script/delete": 424 | return DeleteScript(request); 425 | case "script/applyDiff": 426 | return ApplyDiff(request); 427 | 428 | // Shader operations 429 | case "shader/create": 430 | return CreateShader(request); 431 | case "shader/read": 432 | return ReadShader(request); 433 | case "shader/delete": 434 | return DeleteShader(request); 435 | 436 | // Project operations 437 | case "project/info": 438 | return GetProjectInfo(); 439 | 440 | // Folder operations 441 | case "folder/create": 442 | return CreateFolder(request); 443 | case "folder/rename": 444 | return RenameFolder(request); 445 | case "folder/move": 446 | return MoveFolder(request); 447 | case "folder/delete": 448 | return DeleteFolder(request); 449 | case "folder/list": 450 | return ListFolder(request); 451 | 452 | default: 453 | throw new NotImplementedException($"Method not found: {method}"); 454 | } 455 | } 456 | 457 | static object CreateScript(JObject request) 458 | { 459 | var fileName = request["fileName"]?.ToString(); 460 | if (string.IsNullOrEmpty(fileName)) 461 | throw new ArgumentException("fileName is required"); 462 | 463 | if (!fileName.EndsWith(CS_EXTENSION)) 464 | fileName += CS_EXTENSION; 465 | 466 | var content = request["content"]?.ToString(); 467 | var folder = request["folder"]?.ToString() ?? DEFAULT_SCRIPTS_FOLDER; 468 | 469 | var path = Path.Combine(folder, fileName); 470 | var directory = Path.GetDirectoryName(path); 471 | 472 | // Create directory if needed 473 | if (!AssetDatabase.IsValidFolder(directory)) 474 | { 475 | CreateFolderRecursive(directory); 476 | } 477 | 478 | // Use Unity-safe file creation approach 479 | var scriptContent = content ?? GetDefaultScriptContent(fileName); 480 | 481 | // First, ensure the asset doesn't already exist 482 | if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null) 483 | { 484 | throw new InvalidOperationException($"Asset already exists: {path}"); 485 | } 486 | 487 | // Write file using UTF-8 with BOM (Unity standard) 488 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 489 | var utf8WithBom = new UTF8Encoding(true); 490 | File.WriteAllText(fullPath, scriptContent, utf8WithBom); 491 | 492 | // Import the asset immediately and wait for completion 493 | AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); 494 | 495 | // Verify the asset was imported successfully 496 | var attempts = 0; 497 | const int maxAttempts = 10; 498 | while (AssetDatabase.AssetPathToGUID(path) == "" && attempts < maxAttempts) 499 | { 500 | System.Threading.Thread.Sleep(100); 501 | AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); 502 | attempts++; 503 | } 504 | 505 | if (AssetDatabase.AssetPathToGUID(path) == "") 506 | { 507 | throw new InvalidOperationException($"Failed to import asset: {path}"); 508 | } 509 | 510 | return new 511 | { 512 | path = path, 513 | guid = AssetDatabase.AssetPathToGUID(path) 514 | }; 515 | } 516 | 517 | static object ReadScript(JObject request) 518 | { 519 | var path = request["path"]?.ToString(); 520 | if (string.IsNullOrEmpty(path)) 521 | throw new ArgumentException("path is required"); 522 | 523 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 524 | if (!File.Exists(fullPath)) 525 | throw new FileNotFoundException($"File not found: {path}"); 526 | 527 | return new 528 | { 529 | path = path, 530 | content = File.ReadAllText(fullPath, new UTF8Encoding(true)), 531 | guid = AssetDatabase.AssetPathToGUID(path) 532 | }; 533 | } 534 | 535 | static object DeleteScript(JObject request) 536 | { 537 | var path = request["path"]?.ToString(); 538 | if (string.IsNullOrEmpty(path)) 539 | throw new ArgumentException("path is required"); 540 | 541 | // Verify file exists before deletion 542 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 543 | if (!File.Exists(fullPath)) 544 | throw new FileNotFoundException($"File not found: {path}"); 545 | 546 | // Delete using AssetDatabase 547 | if (!AssetDatabase.DeleteAsset(path)) 548 | throw new InvalidOperationException($"Failed to delete: {path}"); 549 | 550 | // Force immediate refresh 551 | AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); 552 | 553 | // Wait for asset database to process deletion 554 | System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); 555 | 556 | return new { message = "Script deleted successfully" }; 557 | } 558 | 559 | static object ApplyDiff(JObject request) 560 | { 561 | var path = request["path"]?.ToString(); 562 | var diff = request["diff"]?.ToString(); 563 | var options = request["options"] as JObject; 564 | 565 | if (string.IsNullOrEmpty(path)) 566 | throw new ArgumentException("path is required"); 567 | if (string.IsNullOrEmpty(diff)) 568 | throw new ArgumentException("diff is required"); 569 | 570 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 571 | if (!File.Exists(fullPath)) 572 | throw new FileNotFoundException($"File not found: {path}"); 573 | 574 | var dryRun = options?["dryRun"]?.Value<bool>() ?? false; 575 | 576 | // Read current content using UTF-8 with BOM (Unity standard) 577 | var utf8WithBom = new UTF8Encoding(true); 578 | var originalContent = File.ReadAllText(fullPath, utf8WithBom); 579 | var lines = originalContent.Split('\n').ToList(); 580 | 581 | // Parse and apply unified diff 582 | var diffLines = diff.Split('\n'); 583 | var linesAdded = 0; 584 | var linesRemoved = 0; 585 | var currentLine = 0; 586 | 587 | for (int i = 0; i < diffLines.Length; i++) 588 | { 589 | var line = diffLines[i]; 590 | if (line.StartsWith("@@")) 591 | { 592 | // Parse hunk header: @@ -l,s +l,s @@ 593 | var match = System.Text.RegularExpressions.Regex.Match(line, @"@@ -(\d+),?\d* \+(\d+),?\d* @@"); 594 | if (match.Success) 595 | { 596 | currentLine = int.Parse(match.Groups[1].Value) - 1; 597 | } 598 | } 599 | else if (line.StartsWith("-") && !line.StartsWith("---")) 600 | { 601 | // Remove line 602 | if (currentLine < lines.Count) 603 | { 604 | lines.RemoveAt(currentLine); 605 | linesRemoved++; 606 | } 607 | } 608 | else if (line.StartsWith("+") && !line.StartsWith("+++")) 609 | { 610 | // Add line 611 | lines.Insert(currentLine, line.Substring(1)); 612 | currentLine++; 613 | linesAdded++; 614 | } 615 | else if (line.StartsWith(" ")) 616 | { 617 | // Context line 618 | currentLine++; 619 | } 620 | } 621 | 622 | // Write result if not dry run 623 | if (!dryRun) 624 | { 625 | var updatedContent = string.Join("\n", lines); 626 | // Write with UTF-8 with BOM (Unity standard) 627 | File.WriteAllText(fullPath, updatedContent, utf8WithBom); 628 | AssetDatabase.Refresh(); 629 | 630 | // Wait for asset database to process 631 | System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); 632 | } 633 | 634 | return new 635 | { 636 | path = path, 637 | linesAdded = linesAdded, 638 | linesRemoved = linesRemoved, 639 | dryRun = dryRun, 640 | guid = AssetDatabase.AssetPathToGUID(path) 641 | }; 642 | } 643 | 644 | static object CreateShader(JObject request) 645 | { 646 | var name = request["name"]?.ToString(); 647 | if (string.IsNullOrEmpty(name)) 648 | throw new ArgumentException("name is required"); 649 | 650 | if (!name.EndsWith(SHADER_EXTENSION)) 651 | name += SHADER_EXTENSION; 652 | 653 | var content = request["content"]?.ToString(); 654 | var folder = request["folder"]?.ToString() ?? DEFAULT_SHADERS_FOLDER; 655 | 656 | var path = Path.Combine(folder, name); 657 | var directory = Path.GetDirectoryName(path); 658 | 659 | if (!AssetDatabase.IsValidFolder(directory)) 660 | { 661 | CreateFolderRecursive(directory); 662 | } 663 | 664 | // Use Unity-safe file creation approach 665 | var shaderContent = content ?? GetDefaultShaderContent(name); 666 | 667 | // First, ensure the asset doesn't already exist 668 | if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null) 669 | { 670 | throw new InvalidOperationException($"Asset already exists: {path}"); 671 | } 672 | 673 | // Write file using UTF-8 with BOM (Unity standard) 674 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 675 | var utf8WithBom = new UTF8Encoding(true); 676 | File.WriteAllText(fullPath, shaderContent, utf8WithBom); 677 | 678 | // Import the asset immediately and wait for completion 679 | AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate); 680 | 681 | // Verify the asset was imported successfully 682 | var attempts = 0; 683 | const int maxAttempts = 10; 684 | while (AssetDatabase.AssetPathToGUID(path) == "" && attempts < maxAttempts) 685 | { 686 | System.Threading.Thread.Sleep(100); 687 | AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); 688 | attempts++; 689 | } 690 | 691 | if (AssetDatabase.AssetPathToGUID(path) == "") 692 | { 693 | throw new InvalidOperationException($"Failed to import asset: {path}"); 694 | } 695 | 696 | return new 697 | { 698 | path = path, 699 | guid = AssetDatabase.AssetPathToGUID(path) 700 | }; 701 | } 702 | 703 | static object ReadShader(JObject request) 704 | { 705 | var path = request["path"]?.ToString(); 706 | if (string.IsNullOrEmpty(path)) 707 | throw new ArgumentException("path is required"); 708 | 709 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 710 | if (!File.Exists(fullPath)) 711 | throw new FileNotFoundException($"File not found: {path}"); 712 | 713 | return new 714 | { 715 | path = path, 716 | content = File.ReadAllText(fullPath, new UTF8Encoding(true)), 717 | guid = AssetDatabase.AssetPathToGUID(path) 718 | }; 719 | } 720 | 721 | static object DeleteShader(JObject request) 722 | { 723 | var path = request["path"]?.ToString(); 724 | if (string.IsNullOrEmpty(path)) 725 | throw new ArgumentException("path is required"); 726 | 727 | if (!AssetDatabase.DeleteAsset(path)) 728 | throw new InvalidOperationException($"Failed to delete: {path}"); 729 | 730 | // Wait for asset database to process deletion 731 | System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); 732 | 733 | return new { message = "Shader deleted successfully" }; 734 | } 735 | 736 | static object GetProjectInfo() 737 | { 738 | // Detect render pipeline with multiple methods 739 | string renderPipeline = "Built-in"; 740 | string renderPipelineVersion = "N/A"; 741 | string detectionMethod = "Default"; 742 | 743 | try 744 | { 745 | // Method 1: Check GraphicsSettings.renderPipelineAsset 746 | var renderPipelineAsset = UnityEngine.Rendering.GraphicsSettings.renderPipelineAsset; 747 | Debug.Log($"{SERVER_LOG_PREFIX} RenderPipelineAsset: {(renderPipelineAsset != null ? renderPipelineAsset.GetType().FullName : "null")}"); 748 | 749 | if (renderPipelineAsset != null) 750 | { 751 | var assetType = renderPipelineAsset.GetType(); 752 | var typeName = assetType.Name; 753 | var fullTypeName = assetType.FullName; 754 | 755 | Debug.Log($"{SERVER_LOG_PREFIX} Asset type: {typeName}, Full type: {fullTypeName}"); 756 | 757 | if (fullTypeName.Contains("Universal") || typeName.Contains("Universal") || 758 | fullTypeName.Contains("URP") || typeName.Contains("URP")) 759 | { 760 | renderPipeline = "URP"; 761 | detectionMethod = "GraphicsSettings.renderPipelineAsset"; 762 | } 763 | else if (fullTypeName.Contains("HighDefinition") || typeName.Contains("HighDefinition") || 764 | fullTypeName.Contains("HDRP") || typeName.Contains("HDRP")) 765 | { 766 | renderPipeline = "HDRP"; 767 | detectionMethod = "GraphicsSettings.renderPipelineAsset"; 768 | } 769 | else 770 | { 771 | renderPipeline = $"Custom ({typeName})"; 772 | detectionMethod = "GraphicsSettings.renderPipelineAsset"; 773 | } 774 | } 775 | else 776 | { 777 | // Method 2: Check for installed packages if no render pipeline asset 778 | Debug.Log($"{SERVER_LOG_PREFIX} No render pipeline asset found, checking packages..."); 779 | 780 | try 781 | { 782 | var urpPackage = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.universal"); 783 | var hdrpPackage = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.high-definition"); 784 | 785 | if (urpPackage != null) 786 | { 787 | renderPipeline = "URP (Package Available)"; 788 | renderPipelineVersion = urpPackage.version; 789 | detectionMethod = "Package Detection"; 790 | } 791 | else if (hdrpPackage != null) 792 | { 793 | renderPipeline = "HDRP (Package Available)"; 794 | renderPipelineVersion = hdrpPackage.version; 795 | detectionMethod = "Package Detection"; 796 | } 797 | else 798 | { 799 | renderPipeline = "Built-in"; 800 | detectionMethod = "No SRP packages found"; 801 | } 802 | } 803 | catch (System.Exception ex) 804 | { 805 | Debug.LogWarning($"{SERVER_LOG_PREFIX} Package detection failed: {ex.Message}"); 806 | renderPipeline = "Built-in (Package detection failed)"; 807 | detectionMethod = "Package detection error"; 808 | } 809 | } 810 | 811 | // Try to get version info if not already obtained 812 | if (renderPipelineVersion == "N/A" && renderPipeline.StartsWith("URP")) 813 | { 814 | try 815 | { 816 | var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.universal"); 817 | if (packageInfo != null) 818 | { 819 | renderPipelineVersion = packageInfo.version; 820 | } 821 | } 822 | catch (System.Exception ex) 823 | { 824 | Debug.LogWarning($"{SERVER_LOG_PREFIX} URP version detection failed: {ex.Message}"); 825 | renderPipelineVersion = "Version unknown"; 826 | } 827 | } 828 | else if (renderPipelineVersion == "N/A" && renderPipeline.StartsWith("HDRP")) 829 | { 830 | try 831 | { 832 | var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.render-pipelines.high-definition"); 833 | if (packageInfo != null) 834 | { 835 | renderPipelineVersion = packageInfo.version; 836 | } 837 | } 838 | catch (System.Exception ex) 839 | { 840 | Debug.LogWarning($"{SERVER_LOG_PREFIX} HDRP version detection failed: {ex.Message}"); 841 | renderPipelineVersion = "Version unknown"; 842 | } 843 | } 844 | 845 | Debug.Log($"{SERVER_LOG_PREFIX} Detected render pipeline: {renderPipeline} (v{renderPipelineVersion}) via {detectionMethod}"); 846 | } 847 | catch (System.Exception ex) 848 | { 849 | Debug.LogError($"{SERVER_LOG_PREFIX} Render pipeline detection failed: {ex.Message}"); 850 | renderPipeline = "Detection Failed"; 851 | detectionMethod = "Exception occurred"; 852 | } 853 | 854 | return new 855 | { 856 | projectPath = Application.dataPath.Replace("/Assets", ""), 857 | projectName = Application.productName, 858 | unityVersion = Application.unityVersion, 859 | platform = Application.platform.ToString(), 860 | isPlaying = Application.isPlaying, 861 | renderPipeline = renderPipeline, 862 | renderPipelineVersion = renderPipelineVersion, 863 | detectionMethod = detectionMethod 864 | }; 865 | } 866 | 867 | static void CreateFolderRecursive(string path) 868 | { 869 | var folders = path.Split('/'); 870 | var currentPath = folders[0]; 871 | 872 | for (int i = 1; i < folders.Length; i++) 873 | { 874 | var newPath = currentPath + "/" + folders[i]; 875 | if (!AssetDatabase.IsValidFolder(newPath)) 876 | { 877 | AssetDatabase.CreateFolder(currentPath, folders[i]); 878 | } 879 | currentPath = newPath; 880 | } 881 | } 882 | 883 | static string GetDefaultScriptContent(string fileName) 884 | { 885 | var className = Path.GetFileNameWithoutExtension(fileName); 886 | return "using UnityEngine;\n\n" + 887 | $"public class {className} : MonoBehaviour\n" + 888 | "{\n" + 889 | " void Start()\n" + 890 | " {\n" + 891 | " \n" + 892 | " }\n" + 893 | " \n" + 894 | " void Update()\n" + 895 | " {\n" + 896 | " \n" + 897 | " }\n" + 898 | "}"; 899 | } 900 | 901 | static string GetDefaultShaderContent(string fileName) 902 | { 903 | var shaderName = Path.GetFileNameWithoutExtension(fileName); 904 | return $"Shader \"Custom/{shaderName}\"\n" + 905 | "{\n" + 906 | " Properties\n" + 907 | " {\n" + 908 | " _MainTex (\"Texture\", 2D) = \"white\" {}\n" + 909 | " }\n" + 910 | " SubShader\n" + 911 | " {\n" + 912 | " Tags { \"RenderType\"=\"Opaque\" }\n" + 913 | " LOD 200\n" + 914 | "\n" + 915 | " CGPROGRAM\n" + 916 | " #pragma surface surf Standard fullforwardshadows\n" + 917 | "\n" + 918 | " sampler2D _MainTex;\n" + 919 | "\n" + 920 | " struct Input\n" + 921 | " {\n" + 922 | " float2 uv_MainTex;\n" + 923 | " };\n" + 924 | "\n" + 925 | " void surf (Input IN, inout SurfaceOutputStandard o)\n" + 926 | " {\n" + 927 | " fixed4 c = tex2D (_MainTex, IN.uv_MainTex);\n" + 928 | " o.Albedo = c.rgb;\n" + 929 | " o.Alpha = c.a;\n" + 930 | " }\n" + 931 | " ENDCG\n" + 932 | " }\n" + 933 | " FallBack \"Diffuse\"\n" + 934 | "}"; 935 | } 936 | 937 | // Folder operations 938 | static object CreateFolder(JObject request) 939 | { 940 | var path = request["path"]?.ToString(); 941 | if (string.IsNullOrEmpty(path)) 942 | throw new ArgumentException("path is required"); 943 | 944 | if (!path.StartsWith(ASSETS_PREFIX)) 945 | path = Path.Combine(DEFAULT_SCRIPTS_FOLDER, path); 946 | 947 | // Use Unity-safe folder creation 948 | if (AssetDatabase.IsValidFolder(path)) 949 | { 950 | throw new InvalidOperationException($"Folder already exists: {path}"); 951 | } 952 | 953 | // Create directory structure properly 954 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 955 | Directory.CreateDirectory(fullPath); 956 | 957 | // Import the folder immediately 958 | AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport); 959 | 960 | // Verify the folder was imported successfully 961 | var attempts = 0; 962 | const int maxAttempts = 10; 963 | while (!AssetDatabase.IsValidFolder(path) && attempts < maxAttempts) 964 | { 965 | System.Threading.Thread.Sleep(100); 966 | AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); 967 | attempts++; 968 | } 969 | 970 | if (!AssetDatabase.IsValidFolder(path)) 971 | { 972 | throw new InvalidOperationException($"Failed to import folder: {path}"); 973 | } 974 | 975 | return new 976 | { 977 | path = path, 978 | guid = AssetDatabase.AssetPathToGUID(path) 979 | }; 980 | } 981 | 982 | static object CreateFolderOnWorkerThread(JObject request) 983 | { 984 | var path = request["path"]?.ToString(); 985 | if (string.IsNullOrEmpty(path)) 986 | throw new ArgumentException("path is required"); 987 | 988 | if (!path.StartsWith(ASSETS_PREFIX)) 989 | path = Path.Combine(DEFAULT_SCRIPTS_FOLDER, path); 990 | 991 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 992 | Directory.CreateDirectory(fullPath); 993 | 994 | return new 995 | { 996 | path = path, 997 | guid = "" // GUID requires AssetDatabase 998 | }; 999 | } 1000 | 1001 | static object RenameFolder(JObject request) 1002 | { 1003 | var oldPath = request["oldPath"]?.ToString(); 1004 | var newName = request["newName"]?.ToString(); 1005 | 1006 | if (string.IsNullOrEmpty(oldPath)) 1007 | throw new ArgumentException("oldPath is required"); 1008 | if (string.IsNullOrEmpty(newName)) 1009 | throw new ArgumentException("newName is required"); 1010 | 1011 | var error = AssetDatabase.RenameAsset(oldPath, newName); 1012 | if (!string.IsNullOrEmpty(error)) 1013 | throw new InvalidOperationException(error); 1014 | 1015 | // Wait for asset database to process 1016 | System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); 1017 | 1018 | var newPath = Path.Combine(Path.GetDirectoryName(oldPath), newName); 1019 | return new 1020 | { 1021 | oldPath = oldPath, 1022 | newPath = newPath, 1023 | guid = AssetDatabase.AssetPathToGUID(newPath) 1024 | }; 1025 | } 1026 | 1027 | static object RenameFolderOnWorkerThread(JObject request) 1028 | { 1029 | var oldPath = request["oldPath"]?.ToString(); 1030 | var newName = request["newName"]?.ToString(); 1031 | 1032 | if (string.IsNullOrEmpty(oldPath)) 1033 | throw new ArgumentException("oldPath is required"); 1034 | if (string.IsNullOrEmpty(newName)) 1035 | throw new ArgumentException("newName is required"); 1036 | 1037 | var oldFullPath = Path.Combine(Application.dataPath, oldPath.Substring(ASSETS_PREFIX_LENGTH)); 1038 | var parentDir = Path.GetDirectoryName(oldFullPath); 1039 | var newFullPath = Path.Combine(parentDir, newName); 1040 | 1041 | if (!Directory.Exists(oldFullPath)) 1042 | throw new DirectoryNotFoundException($"Directory not found: {oldPath}"); 1043 | 1044 | Directory.Move(oldFullPath, newFullPath); 1045 | 1046 | var newPath = Path.Combine(Path.GetDirectoryName(oldPath), newName); 1047 | return new 1048 | { 1049 | oldPath = oldPath, 1050 | newPath = newPath, 1051 | guid = "" // GUID requires AssetDatabase 1052 | }; 1053 | } 1054 | 1055 | static object MoveFolder(JObject request) 1056 | { 1057 | var sourcePath = request["sourcePath"]?.ToString(); 1058 | var targetPath = request["targetPath"]?.ToString(); 1059 | 1060 | if (string.IsNullOrEmpty(sourcePath)) 1061 | throw new ArgumentException("sourcePath is required"); 1062 | if (string.IsNullOrEmpty(targetPath)) 1063 | throw new ArgumentException("targetPath is required"); 1064 | 1065 | var error = AssetDatabase.MoveAsset(sourcePath, targetPath); 1066 | if (!string.IsNullOrEmpty(error)) 1067 | throw new InvalidOperationException(error); 1068 | 1069 | // Wait for asset database to process 1070 | System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); 1071 | 1072 | return new 1073 | { 1074 | sourcePath = sourcePath, 1075 | targetPath = targetPath, 1076 | guid = AssetDatabase.AssetPathToGUID(targetPath) 1077 | }; 1078 | } 1079 | 1080 | static object MoveFolderOnWorkerThread(JObject request) 1081 | { 1082 | var sourcePath = request["sourcePath"]?.ToString(); 1083 | var targetPath = request["targetPath"]?.ToString(); 1084 | 1085 | if (string.IsNullOrEmpty(sourcePath)) 1086 | throw new ArgumentException("sourcePath is required"); 1087 | if (string.IsNullOrEmpty(targetPath)) 1088 | throw new ArgumentException("targetPath is required"); 1089 | 1090 | var sourceFullPath = Path.Combine(Application.dataPath, sourcePath.Substring(ASSETS_PREFIX_LENGTH)); 1091 | var targetFullPath = Path.Combine(Application.dataPath, targetPath.Substring(ASSETS_PREFIX_LENGTH)); 1092 | 1093 | if (!Directory.Exists(sourceFullPath)) 1094 | throw new DirectoryNotFoundException($"Directory not found: {sourcePath}"); 1095 | 1096 | // Ensure target parent directory exists 1097 | var targetParent = Path.GetDirectoryName(targetFullPath); 1098 | if (!Directory.Exists(targetParent)) 1099 | Directory.CreateDirectory(targetParent); 1100 | 1101 | Directory.Move(sourceFullPath, targetFullPath); 1102 | 1103 | return new 1104 | { 1105 | sourcePath = sourcePath, 1106 | targetPath = targetPath, 1107 | guid = "" // GUID requires AssetDatabase 1108 | }; 1109 | } 1110 | 1111 | static object DeleteFolder(JObject request) 1112 | { 1113 | var path = request["path"]?.ToString(); 1114 | var recursive = request["recursive"]?.Value<bool>() ?? true; 1115 | 1116 | if (string.IsNullOrEmpty(path)) 1117 | throw new ArgumentException("path is required"); 1118 | 1119 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 1120 | if (!Directory.Exists(fullPath)) 1121 | throw new DirectoryNotFoundException($"Directory not found: {path}"); 1122 | 1123 | if (!AssetDatabase.DeleteAsset(path)) 1124 | throw new InvalidOperationException($"Failed to delete folder: {path}"); 1125 | 1126 | // Wait for asset database to process deletion 1127 | System.Threading.Thread.Sleep(ASSET_REFRESH_DELAY_MS); 1128 | 1129 | return new { path = path }; 1130 | } 1131 | 1132 | static object DeleteFolderOnWorkerThread(JObject request) 1133 | { 1134 | var path = request["path"]?.ToString(); 1135 | var recursive = request["recursive"]?.Value<bool>() ?? true; 1136 | 1137 | if (string.IsNullOrEmpty(path)) 1138 | throw new ArgumentException("path is required"); 1139 | 1140 | var fullPath = Path.Combine(Application.dataPath, path.Substring(ASSETS_PREFIX_LENGTH)); 1141 | if (!Directory.Exists(fullPath)) 1142 | throw new DirectoryNotFoundException($"Directory not found: {path}"); 1143 | 1144 | Directory.Delete(fullPath, recursive); 1145 | 1146 | // Also delete .meta file 1147 | var metaPath = fullPath + ".meta"; 1148 | if (File.Exists(metaPath)) 1149 | File.Delete(metaPath); 1150 | 1151 | return new { path = path }; 1152 | } 1153 | 1154 | static object ListFolder(JObject request) 1155 | { 1156 | var path = request["path"]?.ToString() ?? ASSETS_PREFIX; 1157 | var recursive = request["recursive"]?.Value<bool>() ?? false; 1158 | 1159 | var fullPath = Path.Combine(Application.dataPath, path.StartsWith(ASSETS_PREFIX) ? path.Substring(ASSETS_PREFIX_LENGTH) : path); 1160 | if (!Directory.Exists(fullPath)) 1161 | throw new DirectoryNotFoundException($"Directory not found: {path}"); 1162 | 1163 | var entries = new List<object>(); 1164 | 1165 | // Get directories 1166 | var dirs = Directory.GetDirectories(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); 1167 | foreach (var dir in dirs) 1168 | { 1169 | var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, dir); 1170 | entries.Add(new 1171 | { 1172 | path = relativePath, 1173 | name = Path.GetFileName(dir), 1174 | type = "folder", 1175 | guid = AssetDatabase.AssetPathToGUID(relativePath) 1176 | }); 1177 | } 1178 | 1179 | // Get files 1180 | var files = Directory.GetFiles(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) 1181 | .Where(f => !f.EndsWith(".meta")); 1182 | foreach (var file in files) 1183 | { 1184 | var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, file); 1185 | entries.Add(new 1186 | { 1187 | path = relativePath, 1188 | name = Path.GetFileName(file), 1189 | type = "file", 1190 | extension = Path.GetExtension(file), 1191 | guid = AssetDatabase.AssetPathToGUID(relativePath) 1192 | }); 1193 | } 1194 | 1195 | return new 1196 | { 1197 | path = path, 1198 | entries = entries 1199 | }; 1200 | } 1201 | 1202 | static object ListFolderOnWorkerThread(JObject request) 1203 | { 1204 | var path = request["path"]?.ToString() ?? ASSETS_PREFIX; 1205 | var recursive = request["recursive"]?.Value<bool>() ?? false; 1206 | 1207 | var fullPath = Path.Combine(Application.dataPath, path.StartsWith(ASSETS_PREFIX) ? path.Substring(ASSETS_PREFIX_LENGTH) : path); 1208 | if (!Directory.Exists(fullPath)) 1209 | throw new DirectoryNotFoundException($"Directory not found: {path}"); 1210 | 1211 | var entries = new List<object>(); 1212 | 1213 | // Get directories 1214 | var dirs = Directory.GetDirectories(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); 1215 | foreach (var dir in dirs) 1216 | { 1217 | var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, dir); 1218 | entries.Add(new 1219 | { 1220 | path = relativePath, 1221 | name = Path.GetFileName(dir), 1222 | type = "folder", 1223 | guid = "" // GUID requires AssetDatabase 1224 | }); 1225 | } 1226 | 1227 | // Get files 1228 | var files = Directory.GetFiles(fullPath, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) 1229 | .Where(f => !f.EndsWith(".meta")); 1230 | foreach (var file in files) 1231 | { 1232 | var relativePath = ASSETS_PREFIX + GetRelativePath(Application.dataPath, file); 1233 | entries.Add(new 1234 | { 1235 | path = relativePath, 1236 | name = Path.GetFileName(file), 1237 | type = "file", 1238 | extension = Path.GetExtension(file), 1239 | guid = "" // GUID requires AssetDatabase 1240 | }); 1241 | } 1242 | 1243 | return new 1244 | { 1245 | path = path, 1246 | entries = entries 1247 | }; 1248 | } 1249 | 1250 | static string GetRelativePath(string basePath, string fullPath) 1251 | { 1252 | if (!fullPath.StartsWith(basePath)) 1253 | return fullPath; 1254 | 1255 | var relativePath = fullPath.Substring(basePath.Length); 1256 | if (relativePath.StartsWith(Path.DirectorySeparatorChar.ToString())) 1257 | relativePath = relativePath.Substring(1); 1258 | 1259 | return relativePath.Replace(Path.DirectorySeparatorChar, '/'); 1260 | } 1261 | 1262 | static void SendResponse(HttpListenerResponse response, int statusCode, bool success, object result, string error) 1263 | { 1264 | response.StatusCode = statusCode; 1265 | response.ContentType = "application/json; charset=utf-8"; 1266 | response.ContentEncoding = Encoding.UTF8; 1267 | 1268 | var responseData = new Dictionary<string, object> 1269 | { 1270 | ["success"] = success 1271 | }; 1272 | 1273 | if (result != null) 1274 | responseData["result"] = result; 1275 | 1276 | if (!string.IsNullOrEmpty(error)) 1277 | responseData["error"] = error; 1278 | 1279 | var json = JsonConvert.SerializeObject(responseData); 1280 | var buffer = Encoding.UTF8.GetBytes(json); 1281 | 1282 | response.ContentLength64 = buffer.Length; 1283 | response.OutputStream.Write(buffer, 0, buffer.Length); 1284 | response.Close(); 1285 | } 1286 | } 1287 | } ```