#
tokens: 47840/50000 27/29 files (page 1/3)
lines: on (toggle) GitHub
raw markdown copy reset
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 | }
```
Page 1/3FirstPrevNextLast