#
tokens: 45110/50000 28/29 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 2. Use http://codebase.md/quazaai/unitymcpintegration?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── CHANGELOG.md
├── CHANGELOG.md.meta
├── CODE_OF_CONDUCT.md
├── CODE_OF_CONDUCT.md.meta
├── Editor
│   ├── GamePilot.UnityMCP.asmdef
│   ├── GamePilot.UnityMCP.asmdef.meta
│   ├── MCPCodeExecutor.cs
│   ├── MCPCodeExecutor.cs.meta
│   ├── MCPConnectionManager.cs
│   ├── MCPConnectionManager.cs.meta
│   ├── MCPDataCollector.cs
│   ├── MCPDataCollector.cs.meta
│   ├── MCPLogger.cs
│   ├── MCPLogger.cs.meta
│   ├── MCPManager.cs
│   ├── MCPManager.cs.meta
│   ├── MCPMessageHandler.cs
│   ├── MCPMessageHandler.cs.meta
│   ├── MCPMessageSender.cs
│   ├── MCPMessageSender.cs.meta
│   ├── Models
│   │   ├── MCPEditorState.cs
│   │   ├── MCPEditorState.cs.meta
│   │   ├── MCPSceneInfo.cs
│   │   └── MCPSceneInfo.cs.meta
│   ├── Models.meta
│   ├── UI
│   │   ├── MCPDebugSettings.json
│   │   ├── MCPDebugSettings.json.meta
│   │   ├── MCPDebugWindow.cs
│   │   ├── MCPDebugWindow.cs.meta
│   │   ├── MCPDebugWindow.uss
│   │   ├── MCPDebugWindow.uss.meta
│   │   ├── MCPDebugWindow.uxml
│   │   └── MCPDebugWindow.uxml.meta
│   └── UI.meta
├── Editor.meta
├── LICENSE
├── LICENSE.meta
├── mcpInspector.png
├── mcpInspector.png.meta
├── mcpServer
│   ├── .dockerignore
│   ├── .env.example
│   ├── build
│   │   ├── filesystemTools.js
│   │   ├── filesystemTools.js.meta
│   │   ├── index.js
│   │   ├── index.js.meta
│   │   ├── toolDefinitions.js
│   │   ├── toolDefinitions.js.meta
│   │   ├── types.js
│   │   ├── types.js.meta
│   │   ├── websocketHandler.js
│   │   └── websocketHandler.js.meta
│   ├── build.meta
│   ├── docker-compose.yml
│   ├── docker-compose.yml.meta
│   ├── Dockerfile
│   ├── Dockerfile.meta
│   ├── MCPSummary.md
│   ├── MCPSummary.md.meta
│   ├── node_modules.meta
│   ├── package-lock.json
│   ├── package-lock.json.meta
│   ├── package.json
│   ├── package.json.meta
│   ├── smithery.yaml
│   ├── src
│   │   ├── filesystemTools.ts
│   │   ├── filesystemTools.ts.meta
│   │   ├── index.ts
│   │   ├── index.ts.meta
│   │   ├── toolDefinitions.ts
│   │   ├── toolDefinitions.ts.meta
│   │   ├── types.ts
│   │   ├── types.ts.meta
│   │   ├── websocketHandler.ts
│   │   └── websocketHandler.ts.meta
│   ├── src.meta
│   ├── tsconfig.json
│   └── tsconfig.json.meta
├── mcpServer.meta
├── package.json
├── package.json.meta
├── README.md
└── README.md.meta
```

# Files

--------------------------------------------------------------------------------
/mcpServer/.dockerignore:
--------------------------------------------------------------------------------

```
1 | node_modules
2 | npm-debug.log
3 | .git
4 | .github
5 | .vscode
6 | *.md
7 | build/
8 | .env
9 | 
```

--------------------------------------------------------------------------------
/mcpServer/.env.example:
--------------------------------------------------------------------------------

```
1 | # Unity MCP Server Configuration
2 | 
3 | # WebSocket port for Unity Editor connection
4 | MCP_WEBSOCKET_PORT=5010
5 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | 
 82 | # parcel-bundler cache (https://parceljs.org/)
 83 | .cache
 84 | .parcel-cache
 85 | 
 86 | # Next.js build output
 87 | .next
 88 | out
 89 | 
 90 | # Nuxt.js build / generate output
 91 | .nuxt
 92 | dist
 93 | 
 94 | # Gatsby files
 95 | .cache/
 96 | # Comment in the public line in if your project uses Gatsby and not Next.js
 97 | # https://nextjs.org/blog/next-9-1#public-directory-support
 98 | # public
 99 | 
100 | # vuepress build output
101 | .vuepress/dist
102 | 
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 | 
107 | # vitepress build output
108 | **/.vitepress/dist
109 | 
110 | # vitepress cache directory
111 | **/.vitepress/cache
112 | 
113 | # Docusaurus cache and generated files
114 | .docusaurus
115 | 
116 | # Serverless directories
117 | .serverless/
118 | 
119 | # FuseBox cache
120 | .fusebox/
121 | 
122 | # DynamoDB Local files
123 | .dynamodb/
124 | 
125 | # TernJS port file
126 | .tern-port
127 | 
128 | # Stores VSCode versions used for testing VSCode extensions
129 | .vscode-test
130 | 
131 | # yarn v2
132 | .yarn/cache
133 | .yarn/unplugged
134 | .yarn/build-state.yml
135 | .yarn/install-state.gz
136 | .pnp.*
137 | 
138 | 
139 | 
140 | 
141 | 
142 | 
143 | #unity stuff
144 | # This .gitignore file should be placed at the root of your Unity project directory
145 | #
146 | # Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
147 | #
148 | .utmp/
149 | /[Ll]ibrary/
150 | /[Tt]emp/
151 | /[Oo]bj/
152 | /[Bb]uild/
153 | /[Bb]uilds/
154 | /[Ll]ogs/
155 | /[Uu]ser[Ss]ettings/
156 | 
157 | # MemoryCaptures can get excessive in size.
158 | # They also could contain extremely sensitive data
159 | /[Mm]emoryCaptures/
160 | 
161 | # Recordings can get excessive in size
162 | /[Rr]ecordings/
163 | 
164 | # Uncomment this line if you wish to ignore the asset store tools plugin
165 | # /[Aa]ssets/AssetStoreTools*
166 | 
167 | # Autogenerated Jetbrains Rider plugin
168 | /[Aa]ssets/Plugins/Editor/JetBrains*
169 | 
170 | # Visual Studio cache directory
171 | .vs/
172 | 
173 | # Gradle cache directory
174 | .gradle/
175 | 
176 | # Autogenerated VS/MD/Consulo solution and project files
177 | ExportedObj/
178 | .consulo/
179 | *.csproj
180 | *.unityproj
181 | *.sln
182 | *.suo
183 | *.tmp
184 | *.user
185 | *.userprefs
186 | *.pidb
187 | *.booproj
188 | *.svd
189 | *.pdb
190 | *.mdb
191 | *.opendb
192 | *.VC.db
193 | 
194 | # Unity3D generated meta files
195 | *.pidb.meta
196 | *.pdb.meta
197 | *.mdb.meta
198 | 
199 | # Unity3D generated file on crash reports
200 | sysinfo.txt
201 | 
202 | # Builds
203 | *.apk
204 | *.aab
205 | *.unitypackage
206 | *.unitypackage.meta
207 | *.app
208 | 
209 | # Crashlytics generated file
210 | crashlytics-build.properties
211 | 
212 | # Packed Addressables
213 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
214 | 
215 | # Temporary auto-generated Android Assets
216 | /[Aa]ssets/[Ss]treamingAssets/aa.meta
217 | /[Aa]ssets/[Ss]treamingAssets/aa/*
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # 🚀 Advacned Unity MCP Integration 
  2 | 
  3 | [![MCP](https://badge.mcpx.dev)](https://modelcontextprotocol.io/introduction)
  4 | [![smithery badge](https://smithery.ai/badge/@quazaai/unitymcpintegration)](https://smithery.ai/server/@quazaai/unitymcpintegration)
  5 | [![Unity](https://img.shields.io/badge/Unity-2021.3%2B-green?logo=https://w7.pngwing.com/pngs/426/535/png-transparent-unity-new-logo-tech-companies-thumbnail.png)](https://unity.com)
  6 | [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)](https://nodejs.org)
  7 | [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org)
  8 | [![WebSockets](https://img.shields.io/badge/WebSockets-API-orange)](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
  9 | 
 10 | [![Stars](https://img.shields.io/github/stars/quazaai/UnityMCPIntegration)](https://github.com/quazaai/UnityMCPIntegration/stargazers)
 11 | [![Forks](https://img.shields.io/github/forks/quazaai/UnityMCPIntegration)](https://github.com/quazaai/UnityMCPIntegration/network/members)
 12 | [![License](https://img.shields.io/github/license/quazaai/UnityMCPIntegration)](https://github.com/quazaai/UnityMCPIntegration/blob/main/LICENSE)
 13 | 
 14 | <div align="center">
 15 |   <img src="mcpInspector.png" alt="Unity MCP Inspector" width="400" align="right" style="margin-left: 20px; margin-bottom: 20px;"/>
 16 | </div>
 17 | 
 18 | This package provides a seamless integration between [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) and Unity Editor, allowing AI assistants to understand and interact with your Unity projects in real-time. With this integration, AI assistants can access information about your scene hierarchy, project settings, and execute code directly in the Unity Editor context.
 19 | 
 20 | ## 📚 Features
 21 | - Browse and manipulate project files directly
 22 | - Access real-time information about your Unity project
 23 | - Understand your scene hierarchy and game objects
 24 | - Execute C# code directly in the Unity Editor
 25 | - Monitor logs and errors
 26 | - Control the Editor's play mode
 27 | - Wait For Code Execution
 28 | 
 29 | 
 30 | 
 31 | 
 32 | 
 33 | ## 🚀 Getting Started
 34 | 
 35 | ### Prerequisites
 36 | 
 37 | - Unity 2021.3 or later
 38 | - Node.js 18+ (for running the MCP server)
 39 | 
 40 | ### Installation
 41 | 
 42 | #### 1. Install Unity Package
 43 | 
 44 | You have several options to install the Unity package:
 45 | 
 46 | **Option A: Package Manager (Git URL)**
 47 | 1. Open the Unity Package Manager (`Window > Package Manager`)
 48 | 2. Click the `+` button and select `Add package from git URL...`
 49 | 3. Enter the repository URL: `https://github.com/quazaai/UnityMCPIntegration.git`
 50 | 4. Click `Add`
 51 | 
 52 | **Option B: Import Custom Package**
 53 | 1. Clone this repository or [download it as a unityPackage](https://github.com/quazaai/UnityMCPIntegration/releases)
 54 | 2. In Unity, go to `Assets > Import Package > Custom Package`
 55 | 3. Select the `UnityMCPIntegration.unitypackage` file
 56 | 
 57 | 
 58 | 
 59 | #### 2. Set up the MCP Server
 60 | 
 61 | You have two options to run the MCP server:
 62 | 
 63 | **Option A: Run the server directly**
 64 | 
 65 | 1. Navigate to the `mcpServer (likely <path-to-project>\Library\PackageCache\com.quaza.unitymcp@d2b8f1260bca\mcpServer\)` directory
 66 | 2. Install dependencies:
 67 |    ```
 68 |    npm install
 69 |    ```
 70 | 3. Run the server:
 71 |    ```
 72 |    node build/index.js
 73 |    ```
 74 | 
 75 | **Option B: Add to MCP Host configuration**
 76 | 
 77 | Add the server to your MCP Host configuration for Claude Desktop, Custom Implementation etc
 78 | 
 79 | ```json
 80 | {
 81 |   "mcpServers": {
 82 |     "unity-mcp-server": {
 83 |       "command": "node",
 84 |       "args": [
 85 |         "path-to-project>\\Library\\PackageCache\\com.quaza.unitymcp@d2b8f1260bca\\mcpServer\\mcpServer\\build\\index.js"
 86 |       ],
 87 |       "env": {
 88 |         "MCP_WEBSOCKET_PORT": "5010"
 89 |       }
 90 |     }
 91 |   }
 92 | }
 93 | ```
 94 | ### Demo Video
 95 | [![YouTube](http://i.ytimg.com/vi/GxTlahBXs74/hqdefault.jpg)](https://www.youtube.com/watch?v=GxTlahBXs74)
 96 | 
 97 | ### Installing via Smithery
 98 | 
 99 | To install Unity MCP Integration for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@quazaai/unitymcpintegration):
100 | 
101 | ```bash
102 | npx -y @smithery/cli install @quazaai/unitymcpintegration --client claude
103 | ```
104 | 
105 | ### 🔧 Usage
106 | 
107 | #### Debugging and Monitoring
108 | 
109 | You can open the MCP Debug window in Unity to monitor the connection and test features:
110 | 
111 | 1. Go to `Window > MCP Debug`
112 | 2. Use the debug window to:
113 |    - Check connection status
114 |    - Test code execution
115 |    - View logs
116 |    - Monitor events
117 | 
118 | #### Available Tools
119 | 
120 | The Unity MCP integration provides several tools to AI assistants:
121 | 
122 | ##### Unity Editor Tools
123 | - **get_editor_state**: Get comprehensive information about the Unity project and editor state
124 | - **get_current_scene_info**: Get detailed information about the current scene
125 | - **get_game_objects_info**: Get information about specific GameObjects in the scene
126 | - **execute_editor_command**: Execute C# code directly in the Unity Editor
127 | - **get_logs**: Retrieve and filter Unity console logs
128 | - **verify_connection**: Check if there's an active connection to Unity Editor
129 | 
130 | ##### Filesystem Tools
131 | - **read_file**: Read contents of a file in your Unity project
132 | - **read_multiple_files**: Read multiple files at once
133 | - **write_file**: Create or overwrite a file with new content
134 | - **edit_file**: Make targeted edits to existing files with diff preview
135 | - **list_directory**: Get a listing of files and folders in a directory
136 | - **directory_tree**: Get a hierarchical view of directories and files
137 | - **search_files**: Find files matching a search pattern
138 | - **get_file_info**: Get metadata about a specific file or directory
139 | - **find_assets_by_type**: Find all assets of a specific type (e.g. Material, Prefab)
140 | - **list_scripts**: Get a listing of all C# scripts in the project
141 | 
142 | File paths can be absolute or relative to the Unity project's Assets folder. For example, `"Scenes/MyScene.unity"` refers to `<project>/Assets/Scenes/MyScene.unity`.
143 | 
144 | ## 🛠️ Architecture
145 | 
146 | The integration consists of two main components:
147 | 
148 | 1. **Unity Plugin (C#)**: Resides in the Unity Editor and provides access to Editor APIs
149 | 2. **MCP Server (TypeScript/Node.js)**: Implements the MCP protocol and communicates with the Unity plugin
150 | 
151 | Communication between them happens via WebSocket, transferring JSON messages for commands and data.
152 | 
153 | ## File System Access
154 | 
155 | The Unity MCP integration now includes powerful filesystem tools that allow AI assistants to:
156 | 
157 | - Browse, read, and edit files in your Unity project
158 | - Create new files and directories
159 | - Search for specific files or asset types
160 | - Analyze your project structure
161 | - Make targeted code changes with diff previews
162 | 
163 | All file operations are restricted to the Unity project directory for security. The system intelligently handles both absolute and relative paths, always resolving them relative to your project's Assets folder for convenience.
164 | 
165 | Example usages:
166 | - Get a directory listing: `list_directory(path: "Scenes")`
167 | - Read a script file: `read_file(path: "Scripts/Player.cs")`
168 | - Edit a configuration file: `edit_file(path: "Resources/config.json", edits: [{oldText: "value: 10", newText: "value: 20"}], dryRun: true)`
169 | - Find all materials: `find_assets_by_type(assetType: "Material")`
170 | 
171 | ## 👥 Contributing
172 | 
173 | Contributions are welcome! Here's how you can contribute:
174 | 
175 | 1. Fork the repository
176 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
177 | 3. Make your changes
178 | 4. Commit your changes (`git commit -m 'Add some amazing feature'`)
179 | 5. Push to the branch (`git push origin feature/amazing-feature`)
180 | 6. Open a Pull Request
181 | 
182 | ### Development Setup
183 | 
184 | **Unity Side**:
185 | - Open the project in Unity
186 | - Modify the C# scripts in the `UnityMCPConnection/Editor` directory
187 | 
188 | **Server Side**:
189 | - Navigate to the `mcpServer` directory
190 | - Install dependencies: `npm install`
191 | - Make changes to the TypeScript files in the `src` directory
192 | - Build the server: `npm run build`
193 | - Run the server: `node build/index.js`
194 | 
195 | ## 📄 License
196 | 
197 | This project is licensed under the MIT License - see the LICENSE file for details.
198 | 
199 | ## 📞 Support
200 | 
201 | If you encounter any issues or have questions, please file an issue on the GitHub repository.
202 | 
```

--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------

```markdown
 1 | Contributor Covenant Code of Conduct
 2 | Our Pledge
 3 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
 4 | 
 5 | Our Standards
 6 | Examples of behavior that contributes to creating a positive environment include:
 7 | 
 8 | Using welcoming and inclusive language
 9 | Being respectful of differing viewpoints and experiences
10 | Gracefully accepting constructive criticism
11 | Focusing on what is best for the community
12 | Showing empathy towards other community members
13 | Examples of unacceptable behavior by participants include:
14 | 
15 | The use of sexualized language or imagery and unwelcome sexual attention or advances
16 | Trolling, insulting/derogatory comments, and personal or political attacks
17 | Public or private harassment
18 | Publishing others' private information, such as a physical or electronic address, without explicit permission
19 | Other conduct which could reasonably be considered inappropriate in a professional setting
20 | Our Responsibilities
21 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
22 | 
23 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
24 | 
25 | Scope
26 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
27 | 
28 | Enforcement
29 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at {{ email }}. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
30 | 
31 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
32 | 
33 | Attribution
34 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
```

--------------------------------------------------------------------------------
/Editor/UI/MCPDebugSettings.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "port": 5010,
3 |   "autoReconnect": false,
4 |   "globalLoggingEnabled": false,
5 |   "componentLoggingEnabled": {}
6 | }
```

--------------------------------------------------------------------------------
/mcpServer/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
 1 | version: '3.8'
 2 | 
 3 | services:
 4 |   unity-mcp-server:
 5 |     build: .
 6 |     ports:
 7 |       - "${MCP_WEBSOCKET_PORT:-5010}:${MCP_WEBSOCKET_PORT:-5010}"
 8 |     environment:
 9 |       - MCP_WEBSOCKET_PORT=${MCP_WEBSOCKET_PORT:-5010}
10 |     restart: unless-stopped
11 |     volumes:
12 |       # Optional volume for persisting data if needed
13 |       - ./logs:/app/logs
14 | 
```

--------------------------------------------------------------------------------
/mcpServer/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "NodeNext",
 5 |     "moduleResolution": "NodeNext",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true,
12 |     "resolveJsonModule": true,
13 |     "useUnknownInCatchVariables": true
14 |   },
15 |   "include": ["src/**/*"],
16 |   "exclude": ["node_modules"]
17 | }
```

--------------------------------------------------------------------------------
/mcpServer/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     {}
 8 |   commandFunction:
 9 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
10 |     |-
11 |     (config) => ({
12 |       command: 'node',
13 |       args: ['build/index.js']
14 |     })
15 |   exampleConfig: {}
16 | 
```

--------------------------------------------------------------------------------
/mcpServer/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | WORKDIR /app
 5 | 
 6 | # Copy package.json and package-lock.json
 7 | COPY package*.json ./
 8 | 
 9 | # Install dependencies (ignore scripts if any issues with native builds)
10 | RUN npm install --ignore-scripts
11 | 
12 | # Copy the rest of the application
13 | COPY . .
14 | 
15 | # Build the TypeScript source
16 | RUN npm run build
17 | 
18 | # Expose PORT if needed (optional)
19 | 
20 | # Start the MCP server
21 | CMD ["node", "build/index.js"]
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |     "name": "com.quaza.unitymcp",
 3 |     "version": "0.0.1",
 4 |     "displayName": "Unity MCP Integration",
 5 |     "description": "Integration between Model Context Protocol (MCP) and Unity, allowing AI assistants to understand and interact with Unity projects in real-time. Provides access to scene hierarchy, project settings, and code execution in the Unity Editor.",
 6 |     "unity": "2021.3",
 7 |     "dependencies": {
 8 |      
 9 |    },
10 |    "keywords": [
11 |       "ai",
12 |       "Model Context Protocol",
13 |       "Unity AI Tool",
14 |       "Unity MCP"
15 |     ],
16 |     "author": {
17 |       "name": "Shahzad",
18 |       "url": "https://quazaai.com/"
19 |     }
20 |   }
21 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | ## [0.0.1] - 2023-11-11
 2 | 
 3 | ### First Release
 4 | - Initial release of Unity MCP Integration
 5 | - WebSocket-based communication between Unity Editor and MCP server
 6 | - MCP tools implementation:
 7 |     - `get_editor_state` for retrieving Unity project information
 8 |     - `get_current_scene_info` for scene hierarchy details
 9 |     - `get_game_objects_info` for specific GameObject information
10 |     - `execute_editor_command` for executing C# code in Unity Editor
11 |     - `get_logs` for accessing Unity console logs
12 | - Debug window accessible from Window > MCP Debug menu
13 | - Connection status monitoring
14 | - Support for Unity 2021.3+
15 | - Node.js MCP server (TypeScript implementation)
16 | - Comprehensive documentation
17 | - MIT License
```

--------------------------------------------------------------------------------
/mcpServer/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "unity-mcp-server",
 3 |   "version": "0.1.0",
 4 |   "description": "MCP server for Unity integration",
 5 |   "type": "module",
 6 |   "bin": {
 7 |     "unity-mcp-server": "./build/index.js"
 8 |   },
 9 |   "files": [
10 |     "build"
11 |   ],
12 |   "scripts": {
13 |     "build": "tsc",
14 |     "start": "node build/index.js",
15 |     "dev": "tsc --watch",
16 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js"
17 |   },
18 |   "dependencies": {
19 |     "@modelcontextprotocol/sdk": "1.7.0",
20 |     "ws": "^8.16.0",
21 |     "diff": "^5.1.0",
22 |     "minimatch": "^9.0.3",
23 |     "zod": "^3.22.4",
24 |     "zod-to-json-schema": "^3.22.1"
25 |   },
26 |   "devDependencies": {
27 |     "@types/node": "^20.11.24",
28 |     "@types/ws": "^8.5.10",
29 |     "@types/diff": "^5.0.8",
30 |     "@types/minimatch": "^5.1.2",
31 |     "typescript": "^5.3.3"
32 |   }
33 | }
```

--------------------------------------------------------------------------------
/Editor/Models/MCPEditorState.cs:
--------------------------------------------------------------------------------

```csharp
 1 | using System;
 2 | using System.Collections.Generic;
 3 | using Newtonsoft.Json;
 4 | 
 5 | namespace Plugins.GamePilot.Editor.MCP
 6 | {
 7 |     [Serializable]
 8 |     public class MCPEditorState
 9 |     {
10 |         [JsonProperty("activeGameObjects")]
11 |         public string[] ActiveGameObjects { get; set; } = new string[0];
12 |         
13 |         [JsonProperty("selectedObjects")]
14 |         public string[] SelectedObjects { get; set; } = new string[0];
15 |         
16 |         [JsonProperty("playModeState")]
17 |         public string PlayModeState { get; set; } = "Stopped";
18 |         
19 |         [JsonProperty("sceneHierarchy")]
20 |         public List<MCPGameObjectInfo> SceneHierarchy { get; set; } = new List<MCPGameObjectInfo>();
21 |         
22 |         // Removed ProjectStructure property
23 |         
24 |         [JsonProperty("timestamp")]
25 |         public DateTime Timestamp { get; set; } = DateTime.UtcNow;
26 |         
27 |         // Enhanced project information properties
28 |         [JsonProperty("renderPipeline")]
29 |         public string RenderPipeline { get; set; } = "Unknown";
30 |         
31 |         [JsonProperty("buildTarget")]
32 |         public string BuildTarget { get; set; } = "Unknown";
33 |         
34 |         [JsonProperty("projectName")]
35 |         public string ProjectName { get; set; } = "Unknown";
36 |         
37 |         [JsonProperty("graphicsDeviceType")]
38 |         public string GraphicsDeviceType { get; set; } = "Unknown";
39 |         
40 |         [JsonProperty("unityVersion")]
41 |         public string UnityVersion { get; set; } = "Unknown";
42 |         
43 |         [JsonProperty("currentSceneName")]
44 |         public string CurrentSceneName { get; set; } = "Unknown";
45 |         
46 |         [JsonProperty("currentScenePath")]
47 |         public string CurrentScenePath { get; set; } = "Unknown";
48 |         
49 |         [JsonProperty("availableMenuItems")]
50 |         public List<string> AvailableMenuItems { get; set; } = new List<string>();
51 |     }
52 |     
53 |     [Serializable]
54 |     public class MCPGameObjectInfo
55 |     {
56 |         [JsonProperty("name")]
57 |         public string Name { get; set; }
58 |         
59 |         [JsonProperty("path")]
60 |         public string Path { get; set; }
61 |         
62 |         [JsonProperty("components")]
63 |         public string[] Components { get; set; } = new string[0];
64 |         
65 |         [JsonProperty("children")]
66 |         public List<MCPGameObjectInfo> Children { get; set; } = new List<MCPGameObjectInfo>();
67 |         
68 |         [JsonProperty("active")]
69 |         public bool Active { get; set; } = true;
70 |         
71 |         [JsonProperty("layer")]
72 |         public int Layer { get; set; }
73 |         
74 |         [JsonProperty("tag")]
75 |         public string Tag { get; set; }
76 |     }
77 |     
78 |     // Removed MCPProjectStructure class
79 |     
80 |     [Serializable]
81 |     public class LogEntry
82 |     {
83 |         [JsonProperty("message")]
84 |         public string Message { get; set; }
85 |         
86 |         [JsonProperty("stackTrace")]
87 |         public string StackTrace { get; set; }
88 |         
89 |         [JsonProperty("type")]
90 |         public UnityEngine.LogType Type { get; set; }
91 |         
92 |         [JsonProperty("timestamp")]
93 |         public DateTime Timestamp { get; set; }
94 |     }
95 | }
96 | 
```

--------------------------------------------------------------------------------
/Editor/Models/MCPSceneInfo.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using Newtonsoft.Json;
  4 | using UnityEngine;
  5 | 
  6 | namespace Plugins.GamePilot.Editor.MCP
  7 | {
  8 |     [Serializable]
  9 |     public class MCPSceneInfo
 10 |     {
 11 |         [JsonProperty("name")]
 12 |         public string Name { get; set; }
 13 |         
 14 |         [JsonProperty("path")]
 15 |         public string Path { get; set; }
 16 |         
 17 |         [JsonProperty("isDirty")]
 18 |         public bool IsDirty { get; set; }
 19 |         
 20 |         [JsonProperty("rootCount")]
 21 |         public int RootCount { get; set; }
 22 |         
 23 |         [JsonProperty("rootObjects")]
 24 |         public List<MCPGameObjectReference> RootObjects { get; set; } = new List<MCPGameObjectReference>();
 25 |         
 26 |         [JsonProperty("errorMessage")]
 27 |         public string ErrorMessage { get; set; }
 28 |     }
 29 |     
 30 |     [Serializable]
 31 |     public class MCPGameObjectReference
 32 |     {
 33 |         [JsonProperty("name")]
 34 |         public string Name { get; set; }
 35 |         
 36 |         [JsonProperty("instanceID")]
 37 |         public int InstanceID { get; set; }
 38 |         
 39 |         [JsonProperty("path")]
 40 |         public string Path { get; set; }
 41 |         
 42 |         [JsonProperty("active")]
 43 |         public bool Active { get; set; }
 44 |         
 45 |         [JsonProperty("childCount")]
 46 |         public int ChildCount { get; set; }
 47 |         
 48 |         [JsonProperty("children")]
 49 |         public List<MCPGameObjectReference> Children { get; set; }
 50 |     }
 51 |     
 52 |     [Serializable]
 53 |     public class MCPGameObjectDetail
 54 |     {
 55 |         [JsonProperty("name")]
 56 |         public string Name { get; set; }
 57 |         
 58 |         [JsonProperty("instanceID")]
 59 |         public int InstanceID { get; set; }
 60 |         
 61 |         [JsonProperty("path")]
 62 |         public string Path { get; set; }
 63 |         
 64 |         [JsonProperty("active")]
 65 |         public bool Active { get; set; }
 66 |         
 67 |         [JsonProperty("activeInHierarchy")]
 68 |         public bool ActiveInHierarchy { get; set; }
 69 |         
 70 |         [JsonProperty("tag")]
 71 |         public string Tag { get; set; }
 72 |         
 73 |         [JsonProperty("layer")]
 74 |         public int Layer { get; set; }
 75 |         
 76 |         [JsonProperty("layerName")]
 77 |         public string LayerName { get; set; }
 78 |         
 79 |         [JsonProperty("isStatic")]
 80 |         public bool IsStatic { get; set; }
 81 |         
 82 |         [JsonProperty("transform")]
 83 |         public MCPTransformInfo Transform { get; set; }
 84 |         
 85 |         [JsonProperty("components")]
 86 |         public List<MCPComponentInfo> Components { get; set; }
 87 |         
 88 |         [JsonProperty("children")]
 89 |         public List<MCPGameObjectDetail> Children { get; set; }
 90 |     }
 91 |     
 92 |     [Serializable]
 93 |     public class MCPTransformInfo
 94 |     {
 95 |         [JsonProperty("position")]
 96 |         public Vector3 Position { get; set; }
 97 |         
 98 |         [JsonProperty("rotation")]
 99 |         public Vector3 Rotation { get; set; }
100 |         
101 |         [JsonProperty("localPosition")]
102 |         public Vector3 LocalPosition { get; set; }
103 |         
104 |         [JsonProperty("localRotation")]
105 |         public Vector3 LocalRotation { get; set; }
106 |         
107 |         [JsonProperty("localScale")]
108 |         public Vector3 LocalScale { get; set; }
109 |     }
110 |     
111 |     [Serializable]
112 |     public class MCPComponentInfo
113 |     {
114 |         [JsonProperty("type")]
115 |         public string Type { get; set; }
116 |         
117 |         [JsonProperty("isEnabled")]
118 |         public bool IsEnabled { get; set; }
119 |         
120 |         [JsonProperty("instanceID")]
121 |         public int InstanceID { get; set; }
122 |     }
123 | }
124 | 
```

--------------------------------------------------------------------------------
/mcpServer/MCPSummary.md:
--------------------------------------------------------------------------------

```markdown
 1 | <img src="https://r2cdn.perplexity.ai/pplx-full-logo-primary-dark%402x.png" class="logo" width="120"/>
 2 | 
 3 | # 
 4 | 
 5 | ---
 6 | 
 7 | # https://modelcontextprotocol.io/introduction
 8 | 
 9 | https://modelcontextprotocol.io/quickstart/server
10 | https://modelcontextprotocol.io/quickstart/client
11 | https://modelcontextprotocol.io/docs/concepts/tools
12 | give me summary
13 | 
14 | Here is a summary of the provided pages from the Model Context Protocol (MCP) documentation:
15 | 
16 | ---
17 | 
18 | ### **Introduction to MCP**
19 | 
20 | - MCP is an open protocol that standardizes how applications provide context to large language models (LLMs), akin to how USB-C standardizes device connectivity[^1].
21 | - It enables:
22 |     - Pre-built integrations for LLMs to connect with data and tools.
23 |     - Flexibility in switching between LLM providers.
24 |     - Secure data handling within user infrastructure[^1].
25 | - MCP follows a **client-server architecture**:
26 |     - **MCP Hosts**: Applications like Claude Desktop or IDEs that access data via MCP.
27 |     - **MCP Clients**: Protocol clients maintaining connections with servers.
28 |     - **MCP Servers**: Lightweight programs exposing specific capabilities through MCP.
29 |     - **Local Data Sources** and **Remote Services**: Securely accessed by servers[^1].
30 | 
31 | ---
32 | 
33 | ### **Quickstart: Server**
34 | 
35 | - This guide demonstrates building an MCP weather server that provides tools for fetching weather forecasts and alerts, connecting it to a host like Claude for Desktop[^2].
36 | - **Core Concepts**:
37 |     - **Resources**: File-like data accessible by clients.
38 |     - **Tools**: Functions callable by LLMs with user approval.
39 |     - **Prompts**: Templates for specific tasks[^2].
40 | - Example implementation includes:
41 |     - Setting up a Python environment with the `uv` tool.
42 |     - Using the National Weather Service API to fetch weather data.
43 |     - Exposing tools like `get-alerts` and `get-forecast` for integration with hosts like Claude for Desktop[^2].
44 | - The server is tested by configuring Claude for Desktop to recognize and interact with it via commands[^2].
45 | 
46 | ---
47 | 
48 | ### **Quickstart: Client**
49 | 
50 | - This guide explains building an LLM-powered chatbot client capable of connecting to MCP servers[^3].
51 | - Steps include:
52 |     - Setting up a Python project using `uv`.
53 |     - Storing API keys securely in environment files.
54 |     - Creating a client class to manage server connections and process queries[^3].
55 | - Key functionalities:
56 |     - Connecting to MCP servers via Python or JavaScript scripts.
57 |     - Handling queries by leveraging available server tools and integrating responses into natural language outputs using LLMs like Claude[^3].
58 | 
59 | ---
60 | 
61 | ### **Concepts: Tools**
62 | 
63 | - Tools are functions exposed by MCP servers, enabling LLMs to perform specific actions. These tools are defined programmatically and can be invoked securely with user consent[^1].
64 | - Example use cases include automating workflows, accessing external APIs, or interacting with local/remote datasets through standardized interfaces[^1].
65 | 
66 | ---
67 | 
68 | This documentation provides comprehensive guidance on building both servers and clients using MCP, enabling seamless integration between LLMs and various data sources or tools.
69 | 
70 | <div style="text-align: center">⁂</div>
71 | 
72 | [^1]: https://modelcontextprotocol.io/introduction
73 | 
74 | [^2]: https://modelcontextprotocol.io/quickstart/server
75 | 
76 | [^3]: https://modelcontextprotocol.io/quickstart/client
77 | 
78 | [^4]: https://modelcontextprotocol.io/introduction
79 | 
80 | [^5]: https://modelcontextprotocol.io/quickstart/server
81 | 
82 | [^6]: https://modelcontextprotocol.io/quickstart/client
83 | 
84 | [^7]: https://modelcontextprotocol.io/docs/concepts/tools
85 | 
86 | 
```

--------------------------------------------------------------------------------
/mcpServer/src/types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // MCP Server Types for Unity Integration
  2 | 
  3 | // Unity Editor State representation
  4 | export interface UnityEditorState {
  5 |   activeGameObjects: any[];
  6 |   selectedObjects: any[];
  7 |   playModeState: string;
  8 |   sceneHierarchy: any;
  9 |   projectName?: string;
 10 |   unityVersion?: string;
 11 |   renderPipeline?: string;
 12 |   buildTarget?: string;
 13 |   graphicsDeviceType?: string;
 14 |   currentSceneName?: string;
 15 |   currentScenePath?: string;
 16 |   timestamp?: string;
 17 |   availableMenuItems?: string[];
 18 | }
 19 | 
 20 | // Log entry from Unity
 21 | export interface LogEntry {
 22 |   message: string;
 23 |   stackTrace: string;
 24 |   logType: string;
 25 |   timestamp: string;
 26 | }
 27 | 
 28 | // Scene info from Unity
 29 | export interface SceneInfoMessage {
 30 |   type: 'sceneInfo';
 31 |   data: {
 32 |     requestId: string;
 33 |     sceneInfo: any;
 34 |     timestamp: string;
 35 |   };
 36 | }
 37 | 
 38 | // Game objects details from Unity
 39 | export interface GameObjectsDetailsMessage {
 40 |   type: 'gameObjectsDetails';
 41 |   data: {
 42 |     requestId: string;
 43 |     gameObjectDetails: any[];
 44 |     count: number;
 45 |     timestamp: string;
 46 |   };
 47 | }
 48 | 
 49 | // Message types from Unity to Server
 50 | export interface EditorStateMessage {
 51 |   type: 'editorState';
 52 |   data: UnityEditorState;
 53 | }
 54 | 
 55 | export interface CommandResultMessage {
 56 |   type: 'commandResult';
 57 |   data: any;
 58 | }
 59 | 
 60 | export interface LogMessage {
 61 |   type: 'log';
 62 |   data: LogEntry;
 63 | }
 64 | 
 65 | export interface PongMessage {
 66 |   type: 'pong';
 67 |   data: { timestamp: number };
 68 | }
 69 | 
 70 | // Message types from Server to Unity
 71 | export interface ExecuteEditorCommandMessage {
 72 |   type: 'executeEditorCommand';
 73 |   data: {
 74 |     code: string;
 75 |   };
 76 | }
 77 | 
 78 | export interface HandshakeMessage {
 79 |   type: 'handshake';
 80 |   data: { message: string };
 81 | }
 82 | 
 83 | export interface PingMessage {
 84 |   type: 'ping';
 85 |   data: { timestamp: number };
 86 | }
 87 | 
 88 | export interface RequestEditorStateMessage {
 89 |   type: 'requestEditorState';
 90 |   data: Record<string, never>;
 91 | }
 92 | 
 93 | export interface GetSceneInfoMessage {
 94 |   type: 'getSceneInfo';
 95 |   data: {
 96 |     requestId: string;
 97 |     detailLevel: string;
 98 |   };
 99 | }
100 | 
101 | export interface GetGameObjectsInfoMessage {
102 |   type: 'getGameObjectsInfo';
103 |   data: {
104 |     requestId: string;
105 |     instanceIDs: number[];
106 |     detailLevel: string;
107 |   };
108 | }
109 | 
110 | // Union type for all Unity messages
111 | export type UnityMessage = 
112 |   | EditorStateMessage 
113 |   | CommandResultMessage 
114 |   | LogMessage
115 |   | PongMessage
116 |   | SceneInfoMessage
117 |   | GameObjectsDetailsMessage;
118 | 
119 | // Union type for all Server messages
120 | export type ServerMessage =
121 |   | ExecuteEditorCommandMessage
122 |   | HandshakeMessage
123 |   | PingMessage
124 |   | RequestEditorStateMessage
125 |   | GetSceneInfoMessage
126 |   | GetGameObjectsInfoMessage;
127 | 
128 | // Command result handling
129 | export interface CommandPromise {
130 |   resolve: (data?: any) => void;
131 |   reject: (reason?: any) => void;
132 | }
133 | 
134 | export interface TreeEntry {
135 |   name: string;
136 |   type: 'file' | 'directory';
137 |   children?: TreeEntry[];
138 | }
139 | 
140 | export interface MCPSceneInfo {
141 |   name: string;
142 |   path: string;
143 |   rootGameObjects: any[];
144 |   buildIndex: number;
145 |   isDirty: boolean;
146 |   isLoaded: boolean;
147 | }
148 | 
149 | export interface MCPTransformInfo {
150 |   position: { x: number, y: number, z: number };
151 |   rotation: { x: number, y: number, z: number };
152 |   localPosition: { x: number, y: number, z: number };
153 |   localRotation: { x: number, y: number, z: number };
154 |   localScale: { x: number, y: number, z: number };
155 | }
156 | 
157 | export interface MCPComponentInfo {
158 |   type: string;
159 |   isEnabled: boolean;
160 |   instanceID: number;
161 | }
162 | 
163 | export interface MCPGameObjectDetail {
164 |   name: string;
165 |   instanceID: number;
166 |   path: string;
167 |   active: boolean;
168 |   activeInHierarchy: boolean;
169 |   tag: string;
170 |   layer: number;
171 |   layerName: string;
172 |   isStatic: boolean;
173 |   transform: MCPTransformInfo;
174 |   components: MCPComponentInfo[];
175 | }
176 | 
177 | export enum SceneInfoDetail {
178 |   RootObjectsOnly = 'RootObjectsOnly',
179 |   FullHierarchy = 'FullHierarchy'
180 | }
181 | 
182 | export enum GameObjectInfoDetail {
183 |   BasicInfo = 'BasicInfo',
184 |   IncludeComponents = 'IncludeComponents',
185 |   IncludeChildren = 'IncludeChildren',
186 |   IncludeComponentsAndChildren = 'IncludeComponentsAndChildren'
187 | }
```

--------------------------------------------------------------------------------
/Editor/MCPManager.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using UnityEditor;
  3 | using UnityEngine;
  4 | 
  5 | namespace Plugins.GamePilot.Editor.MCP
  6 | {
  7 |     [InitializeOnLoad]
  8 |     public class MCPManager
  9 |     {
 10 |         private static readonly string ComponentName = "MCPManager";
 11 |         private static MCPConnectionManager connectionManager;
 12 |         private static MCPMessageHandler messageHandler;
 13 |         private static MCPDataCollector dataCollector;
 14 |         private static MCPMessageSender messageSender;
 15 |         
 16 |         private static bool isInitialized = false;
 17 |         public static bool IsInitialized => isInitialized;
 18 |         private static bool autoReconnect = false;
 19 |         
 20 |         // Constructor called on editor startup due to [InitializeOnLoad]
 21 |         static MCPManager()
 22 |         {
 23 |             // Initialize logger for this component
 24 |             MCPLogger.InitializeComponent(ComponentName);
 25 |             
 26 |             EditorApplication.delayCall += Initialize;
 27 |         }
 28 |         
 29 |         public static void Initialize()
 30 |         {
 31 |             if (isInitialized)
 32 |                 return;
 33 |                 
 34 |             MCPLogger.Log(ComponentName, "Initializing Model Context Protocol system...");
 35 |             
 36 |             try
 37 |             {
 38 |                 // Create components
 39 |                 dataCollector = new MCPDataCollector();
 40 |                 connectionManager = new MCPConnectionManager();
 41 |                 messageSender = new MCPMessageSender(connectionManager);
 42 |                 messageHandler = new MCPMessageHandler(dataCollector, messageSender);
 43 |                 
 44 |                 // Hook up events
 45 |                 connectionManager.OnMessageReceived += messageHandler.HandleMessage;
 46 |                 connectionManager.OnConnected += OnConnected;
 47 |                 connectionManager.OnDisconnected += OnDisconnected;
 48 |                 connectionManager.OnError += OnError;
 49 |                 
 50 |                 // Start connection
 51 |                 connectionManager.Connect();
 52 |                 
 53 |                 // Register update for connection checking only
 54 |                 EditorApplication.update += Update;
 55 |                 
 56 |                 isInitialized = true;
 57 |                 MCPLogger.Log(ComponentName, "Model Context Protocol system initialized successfully");
 58 |             }
 59 |             catch (Exception ex)
 60 |             {
 61 |                 MCPLogger.LogException(ComponentName, ex);
 62 |                 Debug.LogError($"[MCP] Failed to initialize MCP system: {ex.Message}\n{ex.StackTrace}");
 63 |             }
 64 |         }
 65 | 
 66 |         private static void OnConnected()
 67 |         {
 68 |             try
 69 |             {
 70 |                 MCPLogger.Log(ComponentName, "Connected to MCP server");
 71 |             }
 72 |             catch (Exception ex)
 73 |             {
 74 |                 MCPLogger.LogException(ComponentName, ex);
 75 |             }
 76 |         }
 77 | 
 78 |         private static void OnDisconnected()
 79 |         {
 80 |             try
 81 |             {
 82 |                 MCPLogger.Log(ComponentName, "Disconnected from MCP server");
 83 |             }
 84 |             catch (Exception ex)
 85 |             {
 86 |                 MCPLogger.LogException(ComponentName, ex);
 87 |             }
 88 |         }
 89 | 
 90 |         private static void OnError(string errorMessage)
 91 |         {
 92 |             MCPLogger.LogError(ComponentName, $"Connection error: {errorMessage}");
 93 |         }
 94 |         
 95 |         private static void Update()
 96 |         {
 97 |             try
 98 |             {
 99 |                 // Check if connection manager needs to reconnect
100 |                 if (autoReconnect)
101 |                 {
102 |                     connectionManager?.CheckConnection();
103 |                 }
104 |             }
105 |             catch (Exception ex)
106 |             {
107 |                 MCPLogger.LogException(ComponentName, ex);
108 |             }
109 |         }
110 |         
111 |         // Public methods for manual control
112 |         public static void RetryConnection()
113 |         {
114 |             connectionManager?.Reconnect();
115 |         }
116 |         
117 |         public static void EnableAutoReconnect(bool enable)
118 |         {
119 |             autoReconnect = enable;
120 |             MCPLogger.Log(ComponentName, $"Auto reconnect set to: {enable}");
121 |         }
122 |         
123 |         public static bool IsConnected => connectionManager?.IsConnected ?? false;
124 |         
125 |         public static void Shutdown()
126 |         {
127 |             if (!isInitialized) return;
128 |             
129 |             try
130 |             {
131 |                 MCPLogger.Log(ComponentName, "Shutting down MCP system");
132 |                 
133 |                 // Unregister update callbacks
134 |                 EditorApplication.update -= Update;
135 |                 
136 |                 // Disconnect
137 |                 connectionManager?.Disconnect();
138 |                 
139 |                 // Cleanup
140 |                 dataCollector?.Dispose();
141 |                 
142 |                 isInitialized = false;
143 |                 MCPLogger.Log(ComponentName, "System shutdown completed");
144 |             }
145 |             catch (Exception ex)
146 |             {
147 |                 MCPLogger.LogException(ComponentName, ex);
148 |             }
149 |         }
150 |     }
151 | }
152 | 
```

--------------------------------------------------------------------------------
/Editor/MCPLogger.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using UnityEngine;
  4 | 
  5 | namespace Plugins.GamePilot.Editor.MCP
  6 | {
  7 |     /// <summary>
  8 |     /// Centralized logging system for MCP components that respects component-level logging settings.
  9 |     /// </summary>
 10 |     public static class MCPLogger
 11 |     {
 12 |         // Dictionary to track enabled status for each component
 13 |         private static readonly Dictionary<string, bool> componentLoggingEnabled = new Dictionary<string, bool>();
 14 |         
 15 |         // Default log state (off by default)
 16 |         private static bool globalLoggingEnabled = false;
 17 |         
 18 |         /// <summary>
 19 |         /// Enable or disable logging globally for all components
 20 |         /// </summary>
 21 |         public static bool GlobalLoggingEnabled 
 22 |         { 
 23 |             get => globalLoggingEnabled;
 24 |             set => globalLoggingEnabled = value;
 25 |         }
 26 |         
 27 |         /// <summary>
 28 |         /// Initialize a component for logging
 29 |         /// </summary>
 30 |         /// <param name="componentName">Name of the component</param>
 31 |         /// <param name="enabledByDefault">Whether logging should be enabled by default</param>
 32 |         public static void InitializeComponent(string componentName, bool enabledByDefault = false)
 33 |         {
 34 |             if (!componentLoggingEnabled.ContainsKey(componentName))
 35 |             {
 36 |                 componentLoggingEnabled[componentName] = enabledByDefault;
 37 |             }
 38 |         }
 39 |         
 40 |         /// <summary>
 41 |         /// Set logging state for a specific component
 42 |         /// </summary>
 43 |         /// <param name="componentName">Name of the component</param>
 44 |         /// <param name="enabled">Whether logging should be enabled</param>
 45 |         public static void SetComponentLoggingEnabled(string componentName, bool enabled)
 46 |         {
 47 |             // Make sure component exists before setting state
 48 |             if (!componentLoggingEnabled.ContainsKey(componentName))
 49 |             {
 50 |                 InitializeComponent(componentName, false);
 51 |             }
 52 |             
 53 |             componentLoggingEnabled[componentName] = enabled;
 54 |         }
 55 |         
 56 |         /// <summary>
 57 |         /// Check if logging is enabled for a component
 58 |         /// </summary>
 59 |         /// <param name="componentName">Name of the component</param>
 60 |         /// <returns>True if logging is enabled</returns>
 61 |         public static bool IsLoggingEnabled(string componentName)
 62 |         {
 63 |             // If global logging is disabled, nothing gets logged
 64 |             if (!globalLoggingEnabled) return false;
 65 |             
 66 |             // If component isn't registered, assume it's disabled
 67 |             if (!componentLoggingEnabled.ContainsKey(componentName))
 68 |             {
 69 |                 InitializeComponent(componentName, false);
 70 |                 return false;
 71 |             }
 72 |             
 73 |             // Return component-specific setting
 74 |             return componentLoggingEnabled[componentName];
 75 |         }
 76 |         
 77 |         /// <summary>
 78 |         /// Log a message if logging is enabled for the component
 79 |         /// </summary>
 80 |         /// <param name="componentName">Name of the component</param>
 81 |         /// <param name="message">Message to log</param>
 82 |         public static void Log(string componentName, string message)
 83 |         {
 84 |             if (IsLoggingEnabled(componentName))
 85 |             {
 86 |                 Debug.Log($"[MCP] [{componentName}] {message}");
 87 |             }
 88 |         }
 89 |         
 90 |         /// <summary>
 91 |         /// Log a warning if logging is enabled for the component
 92 |         /// </summary>
 93 |         /// <param name="componentName">Name of the component</param>
 94 |         /// <param name="message">Message to log</param>
 95 |         public static void LogWarning(string componentName, string message)
 96 |         {
 97 |             if (IsLoggingEnabled(componentName))
 98 |             {
 99 |                 Debug.LogWarning($"[MCP] [{componentName}] {message}");
100 |             }
101 |         }
102 |         
103 |         /// <summary>
104 |         /// Log an error if logging is enabled for the component
105 |         /// </summary>
106 |         /// <param name="componentName">Name of the component</param>
107 |         /// <param name="message">Message to log</param>
108 |         public static void LogError(string componentName, string message)
109 |         {
110 |             if (IsLoggingEnabled(componentName))
111 |             {
112 |                 Debug.LogError($"[MCP] [{componentName}] {message}");
113 |             }
114 |         }
115 |         
116 |         /// <summary>
117 |         /// Log an exception if logging is enabled for the component
118 |         /// </summary>
119 |         /// <param name="componentName">Name of the component</param>
120 |         /// <param name="ex">Exception to log</param>
121 |         public static void LogException(string componentName, Exception ex)
122 |         {
123 |             if (IsLoggingEnabled(componentName))
124 |             {
125 |                 Debug.LogError($"[MCP] [{componentName}] Exception: {ex.Message}\n{ex.StackTrace}");
126 |             }
127 |         }
128 |         
129 |         /// <summary>
130 |         /// Get all registered components
131 |         /// </summary>
132 |         public static IEnumerable<string> GetRegisteredComponents()
133 |         {
134 |             return componentLoggingEnabled.Keys;
135 |         }
136 |         
137 |         /// <summary>
138 |         /// Get logging state for a component
139 |         /// </summary>
140 |         public static bool GetComponentLoggingEnabled(string componentName)
141 |         {
142 |             if (!componentLoggingEnabled.ContainsKey(componentName))
143 |                 return false;
144 |                 
145 |             return componentLoggingEnabled[componentName];
146 |         }
147 |     }
148 | }
149 | 
```

--------------------------------------------------------------------------------
/mcpServer/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  4 | import { WebSocketHandler } from './websocketHandler.js';
  5 | import { registerTools } from './toolDefinitions.js';
  6 | import path from 'path';
  7 | import { fileURLToPath } from 'url';
  8 | import fs from 'fs';
  9 | 
 10 | class UnityMCPServer {
 11 |   private server: Server;
 12 |   private wsHandler: WebSocketHandler;
 13 | 
 14 |   constructor() {
 15 |     // Initialize MCP Server
 16 |     this.server = new Server(
 17 |       { name: 'unity-mcp-server', version: '0.2.0' },
 18 |       { capabilities: { tools: {} } }
 19 |     );
 20 | 
 21 |     // Setup project paths and websocket
 22 |     const wsPort = parseInt(process.env.MCP_WEBSOCKET_PORT || '5010');
 23 |     const projectRootPath = this.setupProjectPaths();
 24 |     
 25 |     // Initialize WebSocket Handler for Unity communication
 26 |     this.wsHandler = new WebSocketHandler(wsPort);
 27 | 
 28 |     // Register MCP tools
 29 |     registerTools(this.server, this.wsHandler);
 30 |     
 31 |     // Error handling
 32 |     this.server.onerror = (error) => console.error('[MCP Error]', error);
 33 |     this.setupShutdownHandlers();
 34 |   }
 35 | 
 36 |   private setupProjectPaths(): string {
 37 |     const __filename = fileURLToPath(import.meta.url);
 38 |     const __dirname = path.dirname(__filename);
 39 |     console.error(`[Unity MCP] Server starting from directory: ${__dirname}`);
 40 |     
 41 |     // Get the project root path (parent of Assets)
 42 |     let projectRootPath = process.env.UNITY_PROJECT_PATH || this.determineUnityProjectPath(__dirname);
 43 |     projectRootPath = path.normalize(projectRootPath.replace(/["']/g, ''));
 44 |     
 45 |     // Make sure path ends with a directory separator
 46 |     if (!projectRootPath.endsWith(path.sep)) {
 47 |       projectRootPath += path.sep;
 48 |     }
 49 |     
 50 |     // Create the full path to the Assets folder
 51 |     const projectPath = path.join(projectRootPath, 'Assets') + path.sep;
 52 |     this.setupEnvironmentPath(projectRootPath, projectPath);
 53 |     
 54 |     return projectRootPath;
 55 |   }
 56 | 
 57 |   private setupEnvironmentPath(projectRootPath: string, projectPath: string): void {
 58 |     try {
 59 |       if (fs.existsSync(projectPath)) {
 60 |         console.error(`[Unity MCP] Using project path: ${projectPath}`);
 61 |         process.env.UNITY_PROJECT_PATH = projectPath;
 62 |       } else {
 63 |         console.error(`[Unity MCP] WARNING: Assets folder not found at ${projectPath}`);
 64 |         console.error(`[Unity MCP] Using project root instead: ${projectRootPath}`);
 65 |         process.env.UNITY_PROJECT_PATH = projectRootPath;
 66 |       }
 67 |     } catch (error) {
 68 |       console.error(`[Unity MCP] Error checking project path: ${error}`);
 69 |       process.env.UNITY_PROJECT_PATH = process.cwd();
 70 |     }
 71 |   }
 72 | 
 73 |   private setupShutdownHandlers(): void {
 74 |     const cleanupHandler = async () => {
 75 |       await this.cleanup();
 76 |       process.exit(0);
 77 |     };
 78 |     
 79 |     process.on('SIGINT', cleanupHandler);
 80 |     process.on('SIGTERM', cleanupHandler);
 81 |   }
 82 | 
 83 |   /**
 84 |    * Determine the Unity project path based on the script location
 85 |    */
 86 |   private determineUnityProjectPath(scriptDir: string): string {
 87 |     scriptDir = path.normalize(scriptDir);
 88 |     console.error(`[Unity MCP] Script directory: ${scriptDir}`);
 89 |     
 90 |     // Case 1: Installed in Assets folder
 91 |     const assetsMatch = /^(.+?[\/\\]Assets)[\/\\].*$/i.exec(scriptDir);
 92 |     if (assetsMatch) {
 93 |       const projectRoot = path.dirname(assetsMatch[1]);
 94 |       console.error(`[Unity MCP] Detected installation in Assets folder: ${projectRoot}`);
 95 |       return projectRoot;
 96 |     }
 97 |     
 98 |     // Case 2: Installed via Package Manager
 99 |     const libraryMatch = /^(.+?[\/\\]Library)[\/\\]PackageCache[\/\\].*$/i.exec(scriptDir);
100 |     if (libraryMatch) {
101 |       const projectRoot = path.dirname(libraryMatch[1]);
102 |       console.error(`[Unity MCP] Detected installation via Package Manager: ${projectRoot}`);
103 |       
104 |       const assetsPath = path.join(projectRoot, 'Assets');
105 |       if (fs.existsSync(assetsPath)) {
106 |         return projectRoot;
107 |       }
108 |     }
109 |     
110 |     // Case 3: Check parent directories
111 |     for (const dir of this.getParentDirectories(scriptDir)) {
112 |       // Check if this directory is "UnityMCP"
113 |       if (path.basename(dir) === 'UnityMCP') {
114 |         console.error(`[Unity MCP] Found UnityMCP directory at: ${dir}`);
115 |         return dir;
116 |       }
117 |       
118 |       // Check if this directory contains an Assets folder
119 |       const assetsDir = path.join(dir, 'Assets');
120 |       try {
121 |         if (fs.existsSync(assetsDir) && fs.statSync(assetsDir).isDirectory()) {
122 |           console.error(`[Unity MCP] Found Unity project at: ${dir}`);
123 |           return dir;
124 |         }
125 |       } catch (e) {
126 |         // Ignore errors checking directories
127 |       }
128 |     }
129 |     
130 |     // Fallback
131 |     console.error('[Unity MCP] Could not detect Unity project directory. Using current directory.');
132 |     return process.cwd();
133 |   }
134 | 
135 |   private getParentDirectories(filePath: string): string[] {
136 |     const result: string[] = [];
137 |     const dirs = filePath.split(path.sep);
138 |     
139 |     for (let i = 1; i <= dirs.length; i++) {
140 |       result.push(dirs.slice(0, i).join(path.sep));
141 |     }
142 |     
143 |     return result;
144 |   }
145 | 
146 |   private async cleanup() {
147 |     console.error('Cleaning up resources...');
148 |     await this.wsHandler.close();
149 |     await this.server.close();
150 |   }
151 | 
152 |   async run() {
153 |     const transport = new StdioServerTransport();
154 |     await this.server.connect(transport);
155 |     console.error('[Unity MCP] Server running and ready to accept connections');
156 |     console.error('[Unity MCP] WebSocket server listening on port', this.wsHandler.port);
157 |   }
158 | }
159 | 
160 | // Start the server
161 | const server = new UnityMCPServer();
162 | server.run().catch(err => {
163 |   console.error('Fatal error in MCP server:', err);
164 |   process.exit(1);
165 | });
```

--------------------------------------------------------------------------------
/Editor/MCPCodeExecutor.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.CodeDom.Compiler;
  3 | using System.Collections.Generic;
  4 | using System.Linq;
  5 | using Microsoft.CSharp;
  6 | using UnityEngine;
  7 | using UnityEditor;
  8 | 
  9 | namespace Plugins.GamePilot.Editor.MCP
 10 | {
 11 |     public class MCPCodeExecutor
 12 |     {
 13 |         private readonly List<string> logs = new List<string>();
 14 |         private readonly List<string> errors = new List<string>();
 15 |         private readonly List<string> warnings = new List<string>();
 16 |         
 17 |         public object ExecuteCode(string code)
 18 |         {
 19 |             logs.Clear();
 20 |             errors.Clear();
 21 |             warnings.Clear();
 22 |             
 23 |             // Add log handler to capture output during execution
 24 |             Application.logMessageReceived += LogHandler;
 25 |             
 26 |             try
 27 |             {
 28 |                 return ExecuteCommand(code);
 29 |             }
 30 |             catch (Exception ex)
 31 |             {
 32 |                 string errorMessage = $"Code execution failed: {ex.Message}\n{ex.StackTrace}";
 33 |                 Debug.LogError(errorMessage);
 34 |                 errors.Add(errorMessage);
 35 |                 return null;
 36 |             }
 37 |             finally
 38 |             {
 39 |                 Application.logMessageReceived -= LogHandler;
 40 |                 GC.Collect();
 41 |                 GC.WaitForPendingFinalizers();
 42 |             }
 43 |         }
 44 | 
 45 |         private object ExecuteCommand(string code)
 46 |         {
 47 |             // The code should define a class called "McpScript" with a static method "Execute"
 48 |             // Less restrictive on what the code can contain (namespaces, classes, etc.)
 49 |             using (var provider = new CSharpCodeProvider())
 50 |             {
 51 |                 var options = new CompilerParameters
 52 |                 {
 53 |                     GenerateInMemory = true,
 54 |                     IncludeDebugInformation = true
 55 |                 };
 56 |                 
 57 |                 // Add essential references
 58 |                 AddEssentialReferences(options);
 59 |                 
 60 |                 // Compile the code as provided - with no wrapping
 61 |                 var results = provider.CompileAssemblyFromSource(options, code);
 62 |                 
 63 |                 if (results.Errors.HasErrors)
 64 |                 {
 65 |                     var errorMessages = new List<string>();
 66 |                     foreach (CompilerError error in results.Errors)
 67 |                     {
 68 |                         errorMessages.Add($"Line {error.Line}: {error.ErrorText}");
 69 |                     }
 70 |                     throw new Exception("Compilation failed: " + string.Join("\n", errorMessages));
 71 |                 }
 72 | 
 73 |                 // Get the compiled assembly and execute the code via the McpScript.Execute method
 74 |                 var assembly = results.CompiledAssembly;
 75 |                 var type = assembly.GetType("McpScript");
 76 |                 if (type == null)
 77 |                 {
 78 |                     throw new Exception("Could not find McpScript class in compiled assembly. Make sure your code defines a public class named 'McpScript'.");
 79 |                 }
 80 |                 
 81 |                 var method = type.GetMethod("Execute");
 82 |                 if (method == null)
 83 |                 {
 84 |                     throw new Exception("Could not find Execute method in McpScript class. Make sure your code includes a public static method named 'Execute'.");
 85 |                 }
 86 |                 
 87 |                 return method.Invoke(null, null);
 88 |             }
 89 |         }
 90 | 
 91 |         private void AddEssentialReferences(CompilerParameters options)
 92 |         {
 93 |             // Only add the most essential references to avoid conflicts
 94 |             try
 95 |             {
 96 |                 // Core Unity and .NET references
 97 |                 options.ReferencedAssemblies.Add(typeof(UnityEngine.Object).Assembly.Location); // UnityEngine
 98 |                 options.ReferencedAssemblies.Add(typeof(UnityEditor.Editor).Assembly.Location); // UnityEditor
 99 |                 options.ReferencedAssemblies.Add(typeof(System.Object).Assembly.Location); // mscorlib
100 |                 
101 |                 // Add System.Core for LINQ
102 |                 var systemCore = AppDomain.CurrentDomain.GetAssemblies()
103 |                     .FirstOrDefault(a => a.GetName().Name == "System.Core");
104 |                 if (systemCore != null && !string.IsNullOrEmpty(systemCore.Location))
105 |                 {
106 |                     options.ReferencedAssemblies.Add(systemCore.Location);
107 |                 }
108 |                 
109 |                 // Add netstandard reference
110 |                 var netStandardAssembly = AppDomain.CurrentDomain.GetAssemblies()
111 |                     .FirstOrDefault(a => a.GetName().Name == "netstandard");
112 |                 if (netStandardAssembly != null && !string.IsNullOrEmpty(netStandardAssembly.Location))
113 |                 {
114 |                     options.ReferencedAssemblies.Add(netStandardAssembly.Location);
115 |                 }
116 |                 
117 |                 // Add essential Unity modules
118 |                 AddUnityModule(options, "UnityEngine.CoreModule");
119 |                 AddUnityModule(options, "UnityEngine.PhysicsModule");
120 |                 AddUnityModule(options, "UnityEngine.UIModule");
121 |                 AddUnityModule(options, "UnityEngine.InputModule");
122 |                 AddUnityModule(options, "UnityEngine.AnimationModule");
123 |                 AddUnityModule(options, "UnityEngine.IMGUIModule");
124 |             }
125 |             catch (Exception ex)
126 |             {
127 |                 Debug.LogWarning($"Error adding assembly references: {ex.Message}");
128 |             }
129 |         }
130 | 
131 |         private void AddUnityModule(CompilerParameters options, string moduleName)
132 |         {
133 |             try
134 |             {
135 |                 var assembly = AppDomain.CurrentDomain.GetAssemblies()
136 |                     .FirstOrDefault(a => a.GetName().Name == moduleName);
137 |                     
138 |                 if (assembly != null && !string.IsNullOrEmpty(assembly.Location) && 
139 |                     !options.ReferencedAssemblies.Contains(assembly.Location))
140 |                 {
141 |                     options.ReferencedAssemblies.Add(assembly.Location);
142 |                 }
143 |             }
144 |             catch (Exception ex)
145 |             {
146 |                 Debug.LogWarning($"Failed to add Unity module {moduleName}: {ex.Message}");
147 |             }
148 |         }
149 |         
150 |         private void LogHandler(string message, string stackTrace, LogType type)
151 |         {
152 |             switch (type)
153 |             {
154 |                 case LogType.Log:
155 |                     logs.Add(message);
156 |                     break;
157 |                 case LogType.Warning:
158 |                     warnings.Add(message);
159 |                     break;
160 |                 case LogType.Error:
161 |                 case LogType.Exception:
162 |                 case LogType.Assert:
163 |                     errors.Add($"{message}\n{stackTrace}");
164 |                     break;
165 |             }
166 |         }
167 |         
168 |         public string[] GetLogs() => logs.ToArray();
169 |         public string[] GetErrors() => errors.ToArray();
170 |         public string[] GetWarnings() => warnings.ToArray();
171 |     }
172 | }
```

--------------------------------------------------------------------------------
/Editor/MCPMessageSender.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Threading.Tasks;
  4 | using Newtonsoft.Json;
  5 | using UnityEngine;
  6 | using System.Linq;
  7 | 
  8 | namespace Plugins.GamePilot.Editor.MCP
  9 | {
 10 |     public class MCPMessageSender
 11 |     {
 12 |         private readonly MCPConnectionManager connectionManager;
 13 |         
 14 |         public MCPMessageSender(MCPConnectionManager connectionManager)
 15 |         {
 16 |             this.connectionManager = connectionManager ?? throw new ArgumentNullException(nameof(connectionManager));
 17 |         }
 18 |         
 19 |         public async Task SendEditorStateAsync(MCPEditorState state)
 20 |         {
 21 |             if (state == null) return;
 22 |             if (!connectionManager.IsConnected) return;
 23 |             
 24 |             try
 25 |             {
 26 |                 var message = JsonConvert.SerializeObject(new
 27 |                 {
 28 |                     type = "editorState",
 29 |                     data = state
 30 |                 }, new JsonSerializerSettings
 31 |                 {
 32 |                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
 33 |                 });
 34 |                 
 35 |                 await connectionManager.SendMessageAsync(message);
 36 |             }
 37 |             catch (Exception ex)
 38 |             {
 39 |                 Debug.LogError($"[MCP] Error sending editor state: {ex.Message}");
 40 |             }
 41 |         }
 42 |         
 43 |         public async Task SendLogEntryAsync(LogEntry logEntry)
 44 |         {
 45 |             if (logEntry == null) return;
 46 |             if (!connectionManager.IsConnected) return;
 47 |             
 48 |             try
 49 |             {
 50 |                 var message = JsonConvert.SerializeObject(new
 51 |                 {
 52 |                     type = "log",
 53 |                     data = new
 54 |                     {
 55 |                         message = logEntry.Message,
 56 |                         stackTrace = logEntry.StackTrace,
 57 |                         logType = logEntry.Type.ToString(),
 58 |                         timestamp = logEntry.Timestamp
 59 |                     }
 60 |                 });
 61 |                 
 62 |                 await connectionManager.SendMessageAsync(message);
 63 |             }
 64 |             catch (Exception ex)
 65 |             {
 66 |                 Debug.LogError($"[MCP] Error sending log entry: {ex.Message}");
 67 |             }
 68 |         }
 69 |         
 70 |         public async Task SendCommandResultAsync(string commandId, object result, 
 71 |             IEnumerable<string> logs, IEnumerable<string> errors, IEnumerable<string> warnings)
 72 |         {
 73 |             if (!connectionManager.IsConnected) return;
 74 |             
 75 |             try
 76 |             {
 77 |                 var message = JsonConvert.SerializeObject(new
 78 |                 {
 79 |                     type = "commandResult",
 80 |                     data = new
 81 |                     {
 82 |                         commandId,
 83 |                         result = result,
 84 |                         logs = logs ?? Array.Empty<string>(),
 85 |                         errors = errors ?? Array.Empty<string>(),
 86 |                         warnings = warnings ?? Array.Empty<string>(),
 87 |                         executionSuccess = errors == null || !errors.GetEnumerator().MoveNext()
 88 |                     }
 89 |                 });
 90 |                 
 91 |                 await connectionManager.SendMessageAsync(message);
 92 |             }
 93 |             catch (Exception ex)
 94 |             {
 95 |                 Debug.LogError($"[MCP] Error sending command result: {ex.Message}");
 96 |             }
 97 |         }
 98 |         
 99 |         public async Task SendErrorMessageAsync(string errorCode, string errorMessage)
100 |         {
101 |             if (!connectionManager.IsConnected) return;
102 |             
103 |             try
104 |             {
105 |                 var message = JsonConvert.SerializeObject(new
106 |                 {
107 |                     type = "error",
108 |                     data = new
109 |                     {
110 |                         code = errorCode,
111 |                         message = errorMessage,
112 |                         timestamp = DateTime.UtcNow
113 |                     }
114 |                 });
115 |                 
116 |                 await connectionManager.SendMessageAsync(message);
117 |             }
118 |             catch (Exception ex)
119 |             {
120 |                 Debug.LogError($"[MCP] Error sending error message: {ex.Message}");
121 |             }
122 |         }
123 | 
124 |         public async Task SendGetLogsResponseAsync(string requestId, LogEntry[] logs)
125 |         {
126 |             if (!connectionManager.IsConnected) return;
127 |             
128 |             try
129 |             {
130 |                 var message = JsonConvert.SerializeObject(new
131 |                 {
132 |                     type = "logsResponse",
133 |                     data = new
134 |                     {
135 |                         requestId,
136 |                         logs,
137 |                         timestamp = DateTime.UtcNow
138 |                     }
139 |                 });
140 |                 
141 |                 await connectionManager.SendMessageAsync(message);
142 |             }
143 |             catch (Exception ex)
144 |             {
145 |                 Debug.LogError($"[MCP] Error sending logs response: {ex.Message}");
146 |             }
147 |         }
148 |         
149 |         public async Task SendSceneInfoAsync(string requestId, MCPSceneInfo sceneInfo)
150 |         {
151 |             if (!connectionManager.IsConnected) return;
152 |             
153 |             try
154 |             {
155 |                 var message = JsonConvert.SerializeObject(new
156 |                 {
157 |                     type = "sceneInfo",
158 |                     data = new
159 |                     {
160 |                         requestId,
161 |                         sceneInfo,
162 |                         timestamp = DateTime.UtcNow
163 |                     }
164 |                 }, new JsonSerializerSettings
165 |                 {
166 |                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
167 |                 });
168 |                 
169 |                 await connectionManager.SendMessageAsync(message);
170 |             }
171 |             catch (Exception ex)
172 |             {
173 |                 Debug.LogError($"[MCP] Error sending scene info: {ex.Message}");
174 |             }
175 |         }
176 |         
177 |         public async Task SendGameObjectsDetailsAsync(string requestId, List<MCPGameObjectDetail> gameObjectDetails)
178 |         {
179 |             if (!connectionManager.IsConnected) return;
180 |             
181 |             try
182 |             {
183 |                 var message = JsonConvert.SerializeObject(new
184 |                 {
185 |                     type = "gameObjectsDetails",
186 |                     data = new
187 |                     {
188 |                         requestId,
189 |                         gameObjectDetails,
190 |                         count = gameObjectDetails?.Count ?? 0,
191 |                         timestamp = DateTime.UtcNow
192 |                     }
193 |                 }, new JsonSerializerSettings
194 |                 {
195 |                     ReferenceLoopHandling = ReferenceLoopHandling.Ignore
196 |                 });
197 |                 
198 |                 await connectionManager.SendMessageAsync(message);
199 |             }
200 |             catch (Exception ex)
201 |             {
202 |                 Debug.LogError($"[MCP] Error sending game objects details: {ex.Message}");
203 |             }
204 |         }
205 |         
206 |         // Add new method to send pong message back to the server
207 |         public async Task SendPongAsync()
208 |         {
209 |             if (!connectionManager.IsConnected) return;
210 |             
211 |             try
212 |             {
213 |                 var message = JsonConvert.SerializeObject(new
214 |                 {
215 |                     type = "pong",
216 |                     data = new
217 |                     {
218 |                         timestamp = DateTime.UtcNow.Ticks
219 |                     }
220 |                 });
221 |                 
222 |                 await connectionManager.SendMessageAsync(message);
223 |             }
224 |             catch (Exception ex)
225 |             {
226 |                 Debug.LogError($"[MCP] Error sending pong message: {ex.Message}");
227 |             }
228 |         }
229 |         
230 |         // Add new method to handle single GameObject details request
231 |         public async Task SendGameObjectDetailsAsync(string requestId, GameObject gameObject)
232 |         {
233 |             if (!connectionManager.IsConnected || gameObject == null) return;
234 |             
235 |             try
236 |             {
237 |                 // Create a list with a single game object detail
238 |                 var gameObjectDetail = new MCPGameObjectDetail
239 |                 {
240 |                     Name = gameObject.name,
241 |                     InstanceID = gameObject.GetInstanceID(),
242 |                     Path = GetGameObjectPath(gameObject),
243 |                     Active = gameObject.activeSelf,
244 |                     ActiveInHierarchy = gameObject.activeInHierarchy,
245 |                     Tag = gameObject.tag,
246 |                     Layer = gameObject.layer,
247 |                     LayerName = LayerMask.LayerToName(gameObject.layer),
248 |                     IsStatic = gameObject.isStatic,
249 |                     Transform = new MCPTransformInfo
250 |                     {
251 |                         Position = gameObject.transform.position,
252 |                         Rotation = gameObject.transform.rotation.eulerAngles,
253 |                         LocalPosition = gameObject.transform.localPosition,
254 |                         LocalRotation = gameObject.transform.localRotation.eulerAngles,
255 |                         LocalScale = gameObject.transform.localScale
256 |                     },
257 |                     Components = gameObject.GetComponents<Component>()
258 |                         .Where(c => c != null)
259 |                         .Select(c => new MCPComponentInfo
260 |                         {
261 |                             Type = c.GetType().Name,
262 |                             IsEnabled = GetComponentEnabled(c),
263 |                             InstanceID = c.GetInstanceID()
264 |                         })
265 |                         .ToList()
266 |                 };
267 |                 
268 |                 var details = new List<MCPGameObjectDetail> { gameObjectDetail };
269 |                 
270 |                 // Use the existing method to send the details
271 |                 await SendGameObjectsDetailsAsync(requestId, details);
272 |             }
273 |             catch (Exception ex)
274 |             {
275 |                 Debug.LogError($"[MCP] Error sending game object details: {ex.Message}");
276 |                 await SendErrorMessageAsync("GAME_OBJECT_DETAIL_ERROR", ex.Message);
277 |             }
278 |         }
279 |         
280 |         private string GetGameObjectPath(GameObject obj)
281 |         {
282 |             if (obj == null) return string.Empty;
283 |             
284 |             string path = obj.name;
285 |             var parent = obj.transform.parent;
286 |             
287 |             while (parent != null)
288 |             {
289 |                 path = parent.name + "/" + path;
290 |                 parent = parent.parent;
291 |             }
292 |             
293 |             return path;
294 |         }
295 |         
296 |         private bool GetComponentEnabled(Component component)
297 |         {
298 |             // Try to check if component is enabled (for components that support it)
299 |             try
300 |             {
301 |                 if (component is Behaviour behaviour)
302 |                     return behaviour.enabled;
303 |                 
304 |                 if (component is Renderer renderer)
305 |                     return renderer.enabled;
306 |                 
307 |                 if (component is Collider collider)
308 |                     return collider.enabled;
309 |             }
310 |             catch
311 |             {
312 |                 // Ignore any exceptions
313 |             }
314 |             
315 |             // Default to true for components that don't have an enabled property
316 |             return true;
317 |         }
318 |     }
319 | }
320 | 
```

--------------------------------------------------------------------------------
/Editor/MCPConnectionManager.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Net.WebSockets;
  3 | using System.Text;
  4 | using System.Threading;
  5 | using System.Threading.Tasks;
  6 | using UnityEngine;
  7 | 
  8 | namespace Plugins.GamePilot.Editor.MCP
  9 | {
 10 |     public class MCPConnectionManager
 11 |     {
 12 |         private static readonly string ComponentName = "MCPConnectionManager";
 13 |         private ClientWebSocket webSocket;
 14 |         private Uri serverUri = new Uri("ws://localhost:5010"); // Changed to allow changing
 15 |         private readonly CancellationTokenSource cts = new CancellationTokenSource();
 16 |         private bool isConnected = false;
 17 |         private float reconnectTimer = 0f;
 18 |         private readonly float reconnectInterval = 5f;
 19 |         private string lastErrorMessage = string.Empty;
 20 |         
 21 |         // Statistics
 22 |         private int messagesSent = 0;
 23 |         private int messagesReceived = 0;
 24 |         private int reconnectAttempts = 0;
 25 |         
 26 |         // Events
 27 |         public event Action<string> OnMessageReceived;
 28 |         public event Action OnConnected;
 29 |         public event Action OnDisconnected;
 30 |         public event Action<string> OnError;
 31 |         
 32 |         // Properly track the connection state using the WebSocket state and our own flag
 33 |         public bool IsConnected => isConnected && webSocket?.State == WebSocketState.Open;
 34 |         public string LastErrorMessage => lastErrorMessage;
 35 |         public int MessagesSent => messagesSent;
 36 |         public int MessagesReceived => messagesReceived;
 37 |         public int ReconnectAttempts => reconnectAttempts;
 38 |         public Uri ServerUri 
 39 |         { 
 40 |             get => serverUri;
 41 |             set => serverUri = value;
 42 |         }
 43 |         
 44 |         public MCPConnectionManager()
 45 |         {
 46 |             MCPLogger.InitializeComponent(ComponentName);
 47 |             webSocket = new ClientWebSocket();
 48 |             webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
 49 |         }
 50 |         
 51 |         public async void Connect()
 52 |         {
 53 |             // Double check connections that look open but may be stale
 54 |             if (webSocket != null && webSocket.State == WebSocketState.Open)
 55 |             {
 56 |                 try
 57 |                 {
 58 |                     // Try to send a ping to verify connection is truly active
 59 |                     bool connectionIsActive = await TestConnection();
 60 |                     if (connectionIsActive)
 61 |                     {
 62 |                         MCPLogger.Log(ComponentName, "WebSocket already connected and active");
 63 |                         return;
 64 |                     }
 65 |                     else
 66 |                     {
 67 |                         MCPLogger.Log(ComponentName, "WebSocket appears open but is stale, reconnecting...");
 68 |                         // Fall through to reconnection logic
 69 |                     }
 70 |                 }
 71 |                 catch (Exception)
 72 |                 {
 73 |                     MCPLogger.Log(ComponentName, "WebSocket appears open but failed ping test, reconnecting...");
 74 |                     // Fall through to reconnection logic
 75 |                 }
 76 |             }
 77 |             else if (webSocket != null && webSocket.State == WebSocketState.Connecting)
 78 |             {
 79 |                 MCPLogger.Log(ComponentName, "WebSocket is already connecting");
 80 |                 return;
 81 |             }
 82 |             
 83 |             // Clean up any existing socket
 84 |             if (webSocket != null)
 85 |             {
 86 |                 try
 87 |                 {
 88 |                     if (webSocket.State == WebSocketState.Open)
 89 |                     {
 90 |                         await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, 
 91 |                             "Reconnecting", CancellationToken.None);
 92 |                     }
 93 |                     webSocket.Dispose();
 94 |                 }
 95 |                 catch (Exception ex)
 96 |                 {
 97 |                     MCPLogger.LogWarning(ComponentName, $"Error cleaning up WebSocket: {ex.Message}");
 98 |                 }
 99 |             }
100 |             
101 |             webSocket = new ClientWebSocket();
102 |             webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
103 |             
104 |             try
105 |             {
106 |                 MCPLogger.Log(ComponentName, $"Connecting to MCP Server at {serverUri}");
107 |                 
108 |                 var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
109 |                 using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, timeout.Token);
110 |                 
111 |                 await webSocket.ConnectAsync(serverUri, linkedCts.Token);
112 |                 isConnected = true;
113 |                 
114 |                 MCPLogger.Log(ComponentName, "Successfully connected to MCP Server");
115 |                 OnConnected?.Invoke();
116 |                 StartReceiving();
117 |             }
118 |             catch (OperationCanceledException)
119 |             {
120 |                 lastErrorMessage = "Connection attempt timed out";
121 |                 MCPLogger.LogError(ComponentName, lastErrorMessage);
122 |                 OnError?.Invoke(lastErrorMessage);
123 |                 isConnected = false;
124 |                 OnDisconnected?.Invoke();
125 |             }
126 |             catch (WebSocketException we)
127 |             {
128 |                 lastErrorMessage = $"WebSocket error: {we.Message}";
129 |                 MCPLogger.LogError(ComponentName, lastErrorMessage);
130 |                 OnError?.Invoke(lastErrorMessage);
131 |                 isConnected = false;
132 |                 OnDisconnected?.Invoke();
133 |             }
134 |             catch (Exception e)
135 |             {
136 |                 lastErrorMessage = $"Failed to connect: {e.Message}";
137 |                 MCPLogger.LogError(ComponentName, lastErrorMessage);
138 |                 OnError?.Invoke(lastErrorMessage);
139 |                 isConnected = false;
140 |                 OnDisconnected?.Invoke();
141 |             }
142 |         }
143 |         
144 |         // Test if connection is still valid with a simple ping
145 |         private async Task<bool> TestConnection()
146 |         {
147 |             try
148 |             {
149 |                 // Simple ping test - send a 1-byte message
150 |                 byte[] pingData = new byte[1] { 0 };
151 |                 await webSocket.SendAsync(
152 |                     new ArraySegment<byte>(pingData),
153 |                     WebSocketMessageType.Binary,
154 |                     true,
155 |                     CancellationToken.None);
156 |                 
157 |                 return true;
158 |             }
159 |             catch
160 |             {
161 |                 return false;
162 |             }
163 |         }
164 |         
165 |         public void Reconnect()
166 |         {
167 |             reconnectAttempts++;
168 |             MCPLogger.Log(ComponentName, "Manually reconnecting...");
169 |             reconnectTimer = 0;
170 |             Connect();
171 |         }
172 |         
173 |         public void CheckConnection()
174 |         {
175 |             if (!isConnected || webSocket?.State != WebSocketState.Open)
176 |             {
177 |                 reconnectTimer += UnityEngine.Time.deltaTime;
178 |                 
179 |                 if (reconnectTimer >= reconnectInterval)
180 |                 {
181 |                     reconnectAttempts++;
182 |                     MCPLogger.Log(ComponentName, "Attempting reconnection...");
183 |                     reconnectTimer = 0f;
184 |                     Connect();
185 |                 }
186 |             }
187 |         }
188 |         
189 |         private async void StartReceiving()
190 |         {
191 |             var buffer = new byte[8192]; // 8KB buffer
192 |             
193 |             try
194 |             {
195 |                 while (webSocket.State == WebSocketState.Open && !cts.IsCancellationRequested)
196 |                 {
197 |                     var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
198 |                     
199 |                     if (result.MessageType == WebSocketMessageType.Text)
200 |                     {
201 |                         messagesReceived++;
202 |                         var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
203 |                         MCPLogger.Log(ComponentName, $"Received message: {message.Substring(0, Math.Min(100, message.Length))}...");
204 |                         OnMessageReceived?.Invoke(message);
205 |                     }
206 |                     else if (result.MessageType == WebSocketMessageType.Close)
207 |                     {
208 |                         MCPLogger.Log(ComponentName, "Server requested connection close");
209 |                         isConnected = false;
210 |                         OnDisconnected?.Invoke();
211 |                         break;
212 |                     }
213 |                 }
214 |             }
215 |             catch (OperationCanceledException)
216 |             {
217 |                 // Normal cancellation, don't log as error
218 |                 MCPLogger.Log(ComponentName, "WebSocket receiving was canceled");
219 |             }
220 |             catch (WebSocketException wsEx)
221 |             {
222 |                 if (!cts.IsCancellationRequested)
223 |                 {
224 |                     lastErrorMessage = $"WebSocket error: {wsEx.Message}";
225 |                     MCPLogger.LogError(ComponentName, lastErrorMessage);
226 |                     OnError?.Invoke(lastErrorMessage);
227 |                 }
228 |             }
229 |             catch (Exception e)
230 |             {
231 |                 if (!cts.IsCancellationRequested)
232 |                 {
233 |                     lastErrorMessage = $"Connection error: {e.Message}";
234 |                     MCPLogger.LogError(ComponentName, lastErrorMessage);
235 |                     OnError?.Invoke(lastErrorMessage);
236 |                 }
237 |             }
238 |             finally
239 |             {
240 |                 if (isConnected)
241 |                 {
242 |                     isConnected = false;
243 |                     OnDisconnected?.Invoke();
244 |                 }
245 |             }
246 |         }
247 |         
248 |         public async Task SendMessageAsync(string message)
249 |         {
250 |             if (!isConnected || webSocket?.State != WebSocketState.Open)
251 |             {
252 |                 MCPLogger.LogWarning(ComponentName, "Cannot send message: not connected");
253 |                 return;
254 |             }
255 |             
256 |             try
257 |             {
258 |                 var buffer = Encoding.UTF8.GetBytes(message);
259 |                 await webSocket.SendAsync(
260 |                     new ArraySegment<byte>(buffer),
261 |                     WebSocketMessageType.Text,
262 |                     true,
263 |                     cts.Token);
264 |                 
265 |                 messagesSent++;
266 |                 MCPLogger.Log(ComponentName, $"Sent message: {message.Substring(0, Math.Min(100, message.Length))}...");
267 |             }
268 |             catch (OperationCanceledException)
269 |             {
270 |                 // Normal cancellation, don't log as error
271 |                 MCPLogger.Log(ComponentName, "Send operation was canceled");
272 |             }
273 |             catch (WebSocketException wsEx)
274 |             {
275 |                 lastErrorMessage = $"WebSocket send error: {wsEx.Message}";
276 |                 MCPLogger.LogError(ComponentName, lastErrorMessage);
277 |                 OnError?.Invoke(lastErrorMessage);
278 |                 
279 |                 // Connection might be broken, mark as disconnected
280 |                 isConnected = false;
281 |                 OnDisconnected?.Invoke();
282 |             }
283 |             catch (Exception e)
284 |             {
285 |                 lastErrorMessage = $"Failed to send message: {e.Message}";
286 |                 MCPLogger.LogError(ComponentName, lastErrorMessage);
287 |                 OnError?.Invoke(lastErrorMessage);
288 |                 
289 |                 // Connection might be broken, mark as disconnected
290 |                 isConnected = false;
291 |                 OnDisconnected?.Invoke();
292 |             }
293 |         }
294 |         
295 |         public void Disconnect()
296 |         {
297 |             try
298 |             {
299 |                 MCPLogger.Log(ComponentName, "Disconnecting from server");
300 |                 
301 |                 // Cancel any pending operations
302 |                 if (!cts.IsCancellationRequested)
303 |                 {
304 |                     cts.Cancel();
305 |                 }
306 |                 
307 |                 if (webSocket != null && webSocket.State == WebSocketState.Open)
308 |                 {
309 |                     // Begin graceful close
310 |                     var closeTask = webSocket.CloseAsync(
311 |                         WebSocketCloseStatus.NormalClosure,
312 |                         "Client disconnecting",
313 |                         CancellationToken.None);
314 |                     
315 |                     // Give it a moment to close gracefully
316 |                     Task.WaitAny(new[] { closeTask }, 1000);
317 |                 }
318 |                 
319 |                 // Dispose resources
320 |                 if (webSocket != null)
321 |                 {
322 |                     webSocket.Dispose();
323 |                     webSocket = null;
324 |                 }
325 |             }
326 |             catch (Exception e)
327 |             {
328 |                 MCPLogger.LogWarning(ComponentName, $"Error during disconnect: {e.Message}");
329 |             }
330 |             finally
331 |             {
332 |                 isConnected = false;
333 |                 OnDisconnected?.Invoke();
334 |             }
335 |         }
336 |         
337 |         ~MCPConnectionManager()
338 |         {
339 |             // Ensure resources are cleaned up
340 |             Disconnect();
341 |         }
342 |     }
343 | }
344 | 
```

--------------------------------------------------------------------------------
/mcpServer/src/websocketHandler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WebSocketServer, WebSocket } from 'ws';
  2 | import { 
  3 |   UnityMessage, 
  4 |   UnityEditorState, 
  5 |   LogEntry,
  6 |   CommandPromise 
  7 | } from './types.js';
  8 | 
  9 | export class WebSocketHandler {
 10 |   private wsServer!: WebSocketServer; // Add definite assignment assertion
 11 |   private _port: number; // Make this a private field, not readonly
 12 |   private unityConnection: WebSocket | null = null;
 13 |   private editorState: UnityEditorState = {
 14 |     activeGameObjects: [],
 15 |     selectedObjects: [],
 16 |     playModeState: 'Stopped',
 17 |     sceneHierarchy: {}
 18 |   };
 19 |   
 20 |   private logBuffer: LogEntry[] = [];
 21 |   private readonly maxLogBufferSize = 1000;
 22 |   private commandResultPromise: CommandPromise | null = null;
 23 |   private commandStartTime: number | null = null;
 24 |   private lastHeartbeat: number = 0;
 25 |   private connectionEstablished: boolean = false;
 26 |   private pendingRequests: Record<string, {
 27 |     resolve: (data?: any) => void;
 28 |     reject: (reason?: any) => void;
 29 |     type: string;
 30 |   }> = {};
 31 | 
 32 |   constructor(port: number = 5010) {
 33 |     this._port = port; // Store in private field
 34 |     this.initializeWebSocketServer(port);
 35 |   }
 36 | 
 37 |   // Add a getter to expose port as readonly
 38 |   public get port(): number {
 39 |     return this._port;
 40 |   }
 41 | 
 42 |   private initializeWebSocketServer(port: number): void {
 43 |     try {
 44 |       this.wsServer = new WebSocketServer({ port });
 45 |       this.setupWebSocketServer();
 46 |       console.error(`[Unity MCP] WebSocket server started on port ${this._port}`);
 47 |     } catch (error) {
 48 |       console.error(`[Unity MCP] ERROR starting WebSocket server on port ${port}:`, error);
 49 |       this.tryAlternativePort(port);
 50 |     }
 51 |   }
 52 | 
 53 |   private tryAlternativePort(originalPort: number): void {
 54 |     try {
 55 |       const alternativePort = originalPort + 1;
 56 |       console.error(`[Unity MCP] Trying alternative port ${alternativePort}...`);
 57 |       this._port = alternativePort; // Update the private field instead of readonly property
 58 |       this.wsServer = new WebSocketServer({ port: alternativePort });
 59 |       this.setupWebSocketServer();
 60 |       console.error(`[Unity MCP] WebSocket server started on alternative port ${this._port}`);
 61 |     } catch (secondError) {
 62 |       console.error(`[Unity MCP] FATAL: Could not start WebSocket server:`, secondError);
 63 |       throw new Error(`Failed to start WebSocket server: ${secondError}`);
 64 |     }
 65 |   }
 66 | 
 67 |   private setupWebSocketServer() {
 68 |     console.error(`[Unity MCP] WebSocket server starting on port ${this._port}`);
 69 |     
 70 |     this.wsServer.on('listening', () => {
 71 |       console.error('[Unity MCP] WebSocket server is listening for connections');
 72 |     });
 73 |     
 74 |     this.wsServer.on('error', (error) => {
 75 |       console.error('[Unity MCP] WebSocket server error:', error);
 76 |     });
 77 |     
 78 |     this.wsServer.on('connection', this.handleNewConnection.bind(this));
 79 |   }
 80 | 
 81 |   private handleNewConnection(ws: WebSocket): void {
 82 |     console.error('[Unity MCP] Unity Editor connected');
 83 |     this.unityConnection = ws;
 84 |     this.connectionEstablished = true;
 85 |     this.lastHeartbeat = Date.now();
 86 |     
 87 |     // Send a simple handshake message to verify connection
 88 |     this.sendHandshake();
 89 |     
 90 |     ws.on('message', (data) => this.handleIncomingMessage(data));
 91 |     
 92 |     ws.on('error', (error) => {
 93 |       console.error('[Unity MCP] WebSocket error:', error);
 94 |       this.connectionEstablished = false;
 95 |     });
 96 |     
 97 |     ws.on('close', () => {
 98 |       console.error('[Unity MCP] Unity Editor disconnected');
 99 |       this.unityConnection = null;
100 |       this.connectionEstablished = false;
101 |     });
102 |     
103 |     // Keep the automatic heartbeat for internal connection validation
104 |     const pingInterval = setInterval(() => {
105 |       if (ws.readyState === WebSocket.OPEN) {
106 |         this.sendPing();
107 |       } else {
108 |         clearInterval(pingInterval);
109 |       }
110 |     }, 30000); // Send heartbeat every 30 seconds
111 |   }
112 | 
113 |   private handleIncomingMessage(data: any): void {
114 |     try {
115 |       // Update heartbeat on any message
116 |       this.lastHeartbeat = Date.now();
117 |       
118 |       const message = JSON.parse(data.toString());
119 |       console.error('[Unity MCP] Received message type:', message.type);
120 |       
121 |       this.handleUnityMessage(message);
122 |     } catch (error) {
123 |       console.error('[Unity MCP] Error handling message:', error);
124 |     }
125 |   }
126 | 
127 |   private sendHandshake() {
128 |     this.sendToUnity({
129 |       type: 'handshake',
130 |       data: { message: 'MCP Server Connected' }
131 |     });
132 |   }
133 |   
134 |   // Renamed from sendHeartbeat to sendPing for consistency with protocol
135 |   private sendPing() {
136 |     this.sendToUnity({
137 |       type: "ping",
138 |       data: { timestamp: Date.now() }
139 |     });
140 |   }
141 | 
142 |   // Helper method to safely send messages to Unity
143 |   private sendToUnity(message: any): void {
144 |     try {
145 |       if (this.unityConnection && this.unityConnection.readyState === WebSocket.OPEN) {
146 |         this.unityConnection.send(JSON.stringify(message));
147 |       }
148 |     } catch (error) {
149 |       console.error(`[Unity MCP] Error sending message: ${error}`);
150 |       this.connectionEstablished = false;
151 |     }
152 |   }
153 | 
154 |   private handleUnityMessage(message: UnityMessage) {
155 |     switch (message.type) {
156 |       case 'editorState':
157 |         this.editorState = message.data;
158 |         break;
159 |       
160 |       case 'commandResult':
161 |         // Resolve the pending command result promise
162 |         if (this.commandResultPromise) {
163 |           this.commandResultPromise.resolve(message.data);
164 |           this.commandResultPromise = null;
165 |           this.commandStartTime = null;
166 |         }
167 |         break;
168 | 
169 |       case 'log':
170 |         this.addLogEntry(message.data);
171 |         break;
172 |         
173 |       case 'pong':
174 |         // Update heartbeat reception timestamp when receiving pong
175 |         this.lastHeartbeat = Date.now();
176 |         this.connectionEstablished = true;
177 |         break;
178 | 
179 |       case 'sceneInfo':
180 |       case 'gameObjectsDetails':
181 |         this.handleRequestResponse(message);
182 |         break;
183 |       
184 |       default:
185 |         console.error('[Unity MCP] Unknown message type:', message);
186 |         break;
187 |     }
188 |   }
189 | 
190 |   private handleRequestResponse(message: UnityMessage): void {
191 |     const requestId = message.data?.requestId;
192 |     if (requestId && this.pendingRequests[requestId]) {
193 |       // Fix the type issue by checking the property exists first
194 |       if (this.pendingRequests[requestId]) {
195 |         this.pendingRequests[requestId].resolve(message.data);
196 |         delete this.pendingRequests[requestId];
197 |       }
198 |     }
199 |   }
200 | 
201 |   private addLogEntry(logEntry: LogEntry) {
202 |     // Add to buffer, removing oldest if at capacity
203 |     this.logBuffer.push(logEntry);
204 |     if (this.logBuffer.length > this.maxLogBufferSize) {
205 |       this.logBuffer.shift();
206 |     }
207 |   }
208 | 
209 |   public async executeEditorCommand(code: string, timeoutMs: number = 5000): Promise<any> {
210 |     if (!this.isConnected()) {
211 |       throw new Error('Unity Editor is not connected');
212 |     }
213 | 
214 |     try {
215 |       // Start timing the command execution
216 |       this.commandStartTime = Date.now();
217 |       
218 |       // Send the command to Unity
219 |       this.sendToUnity({
220 |         type: 'executeEditorCommand',
221 |         data: { code }
222 |       });
223 | 
224 |       // Wait for result with timeout
225 |       return await Promise.race([
226 |         new Promise((resolve, reject) => {
227 |           this.commandResultPromise = { resolve, reject };
228 |         }),
229 |         new Promise((_, reject) =>
230 |           setTimeout(() => reject(new Error(
231 |             `Command execution timed out after ${timeoutMs/1000} seconds`
232 |           )), timeoutMs)
233 |         )
234 |       ]);
235 |     } catch (error) {
236 |       // Reset command promise state if there's an error
237 |       this.commandResultPromise = null;
238 |       this.commandStartTime = null;
239 |       throw error;
240 |     }
241 |   }
242 | 
243 |   // Return the current editor state - only used by tools, doesn't request updates
244 |   public getEditorState(): UnityEditorState {
245 |     return this.editorState;
246 |   }
247 | 
248 |   public getLogEntries(options: {
249 |     types?: string[],
250 |     count?: number,
251 |     fields?: string[],
252 |     messageContains?: string,
253 |     stackTraceContains?: string,
254 |     timestampAfter?: string,
255 |     timestampBefore?: string
256 |   } = {}): Partial<LogEntry>[] {
257 |     const {
258 |       types,
259 |       count = 100,
260 |       fields,
261 |       messageContains,
262 |       stackTraceContains,
263 |       timestampAfter,
264 |       timestampBefore
265 |     } = options;
266 | 
267 |     // Apply all filters
268 |     let filteredLogs = this.filterLogs(types, messageContains, stackTraceContains, 
269 |                                      timestampAfter, timestampBefore);
270 | 
271 |     // Apply count limit
272 |     filteredLogs = filteredLogs.slice(-count);
273 | 
274 |     // Apply field selection if specified
275 |     if (fields?.length) {
276 |       return this.selectFields(filteredLogs, fields);
277 |     }
278 | 
279 |     return filteredLogs;
280 |   }
281 | 
282 |   private filterLogs(types?: string[], messageContains?: string, 
283 |                    stackTraceContains?: string, timestampAfter?: string, 
284 |                    timestampBefore?: string): LogEntry[] {
285 |     return this.logBuffer.filter(log => {
286 |       // Type filter
287 |       if (types && !types.includes(log.logType)) return false;
288 |       
289 |       // Message content filter
290 |       if (messageContains && !log.message.includes(messageContains)) return false;
291 |       
292 |       // Stack trace content filter
293 |       if (stackTraceContains && !log.stackTrace.includes(stackTraceContains)) return false;
294 |       
295 |       // Timestamp filters
296 |       if (timestampAfter && new Date(log.timestamp) < new Date(timestampAfter)) return false;
297 |       if (timestampBefore && new Date(log.timestamp) > new Date(timestampBefore)) return false;
298 |       
299 |       return true;
300 |     });
301 |   }
302 | 
303 |   private selectFields(logs: LogEntry[], fields: string[]): Partial<LogEntry>[] {
304 |     return logs.map(log => {
305 |       const selectedFields: Partial<LogEntry> = {};
306 |       fields.forEach(field => {
307 |         if (field in log) {
308 |           selectedFields[field as keyof LogEntry] = log[field as keyof LogEntry];
309 |         }
310 |       });
311 |       return selectedFields;
312 |     });
313 |   }
314 | 
315 |   public isConnected(): boolean {
316 |     // More robust connection check
317 |     if (this.unityConnection === null || this.unityConnection.readyState !== WebSocket.OPEN) {
318 |       return false;
319 |     }
320 |     
321 |     // Check if we've received messages from Unity recently
322 |     if (!this.connectionEstablished) {
323 |       return false;
324 |     }
325 |     
326 |     // Check if we've received a heartbeat in the last 60 seconds
327 |     const heartbeatTimeout = 60000; // 60 seconds
328 |     if (Date.now() - this.lastHeartbeat > heartbeatTimeout) {
329 |       console.error('[Unity MCP] Connection may be stale - no recent communication');
330 |       return false;
331 |     }
332 |     
333 |     return true;
334 |   }
335 |   
336 |   public requestEditorState() {
337 |     this.sendToUnity({
338 |       type: 'requestEditorState',
339 |       data: {}
340 |     });
341 |   }
342 | 
343 |   public async requestSceneInfo(detailLevel: string): Promise<any> {
344 |     return this.makeUnityRequest('getSceneInfo', { detailLevel }, 'sceneInfo');
345 |   }
346 |   
347 |   public async requestGameObjectsInfo(instanceIDs: number[], detailLevel: string): Promise<any> {
348 |     return this.makeUnityRequest('getGameObjectsInfo', { instanceIDs, detailLevel }, 'gameObjectDetails');
349 |   }
350 | 
351 |   private async makeUnityRequest(type: string, data: any, resultField: string): Promise<any> {
352 |     if (!this.isConnected()) {
353 |       throw new Error('Unity Editor is not connected');
354 |     }
355 |     
356 |     const requestId = crypto.randomUUID();
357 |     data.requestId = requestId;
358 |     
359 |     // Create a promise that will be resolved when we get the response
360 |     const responsePromise = new Promise((resolve, reject) => {
361 |       const timeout = setTimeout(() => {
362 |         delete this.pendingRequests[requestId];
363 |         reject(new Error(`Request for ${type} timed out`));
364 |       }, 10000); // 10 second timeout
365 |       
366 |       this.pendingRequests[requestId] = {
367 |         resolve: (data) => {
368 |           clearTimeout(timeout);
369 |           resolve(data[resultField]);
370 |         },
371 |         reject,
372 |         type
373 |       };
374 |     });
375 |     
376 |     // Send the request to Unity
377 |     this.sendToUnity({
378 |       type,
379 |       data
380 |     });
381 |     
382 |     return responsePromise;
383 |   }
384 | 
385 |   // Support for file system tools by adding a method to send generic messages
386 |   public async sendMessage(message: string | object) {
387 |     if (this.unityConnection && this.unityConnection.readyState === WebSocket.OPEN) {
388 |       const messageStr = typeof message === 'string' ? message : JSON.stringify(message);
389 |       
390 |       return new Promise<void>((resolve, reject) => {
391 |         this.unityConnection!.send(messageStr, (err) => {
392 |           if (err) {
393 |             reject(err);
394 |           } else {
395 |             resolve();
396 |           }
397 |         });
398 |       });
399 |     }
400 |     return Promise.resolve();
401 |   }
402 |   
403 |   public async close() {
404 |     if (this.unityConnection) {
405 |       try {
406 |         this.unityConnection.close();
407 |       } catch (error) {
408 |         console.error('[Unity MCP] Error closing Unity connection:', error);
409 |       }
410 |       this.unityConnection = null;
411 |     }
412 |     
413 |     return new Promise<void>((resolve) => {
414 |       try {
415 |         this.wsServer.close(() => {
416 |           console.error('[Unity MCP] WebSocket server closed');
417 |           resolve();
418 |         });
419 |       } catch (error) {
420 |         console.error('[Unity MCP] Error closing WebSocket server:', error);
421 |         resolve(); // Resolve anyway to allow the process to exit
422 |       }
423 |     });
424 |   }
425 | }
```

--------------------------------------------------------------------------------
/Editor/MCPMessageHandler.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.IO;
  4 | using System.Threading.Tasks;
  5 | using UnityEditor;
  6 | using UnityEngine;
  7 | using Newtonsoft.Json;
  8 | using Newtonsoft.Json.Linq;
  9 | using System.Linq;
 10 | 
 11 | namespace Plugins.GamePilot.Editor.MCP
 12 | {
 13 |     public class MCPMessageHandler
 14 |     {
 15 |         private readonly MCPDataCollector dataCollector;
 16 |         private readonly MCPCodeExecutor codeExecutor;
 17 |         private readonly MCPMessageSender messageSender;
 18 |         
 19 |         public MCPMessageHandler(MCPDataCollector dataCollector, MCPMessageSender messageSender)
 20 |         {
 21 |             this.dataCollector = dataCollector ?? throw new ArgumentNullException(nameof(dataCollector));
 22 |             this.messageSender = messageSender ?? throw new ArgumentNullException(nameof(messageSender));
 23 |             this.codeExecutor = new MCPCodeExecutor();
 24 |         }
 25 |         
 26 |         public async void HandleMessage(string messageJson)
 27 |         {
 28 |             if (string.IsNullOrEmpty(messageJson)) return;
 29 |             
 30 |             try
 31 |             {
 32 |                 Debug.Log($"[MCP] Received message: {messageJson}");
 33 |                 var message = JsonConvert.DeserializeObject<MCPMessage>(messageJson);
 34 |                 if (message == null) return;
 35 |                 
 36 |                 switch (message.Type)
 37 |                 {
 38 |                     case "selectGameObject":
 39 |                         await HandleSelectGameObjectAsync(message.Data);
 40 |                         break;
 41 |                     
 42 |                     case "togglePlayMode":
 43 |                         await HandleTogglePlayModeAsync();
 44 |                         break;
 45 |                     
 46 |                     case "executeEditorCommand":
 47 |                         await HandleExecuteCommandAsync(message.Data);
 48 |                         break;
 49 |                     
 50 |                     case "requestEditorState": // Consolidated to a single message type
 51 |                         await HandleRequestEditorStateAsync(message.Data);
 52 |                         break;
 53 |                         
 54 |                     case "getLogs":
 55 |                         await HandleGetLogsAsync(message.Data);
 56 |                         break;
 57 |                         
 58 |                     case "handshake":
 59 |                         await HandleHandshakeAsync(message.Data);
 60 |                         break;
 61 |                         
 62 |                     case "getSceneInfo":
 63 |                         await HandleGetSceneInfoAsync(message.Data);
 64 |                         break;
 65 |                         
 66 |                     case "getGameObjectsInfo":
 67 |                         await HandleGetGameObjectsInfoAsync(message.Data);
 68 |                         break;
 69 |                         
 70 |                     case "ping": // Renamed from 'heartbeat' to 'ping' to match protocol
 71 |                         await HandlePingAsync(message.Data);
 72 |                         break;
 73 |                         
 74 |                     default:
 75 |                         Debug.LogWarning($"[MCP] Unknown message type: {message.Type}");
 76 |                         break;
 77 |                 }
 78 |             }
 79 |             catch (Exception ex)
 80 |             {
 81 |                 Debug.LogError($"[MCP] Error handling message: {ex.Message}\nMessage: {messageJson}");
 82 |             }
 83 |         }
 84 |         
 85 |         private async Task HandleHandshakeAsync(JToken data)
 86 |         {
 87 |             try
 88 |             {
 89 |                 string message = data["message"]?.ToString() ?? "Server connected";
 90 |                 Debug.Log($"[MCP] Handshake received: {message}");
 91 |                 
 92 |                 // Send a simple acknowledgment, but don't send full editor state until requested
 93 |                 await messageSender.SendPongAsync();
 94 |             }
 95 |             catch (Exception ex)
 96 |             {
 97 |                 Debug.LogError($"[MCP] Error handling handshake: {ex.Message}");
 98 |             }
 99 |         }
100 |         
101 |         // Add a ping handler to respond to server heartbeats
102 |         private async Task HandlePingAsync(JToken data)
103 |         {
104 |             try
105 |             {
106 |                 // Simply respond with a pong message
107 |                 await messageSender.SendPongAsync();
108 |             }
109 |             catch (Exception ex)
110 |             {
111 |                 Debug.LogError($"[MCP] Error handling ping: {ex.Message}");
112 |             }
113 |         }
114 |         
115 |         private async Task HandleSelectGameObjectAsync(JToken data)
116 |         {
117 |             try
118 |             {
119 |                 string objectPath = data["path"]?.ToString();
120 |                 string requestId = data["requestId"]?.ToString();
121 |                 
122 |                 if (string.IsNullOrEmpty(objectPath)) return;
123 |                 
124 |                 var obj = GameObject.Find(objectPath);
125 |                 if (obj != null)
126 |                 {
127 |                     Selection.activeGameObject = obj;
128 |                     Debug.Log($"[MCP] Selected GameObject: {objectPath}");
129 |                     
130 |                     // If requestId was provided, send back object details
131 |                     if (!string.IsNullOrEmpty(requestId))
132 |                     {
133 |                         // Use the new method to send details for a single GameObject
134 |                         await messageSender.SendGameObjectDetailsAsync(requestId, obj);
135 |                     }
136 |                 }
137 |                 else
138 |                 {
139 |                     Debug.LogWarning($"[MCP] GameObject not found: {objectPath}");
140 |                     
141 |                     if (!string.IsNullOrEmpty(requestId))
142 |                     {
143 |                         await messageSender.SendErrorMessageAsync("OBJECT_NOT_FOUND", $"GameObject not found: {objectPath}");
144 |                     }
145 |                 }
146 |             }
147 |             catch (Exception ex)
148 |             {
149 |                 Debug.LogError($"[MCP] Error selecting GameObject: {ex.Message}");
150 |             }
151 |         }
152 |         
153 |         private async Task HandleTogglePlayModeAsync()
154 |         {
155 |             try
156 |             {
157 |                 EditorApplication.isPlaying = !EditorApplication.isPlaying;
158 |                 Debug.Log($"[MCP] Toggled play mode to: {EditorApplication.isPlaying}");
159 |                 
160 |                 // Send updated editor state after toggling play mode
161 |                 var editorState = dataCollector.GetEditorState();
162 |                 await messageSender.SendEditorStateAsync(editorState);
163 |             }
164 |             catch (Exception ex)
165 |             {
166 |                 Debug.LogError($"[MCP] Error toggling play mode: {ex.Message}");
167 |             }
168 |         }
169 |         
170 |         private async Task HandleExecuteCommandAsync(JToken data)
171 |         {
172 |             try
173 |             {
174 |                 // Support both old and new parameter naming
175 |                 string commandId = data["commandId"]?.ToString() ?? data["id"]?.ToString() ?? Guid.NewGuid().ToString();
176 |                 string code = data["code"]?.ToString();
177 |                 
178 |                 if (string.IsNullOrEmpty(code))
179 |                 {
180 |                     Debug.LogWarning("[MCP] Received empty code to execute");
181 |                     await messageSender.SendErrorMessageAsync("EMPTY_CODE", "Received empty code to execute");
182 |                     return;
183 |                 }
184 |                 
185 |                 Debug.Log($"[MCP] Executing command: {commandId}\n{code}");
186 |                 
187 |                 var result = codeExecutor.ExecuteCode(code);
188 |                 
189 |                 // Send back the results
190 |                 await messageSender.SendCommandResultAsync(
191 |                     commandId,
192 |                     result,
193 |                     codeExecutor.GetLogs(),
194 |                     codeExecutor.GetErrors(),
195 |                     codeExecutor.GetWarnings()
196 |                 );
197 |                 
198 |                 Debug.Log($"[MCP] Command execution completed");
199 |             }
200 |             catch (Exception ex)
201 |             {
202 |                 Debug.LogError($"[MCP] Error executing command: {ex.Message}");
203 |             }
204 |         }
205 |         
206 |         // Renamed from HandleGetEditorStateAsync to HandleRequestEditorStateAsync for clarity
207 |         private async Task HandleRequestEditorStateAsync(JToken data)
208 |         {
209 |             try
210 |             {
211 |                 // Get current editor state with enhanced project info
212 |                 var editorState = GetEnhancedEditorState();
213 |                 
214 |                 // Send it to the server
215 |                 await messageSender.SendEditorStateAsync(editorState);
216 |             }
217 |             catch (Exception ex)
218 |             {
219 |                 Debug.LogError($"[MCP] Error getting editor state: {ex.Message}");
220 |             }
221 |         }
222 |         
223 |         // New method to get enhanced editor state with more project information
224 |         private MCPEditorState GetEnhancedEditorState()
225 |         {
226 |             // Get base editor state from data collector
227 |             var state = dataCollector.GetEditorState();
228 |             
229 |             // Add additional project information
230 |             EnhanceEditorStateWithProjectInfo(state);
231 |             
232 |             return state;
233 |         }
234 |         
235 |         // Add additional project information to the editor state
236 |         private void EnhanceEditorStateWithProjectInfo(MCPEditorState state)
237 |         {
238 |             try
239 |             {
240 |                 // Add information about current rendering pipeline
241 |                 state.RenderPipeline = GetCurrentRenderPipeline();
242 |                 
243 |                 // Add current build target platform
244 |                 state.BuildTarget = EditorUserBuildSettings.activeBuildTarget.ToString();
245 |                 
246 |                 // Add project name
247 |                 state.ProjectName = Application.productName;
248 |                 
249 |                 // Add graphics API info
250 |                 state.GraphicsDeviceType = SystemInfo.graphicsDeviceType.ToString();
251 |                 
252 |                 // Add Unity version
253 |                 state.UnityVersion = Application.unityVersion;
254 |                 
255 |                 // Add current scene name
256 |                 var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
257 |                 state.CurrentSceneName = currentScene.name;
258 |                 state.CurrentScenePath = currentScene.path;
259 |             }
260 |             catch (Exception ex)
261 |             {
262 |                 Debug.LogError($"[MCP] Error enhancing editor state: {ex.Message}");
263 |             }
264 |         }
265 |         
266 |         // Helper to determine current render pipeline
267 |         private string GetCurrentRenderPipeline()
268 |         {
269 |             if (UnityEngine.Rendering.GraphicsSettings.defaultRenderPipeline == null)
270 |                 return "Built-in Render Pipeline";
271 |                 
272 |             var pipelineType = UnityEngine.Rendering.GraphicsSettings.defaultRenderPipeline.GetType().Name;
273 |             
274 |             // Try to make the name more user-friendly
275 |             if (pipelineType.Contains("Universal"))
276 |                 return "Universal Render Pipeline (URP)";
277 |             else if (pipelineType.Contains("HD"))
278 |                 return "High Definition Render Pipeline (HDRP)";
279 |             else if (pipelineType.Contains("Lightweight"))
280 |                 return "Lightweight Render Pipeline (LWRP)";
281 |             else
282 |                 return pipelineType;
283 |         }
284 |         
285 |         private async Task HandleGetLogsAsync(JToken data)
286 |         {
287 |             try
288 |             {
289 |                 string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString();
290 |                 int count = data["count"]?.Value<int>() ?? 50;
291 |                 
292 |                 // Get logs from collector
293 |                 var logs = dataCollector.GetRecentLogs(count);
294 |                 
295 |                 // Send logs back to server
296 |                 await messageSender.SendGetLogsResponseAsync(requestId, logs);
297 |             }
298 |             catch (Exception ex)
299 |             {
300 |                 Debug.LogError($"[MCP] Error getting logs: {ex.Message}");
301 |             }
302 |         }
303 |         
304 |         private async Task HandleGetSceneInfoAsync(JToken data)
305 |         {
306 |             try
307 |             {
308 |                 string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString();
309 |                 string detailLevelStr = data["detailLevel"]?.ToString() ?? "RootObjectsOnly";
310 |                 
311 |                 // Parse the detail level
312 |                 SceneInfoDetail detailLevel;
313 |                 if (!Enum.TryParse(detailLevelStr, true, out detailLevel))
314 |                 {
315 |                     detailLevel = SceneInfoDetail.RootObjectsOnly;
316 |                 }
317 |                 
318 |                 // Get scene info
319 |                 var sceneInfo = dataCollector.GetCurrentSceneInfo(detailLevel);
320 |                 
321 |                 // Send it to the server
322 |                 await messageSender.SendSceneInfoAsync(requestId, sceneInfo);
323 |             }
324 |             catch (Exception ex)
325 |             {
326 |                 Debug.LogError($"[MCP] Error handling getSceneInfo: {ex.Message}");
327 |                 await messageSender.SendErrorMessageAsync("SCENE_INFO_ERROR", ex.Message);
328 |             }
329 |         }
330 |         
331 |         private async Task HandleGetGameObjectsInfoAsync(JToken data)
332 |         {
333 |             try
334 |             {
335 |                 string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString();
336 |                 string detailLevelStr = data["detailLevel"]?.ToString() ?? "BasicInfo";
337 |                 
338 |                 // Get the list of instance IDs
339 |                 int[] instanceIDs;
340 |                 if (data["instanceIDs"] != null && data["instanceIDs"].Type == JTokenType.Array)
341 |                 {
342 |                     instanceIDs = data["instanceIDs"].ToObject<int[]>();
343 |                 }
344 |                 else
345 |                 {
346 |                     await messageSender.SendErrorMessageAsync("INVALID_PARAMS", "instanceIDs array is required");
347 |                     return;
348 |                 }
349 |                 
350 |                 // Parse the detail level
351 |                 GameObjectInfoDetail detailLevel;
352 |                 if (!Enum.TryParse(detailLevelStr, true, out detailLevel))
353 |                 {
354 |                     detailLevel = GameObjectInfoDetail.BasicInfo;
355 |                 }
356 |                 
357 |                 // Get game object details
358 |                 var gameObjectDetails = dataCollector.GetGameObjectsInfo(instanceIDs, detailLevel);
359 |                 
360 |                 // Send to server
361 |                 await messageSender.SendGameObjectsDetailsAsync(requestId, gameObjectDetails);
362 |             }
363 |             catch (Exception ex)
364 |             {
365 |                 Debug.LogError($"[MCP] Error handling getGameObjectsInfo: {ex.Message}");
366 |                 await messageSender.SendErrorMessageAsync("GAME_OBJECT_INFO_ERROR", ex.Message);
367 |             }
368 |         }
369 |     }
370 |     
371 |     internal class MCPMessage
372 |     {
373 |         [JsonProperty("type")]
374 |         public string Type { get; set; }
375 |         
376 |         [JsonProperty("data")]
377 |         public JToken Data { get; set; }
378 |     }
379 | }
380 | 
```

--------------------------------------------------------------------------------
/Editor/MCPDataCollector.cs:
--------------------------------------------------------------------------------

```csharp
  1 | using System;
  2 | using System.Collections.Generic;
  3 | using System.Linq;
  4 | using UnityEditor;
  5 | using UnityEngine;
  6 | 
  7 | namespace Plugins.GamePilot.Editor.MCP
  8 | {
  9 |     public enum SceneInfoDetail
 10 |     {
 11 |         RootObjectsOnly,
 12 |         FullHierarchy
 13 |     }
 14 | 
 15 |     public enum GameObjectInfoDetail
 16 |     {
 17 |         BasicInfo,
 18 |         IncludeComponents,
 19 |         IncludeChildren,
 20 |         IncludeComponentsAndChildren  // New option to include both components and children
 21 |     }
 22 | 
 23 |     public class MCPDataCollector : IDisposable
 24 |     {
 25 |         private readonly Queue<LogEntry> logBuffer = new Queue<LogEntry>();
 26 |         private readonly int maxLogBufferSize = 1000;
 27 |         private bool isLoggingEnabled = true;
 28 |         
 29 |         public MCPDataCollector()
 30 |         {
 31 |             // Start capturing logs
 32 |             Application.logMessageReceived += HandleLogMessage;
 33 |         }
 34 |         
 35 |         public void Dispose()
 36 |         {
 37 |             // Unsubscribe to prevent memory leaks
 38 |             Application.logMessageReceived -= HandleLogMessage;
 39 |         }
 40 |         
 41 |         private void HandleLogMessage(string message, string stackTrace, LogType type)
 42 |         {
 43 |             if (!isLoggingEnabled) return;
 44 |             
 45 |             var logEntry = new LogEntry
 46 |             {
 47 |                 Message = message,
 48 |                 StackTrace = stackTrace,
 49 |                 Type = type,
 50 |                 Timestamp = DateTime.UtcNow
 51 |             };
 52 |             
 53 |             lock (logBuffer)
 54 |             {
 55 |                 logBuffer.Enqueue(logEntry);
 56 |                 while (logBuffer.Count > maxLogBufferSize)
 57 |                 {
 58 |                     logBuffer.Dequeue();
 59 |                 }
 60 |             }
 61 |         }
 62 |         
 63 |         public bool IsLoggingEnabled
 64 |         {
 65 |             get => isLoggingEnabled;
 66 |             set
 67 |             {
 68 |                 if (isLoggingEnabled == value) return;
 69 |                 
 70 |                 isLoggingEnabled = value;
 71 |                 if (value)
 72 |                 {
 73 |                     Application.logMessageReceived += HandleLogMessage;
 74 |                 }
 75 |                 else
 76 |                 {
 77 |                     Application.logMessageReceived -= HandleLogMessage;
 78 |                 }
 79 |             }
 80 |         }
 81 |         
 82 |         public LogEntry[] GetRecentLogs(int count = 50)
 83 |         {
 84 |             lock (logBuffer)
 85 |             {
 86 |                 return logBuffer.Reverse().Take(count).Reverse().ToArray();
 87 |             }
 88 |         }
 89 |         
 90 |         public MCPEditorState GetEditorState()
 91 |         {
 92 |             var state = new MCPEditorState
 93 |             {
 94 |                 ActiveGameObjects = GetActiveGameObjects(),
 95 |                 SelectedObjects = GetSelectedObjects(),
 96 |                 PlayModeState = EditorApplication.isPlaying ? "Playing" : "Stopped",
 97 |                 SceneHierarchy = GetSceneHierarchy(),
 98 |                 Timestamp = DateTime.UtcNow
 99 |             };
100 |             
101 |             return state;
102 |         }
103 |         
104 |         private string[] GetActiveGameObjects()
105 |         {
106 |             try
107 |             {
108 |                 var foundObjects = GameObject.FindObjectsByType<GameObject>(FindObjectsSortMode.None);
109 |                 return foundObjects.Where(o => o != null).Select(obj => obj.name).ToArray();
110 |             }
111 |             catch (Exception ex)
112 |             {
113 |                 Debug.LogError($"[MCP] Error getting active GameObjects: {ex.Message}");
114 |                 return new string[0];
115 |             }
116 |         }
117 |         
118 |         private string[] GetSelectedObjects()
119 |         {
120 |             try
121 |             {
122 |                 return Selection.gameObjects.Where(o => o != null).Select(obj => obj.name).ToArray();
123 |             }
124 |             catch (Exception ex)
125 |             {
126 |                 Debug.LogError($"[MCP] Error getting selected objects: {ex.Message}");
127 |                 return new string[0];
128 |             }
129 |         }
130 |         
131 |         private List<MCPGameObjectInfo> GetSceneHierarchy()
132 |         {
133 |             var hierarchy = new List<MCPGameObjectInfo>();
134 |             
135 |             try
136 |             {
137 |                 var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
138 |                 if (scene.IsValid())
139 |                 {
140 |                     var rootObjects = scene.GetRootGameObjects();
141 |                     foreach (var root in rootObjects.Where(o => o != null))
142 |                     {
143 |                         hierarchy.Add(GetGameObjectHierarchy(root));
144 |                     }
145 |                 }
146 |             }
147 |             catch (Exception ex)
148 |             {
149 |                 Debug.LogError($"[MCP] Error getting scene hierarchy: {ex.Message}");
150 |             }
151 |             
152 |             return hierarchy;
153 |         }
154 |         
155 |         private MCPGameObjectInfo GetGameObjectHierarchy(GameObject obj)
156 |         {
157 |             if (obj == null) return null;
158 |             
159 |             try
160 |             {
161 |                 var info = new MCPGameObjectInfo
162 |                 {
163 |                     Name = obj.name,
164 |                     Path = GetGameObjectPath(obj),
165 |                     Components = obj.GetComponents<Component>()
166 |                         .Where(c => c != null)
167 |                         .Select(c => c.GetType().Name)
168 |                         .ToArray(),
169 |                     Children = new List<MCPGameObjectInfo>(),
170 |                     Active = obj.activeSelf,
171 |                     Layer = obj.layer,
172 |                     Tag = obj.tag
173 |                 };
174 |                 
175 |                 var transform = obj.transform;
176 |                 for (int i = 0; i < transform.childCount; i++)
177 |                 {
178 |                     var childTransform = transform.GetChild(i);
179 |                     if (childTransform != null && childTransform.gameObject != null)
180 |                     {
181 |                         var childInfo = GetGameObjectHierarchy(childTransform.gameObject);
182 |                         if (childInfo != null)
183 |                         {
184 |                             info.Children.Add(childInfo);
185 |                         }
186 |                     }
187 |                 }
188 |                 
189 |                 return info;
190 |             }
191 |             catch (Exception ex)
192 |             {
193 |                 Debug.LogWarning($"[MCP] Error processing GameObject {obj.name}: {ex.Message}");
194 |                 return new MCPGameObjectInfo { Name = obj.name, Path = GetGameObjectPath(obj) };
195 |             }
196 |         }
197 |         
198 |         private string GetGameObjectPath(GameObject obj)
199 |         {
200 |             if (obj == null) return string.Empty;
201 |             
202 |             try
203 |             {
204 |                 string path = obj.name;
205 |                 Transform parent = obj.transform.parent;
206 |                 
207 |                 while (parent != null)
208 |                 {
209 |                     path = parent.name + "/" + path;
210 |                     parent = parent.parent;
211 |                 }
212 |                 
213 |                 return path;
214 |             }
215 |             catch (Exception)
216 |             {
217 |                 return obj.name;
218 |             }
219 |         }
220 | 
221 |         // New method to get current scene information based on requested detail level
222 |         public MCPSceneInfo GetCurrentSceneInfo(SceneInfoDetail detailLevel)
223 |         {
224 |             try
225 |             {
226 |                 var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
227 |                 
228 |                 var sceneInfo = new MCPSceneInfo
229 |                 {
230 |                     Name = scene.name,
231 |                     Path = scene.path,
232 |                     IsDirty = scene.isDirty,
233 |                     RootCount = scene.rootCount
234 |                 };
235 |                 
236 |                 if (scene.IsValid())
237 |                 {
238 |                     var rootObjects = scene.GetRootGameObjects();
239 |                     
240 |                     // Map scene objects based on detail level requested
241 |                     switch (detailLevel)
242 |                     {
243 |                         case SceneInfoDetail.RootObjectsOnly:
244 |                             sceneInfo.RootObjects = rootObjects
245 |                                 .Where(o => o != null)
246 |                                 .Select(o => new MCPGameObjectReference
247 |                                 {
248 |                                     Name = o.name,
249 |                                     InstanceID = o.GetInstanceID(),
250 |                                     Path = GetGameObjectPath(o),
251 |                                     Active = o.activeSelf,
252 |                                     ChildCount = o.transform.childCount
253 |                                 })
254 |                                 .ToList();
255 |                             break;
256 |                             
257 |                         case SceneInfoDetail.FullHierarchy:
258 |                             sceneInfo.RootObjects = rootObjects
259 |                                 .Where(o => o != null)
260 |                                 .Select(o => GetGameObjectReferenceWithChildren(o))
261 |                                 .ToList();
262 |                             break;
263 |                     }
264 |                 }
265 |                 
266 |                 return sceneInfo;
267 |             }
268 |             catch (Exception ex)
269 |             {
270 |                 Debug.LogError($"[MCP] Error getting scene info: {ex.Message}");
271 |                 return new MCPSceneInfo { Name = "Error", ErrorMessage = ex.Message };
272 |             }
273 |         }
274 |         
275 |         // Helper method to create a game object reference with children
276 |         private MCPGameObjectReference GetGameObjectReferenceWithChildren(GameObject obj)
277 |         {
278 |             if (obj == null) return null;
279 |             
280 |             var reference = new MCPGameObjectReference
281 |             {
282 |                 Name = obj.name,
283 |                 InstanceID = obj.GetInstanceID(),
284 |                 Path = GetGameObjectPath(obj),
285 |                 Active = obj.activeSelf,
286 |                 ChildCount = obj.transform.childCount,
287 |                 Children = new List<MCPGameObjectReference>()
288 |             };
289 |             
290 |             // Add all children
291 |             var transform = obj.transform;
292 |             for (int i = 0; i < transform.childCount; i++)
293 |             {
294 |                 var childTransform = transform.GetChild(i);
295 |                 if (childTransform != null && childTransform.gameObject != null)
296 |                 {
297 |                     var childRef = GetGameObjectReferenceWithChildren(childTransform.gameObject);
298 |                     if (childRef != null)
299 |                     {
300 |                         reference.Children.Add(childRef);
301 |                     }
302 |                 }
303 |             }
304 |             
305 |             return reference;
306 |         }
307 |         
308 |         // Get detailed information about specific game objects
309 |         public List<MCPGameObjectDetail> GetGameObjectsInfo(int[] objectInstanceIDs, GameObjectInfoDetail detailLevel)
310 |         {
311 |             var results = new List<MCPGameObjectDetail>();
312 |             
313 |             try
314 |             {
315 |                 foreach (var id in objectInstanceIDs)
316 |                 {
317 |                     var obj = EditorUtility.InstanceIDToObject(id) as GameObject;
318 |                     if (obj != null)
319 |                     {
320 |                         results.Add(GetGameObjectDetail(obj, detailLevel));
321 |                     }
322 |                 }
323 |             }
324 |             catch (Exception ex)
325 |             {
326 |                 Debug.LogError($"[MCP] Error getting game object details: {ex.Message}");
327 |             }
328 |             
329 |             return results;
330 |         }
331 |         
332 |         // Helper method to get detailed info about a game object
333 |         private MCPGameObjectDetail GetGameObjectDetail(GameObject obj, GameObjectInfoDetail detailLevel)
334 |         {
335 |             var detail = new MCPGameObjectDetail
336 |             {
337 |                 Name = obj.name,
338 |                 InstanceID = obj.GetInstanceID(),
339 |                 Path = GetGameObjectPath(obj),
340 |                 Active = obj.activeSelf,
341 |                 ActiveInHierarchy = obj.activeInHierarchy,
342 |                 Tag = obj.tag,
343 |                 Layer = obj.layer,
344 |                 LayerName = LayerMask.LayerToName(obj.layer),
345 |                 IsStatic = obj.isStatic,
346 |                 Transform = new MCPTransformInfo
347 |                 {
348 |                     Position = obj.transform.position,
349 |                     Rotation = obj.transform.rotation.eulerAngles,
350 |                     LocalPosition = obj.transform.localPosition,
351 |                     LocalRotation = obj.transform.localRotation.eulerAngles,
352 |                     LocalScale = obj.transform.localScale
353 |                 }
354 |             };
355 |             
356 |             // Include components if requested
357 |             if (detailLevel == GameObjectInfoDetail.IncludeComponents || 
358 |                 detailLevel == GameObjectInfoDetail.IncludeComponentsAndChildren)
359 |             {
360 |                 detail.Components = obj.GetComponents<Component>()
361 |                     .Where(c => c != null)
362 |                     .Select(c => new MCPComponentInfo
363 |                     {
364 |                         Type = c.GetType().Name,
365 |                         IsEnabled = GetComponentEnabled(c),
366 |                         InstanceID = c.GetInstanceID()
367 |                     })
368 |                     .ToList();
369 |             }
370 |             
371 |             // Include children if requested
372 |             if (detailLevel == GameObjectInfoDetail.IncludeChildren || 
373 |                 detailLevel == GameObjectInfoDetail.IncludeComponentsAndChildren)
374 |             {
375 |                 detail.Children = new List<MCPGameObjectDetail>();
376 |                 var transform = obj.transform;
377 |                 
378 |                 for (int i = 0; i < transform.childCount; i++)
379 |                 {
380 |                     var childTransform = transform.GetChild(i);
381 |                     if (childTransform != null && childTransform.gameObject != null)
382 |                     {
383 |                         // When including both components and children, make sure to include components for children too
384 |                         GameObjectInfoDetail childDetailLevel = detailLevel == GameObjectInfoDetail.IncludeComponentsAndChildren 
385 |                             ? GameObjectInfoDetail.IncludeComponentsAndChildren 
386 |                             : GameObjectInfoDetail.BasicInfo;
387 |                             
388 |                         var childDetail = GetGameObjectDetail(
389 |                             childTransform.gameObject, 
390 |                             childDetailLevel);
391 |                             
392 |                         detail.Children.Add(childDetail);
393 |                     }
394 |                 }
395 |             }
396 |             
397 |             return detail;
398 |         }
399 |         
400 |         // Helper for getting component enabled state
401 |         private bool GetComponentEnabled(Component component)
402 |         {
403 |             // Try to check if component is enabled (for components that support it)
404 |             try
405 |             {
406 |                 if (component is Behaviour behaviour)
407 |                     return behaviour.enabled;
408 |                 
409 |                 if (component is Renderer renderer)
410 |                     return renderer.enabled;
411 |                 
412 |                 if (component is Collider collider)
413 |                     return collider.enabled;
414 |             }
415 |             catch
416 |             {
417 |                 // Ignore any exceptions
418 |             }
419 |             
420 |             // Default to true for components that don't have an enabled property
421 |             return true;
422 |         }
423 |     }
424 | }
425 | 
```

--------------------------------------------------------------------------------
/mcpServer/src/filesystemTools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  2 | import { WebSocketHandler } from './websocketHandler.js';
  3 | import fs from 'fs/promises';
  4 | import { Dirent } from 'fs'; // Import Dirent instead of DirEnt
  5 | import path from 'path';
  6 | import { createTwoFilesPatch } from 'diff';
  7 | import { minimatch } from 'minimatch';
  8 | import {
  9 |   ReadFileArgsSchema,
 10 |   ReadMultipleFilesArgsSchema,
 11 |   WriteFileArgsSchema,
 12 |   EditFileArgsSchema,
 13 |   ListDirectoryArgsSchema,
 14 |   DirectoryTreeArgsSchema,
 15 |   SearchFilesArgsSchema,
 16 |   GetFileInfoArgsSchema,
 17 |   FindAssetsByTypeArgsSchema
 18 | } from './toolDefinitions.js';
 19 | 
 20 | // Interface definitions
 21 | interface FileInfo {
 22 |   size: number;
 23 |   created: Date;
 24 |   modified: Date;
 25 |   accessed: Date;
 26 |   isDirectory: boolean;
 27 |   isFile: boolean;
 28 |   permissions: string;
 29 | }
 30 | 
 31 | interface TreeEntry {
 32 |   name: string;
 33 |   type: 'file' | 'directory';
 34 |   children?: TreeEntry[];
 35 | }
 36 | 
 37 | // Helper functions
 38 | async function validatePath(requestedPath: string, assetRootPath: string): Promise<string> {
 39 |   // If path is empty or just quotes, use the asset root path directly
 40 |   if (!requestedPath || requestedPath.trim() === '' || requestedPath.trim() === '""' || requestedPath.trim() === "''") {
 41 |     console.error(`[Unity MCP] Using asset root path: ${assetRootPath}`);
 42 |     return assetRootPath;
 43 |   }
 44 |   
 45 |   // Clean the path to remove any unexpected quotes or escape characters
 46 |   let cleanPath = requestedPath.replace(/['"\\]/g, '');
 47 |   
 48 |   // Handle empty path after cleaning
 49 |   if (!cleanPath || cleanPath.trim() === '') {
 50 |     return assetRootPath;
 51 |   }
 52 |   
 53 |   // Normalize path to handle both Windows and Unix-style paths
 54 |   const normalized = path.normalize(cleanPath);
 55 |   
 56 |   // Resolve the path (absolute or relative)
 57 |   let absolute = resolvePathToAssetRoot(normalized, assetRootPath);
 58 |   const resolvedPath = path.resolve(absolute);
 59 |   
 60 |   // Ensure we don't escape out of the Unity project folder
 61 |   validatePathSecurity(resolvedPath, assetRootPath, requestedPath);
 62 |   
 63 |   return resolvedPath;
 64 | }
 65 | 
 66 | function resolvePathToAssetRoot(pathToResolve: string, assetRootPath: string): string {
 67 |   if (path.isAbsolute(pathToResolve)) {
 68 |     console.error(`[Unity MCP] Absolute path requested: ${pathToResolve}`);
 69 |     
 70 |     // If the absolute path is outside the project, try alternative resolutions
 71 |     if (!pathToResolve.startsWith(assetRootPath)) {
 72 |       // Try 1: Treat as relative path
 73 |       const tryRelative = path.join(assetRootPath, pathToResolve);
 74 |       try {
 75 |         fs.access(tryRelative);
 76 |         console.error(`[Unity MCP] Treating as relative path: ${tryRelative}`);
 77 |         return tryRelative;
 78 |       } catch {
 79 |         // Try 2: Try to extract path relative to Assets if it contains "Assets"
 80 |         if (pathToResolve.includes('Assets')) {
 81 |           const assetsIndex = pathToResolve.indexOf('Assets');
 82 |           const relativePath = pathToResolve.substring(assetsIndex + 7); // +7 to skip "Assets/"
 83 |           const newPath = path.join(assetRootPath, relativePath);
 84 |           console.error(`[Unity MCP] Trying via Assets path: ${newPath}`);
 85 |           
 86 |           try {
 87 |             fs.access(newPath);
 88 |             return newPath;
 89 |           } catch { /* Use original if all else fails */ }
 90 |         }
 91 |       }
 92 |     }
 93 |     return pathToResolve;
 94 |   } else {
 95 |     // For relative paths, join with asset root path
 96 |     return path.join(assetRootPath, pathToResolve);
 97 |   }
 98 | }
 99 | 
100 | function validatePathSecurity(resolvedPath: string, assetRootPath: string, requestedPath: string): void {
101 |   if (!resolvedPath.startsWith(assetRootPath) && requestedPath.trim() !== '') {
102 |     console.error(`[Unity MCP] Access denied: Path ${requestedPath} is outside the project directory`);
103 |     console.error(`[Unity MCP] Resolved to: ${resolvedPath}`);
104 |     console.error(`[Unity MCP] Expected to be within: ${assetRootPath}`);
105 |     throw new Error(`Access denied: Path ${requestedPath} is outside the Unity project directory`);
106 |   }
107 | }
108 | 
109 | async function getFileStats(filePath: string): Promise<FileInfo> {
110 |   const stats = await fs.stat(filePath);
111 |   return {
112 |     size: stats.size,
113 |     created: stats.birthtime,
114 |     modified: stats.mtime,
115 |     accessed: stats.atime,
116 |     isDirectory: stats.isDirectory(),
117 |     isFile: stats.isFile(),
118 |     permissions: stats.mode.toString(8).slice(-3),
119 |   };
120 | }
121 | 
122 | async function searchFiles(
123 |   rootPath: string,
124 |   pattern: string,
125 |   excludePatterns: string[] = []
126 | ): Promise<string[]> {
127 |   const results: string[] = [];
128 | 
129 |   async function search(currentPath: string) {
130 |     const entries = await fs.readdir(currentPath, { withFileTypes: true });
131 | 
132 |     for (const entry of entries) {
133 |       const fullPath = path.join(currentPath, entry.name);
134 | 
135 |       try {
136 |         // Check if path matches any exclude pattern
137 |         const relativePath = path.relative(rootPath, fullPath);
138 |         if (isPathExcluded(relativePath, excludePatterns)) continue;
139 | 
140 |         if (entry.name.toLowerCase().includes(pattern.toLowerCase())) {
141 |           results.push(fullPath);
142 |         }
143 | 
144 |         if (entry.isDirectory()) {
145 |           await search(fullPath);
146 |         }
147 |       } catch (error) {
148 |         // Skip invalid paths during search
149 |         continue;
150 |       }
151 |     }
152 |   }
153 | 
154 |   await search(rootPath);
155 |   return results;
156 | }
157 | 
158 | function isPathExcluded(relativePath: string, excludePatterns: string[]): boolean {
159 |   return excludePatterns.some(pattern => {
160 |     const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`;
161 |     return minimatch(relativePath, globPattern, { dot: true });
162 |   });
163 | }
164 | 
165 | function normalizeLineEndings(text: string): string {
166 |   return text.replace(/\r\n/g, '\n');
167 | }
168 | 
169 | function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string {
170 |   // Ensure consistent line endings for diff
171 |   const normalizedOriginal = normalizeLineEndings(originalContent);
172 |   const normalizedNew = normalizeLineEndings(newContent);
173 | 
174 |   return createTwoFilesPatch(
175 |     filepath,
176 |     filepath,
177 |     normalizedOriginal,
178 |     normalizedNew,
179 |     'original',
180 |     'modified'
181 |   );
182 | }
183 | 
184 | async function applyFileEdits(
185 |   filePath: string,
186 |   edits: Array<{oldText: string, newText: string}>,
187 |   dryRun = false
188 | ): Promise<string> {
189 |   // Read file content and normalize line endings
190 |   const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
191 | 
192 |   // Apply edits sequentially
193 |   let modifiedContent = content;
194 |   for (const edit of edits) {
195 |     const normalizedOld = normalizeLineEndings(edit.oldText);
196 |     const normalizedNew = normalizeLineEndings(edit.newText);
197 | 
198 |     // If exact match exists, use it
199 |     if (modifiedContent.includes(normalizedOld)) {
200 |       modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
201 |       continue;
202 |     }
203 | 
204 |     // Try line-by-line matching with whitespace flexibility
205 |     modifiedContent = applyFlexibleLineEdit(modifiedContent, normalizedOld, normalizedNew);
206 |   }
207 | 
208 |   // Create unified diff
209 |   const diff = createUnifiedDiff(content, modifiedContent, filePath);
210 |   const formattedDiff = formatDiff(diff);
211 | 
212 |   if (!dryRun) {
213 |     await fs.writeFile(filePath, modifiedContent, 'utf-8');
214 |   }
215 | 
216 |   return formattedDiff;
217 | }
218 | 
219 | function applyFlexibleLineEdit(content: string, oldText: string, newText: string): string {
220 |   const oldLines = oldText.split('\n');
221 |   const contentLines = content.split('\n');
222 |   
223 |   for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
224 |     const potentialMatch = contentLines.slice(i, i + oldLines.length);
225 | 
226 |     // Compare lines with normalized whitespace
227 |     const isMatch = oldLines.every((oldLine, j) => {
228 |       const contentLine = potentialMatch[j];
229 |       return oldLine.trim() === contentLine.trim();
230 |     });
231 | 
232 |     if (isMatch) {
233 |       // Preserve indentation
234 |       const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
235 |       const newLines = newText.split('\n').map((line, j) => {
236 |         if (j === 0) return originalIndent + line.trimStart();
237 |         
238 |         const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || '';
239 |         const newIndent = line.match(/^\s*/)?.[0] || '';
240 |         
241 |         if (oldIndent && newIndent) {
242 |           const relativeIndent = newIndent.length - oldIndent.length;
243 |           return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart();
244 |         }
245 |         return line;
246 |       });
247 | 
248 |       contentLines.splice(i, oldLines.length, ...newLines);
249 |       return contentLines.join('\n');
250 |     }
251 |   }
252 | 
253 |   throw new Error(`Could not find exact match for edit:\n${oldText}`);
254 | }
255 | 
256 | function formatDiff(diff: string): string {
257 |   let numBackticks = 3;
258 |   while (diff.includes('`'.repeat(numBackticks))) {
259 |     numBackticks++;
260 |   }
261 |   return `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
262 | }
263 | 
264 | async function buildDirectoryTree(currentPath: string, assetRootPath: string, maxDepth: number = 5, currentDepth: number = 0): Promise<TreeEntry[]> {
265 |   if (currentDepth >= maxDepth) {
266 |     return [{ name: "...", type: "directory" }];
267 |   }
268 |   
269 |   const validPath = await validatePath(currentPath, assetRootPath);
270 |   const entries = await fs.readdir(validPath, { withFileTypes: true });
271 |   const result: TreeEntry[] = [];
272 | 
273 |   for (const entry of entries) {
274 |     const entryData: TreeEntry = {
275 |       name: entry.name,
276 |       type: entry.isDirectory() ? 'directory' : 'file'
277 |     };
278 | 
279 |     if (entry.isDirectory()) {
280 |       const subPath = path.join(currentPath, entry.name);
281 |       entryData.children = await buildDirectoryTree(subPath, assetRootPath, maxDepth, currentDepth + 1);
282 |     }
283 | 
284 |     result.push(entryData);
285 |   }
286 | 
287 |   return result;
288 | }
289 | 
290 | // Function to recognize Unity asset types based on file extension
291 | function getUnityAssetType(filePath: string): string {
292 |   const ext = path.extname(filePath).toLowerCase();
293 |   
294 |   // Common Unity asset types
295 |   const assetTypes: Record<string, string> = {
296 |     '.unity': 'Scene',
297 |     '.prefab': 'Prefab',
298 |     '.mat': 'Material',
299 |     '.fbx': 'Model',
300 |     '.cs': 'Script',
301 |     '.anim': 'Animation',
302 |     '.controller': 'Animator Controller',
303 |     '.asset': 'ScriptableObject',
304 |     '.png': 'Texture',
305 |     '.jpg': 'Texture',
306 |     '.jpeg': 'Texture',
307 |     '.tga': 'Texture',
308 |     '.wav': 'Audio',
309 |     '.mp3': 'Audio',
310 |     '.ogg': 'Audio',
311 |     '.shader': 'Shader',
312 |     '.compute': 'Compute Shader',
313 |     '.ttf': 'Font',
314 |     '.otf': 'Font',
315 |     '.physicMaterial': 'Physics Material',
316 |     '.mask': 'Avatar Mask',
317 |     '.playable': 'Playable',
318 |     '.mixer': 'Audio Mixer',
319 |     '.renderTexture': 'Render Texture',
320 |     '.lighting': 'Lighting Settings',
321 |     '.shadervariants': 'Shader Variants',
322 |     '.spriteatlas': 'Sprite Atlas',
323 |     '.guiskin': 'GUI Skin',
324 |     '.flare': 'Flare',
325 |     '.brush': 'Brush',
326 |     '.overrideController': 'Animator Override Controller',
327 |     '.preset': 'Preset',
328 |     '.terrainlayer': 'Terrain Layer',
329 |     '.signal': 'Signal',
330 |     '.signalasset': 'Signal Asset',
331 |     '.giparams': 'Global Illumination Parameters',
332 |     '.cubemap': 'Cubemap',
333 |   };
334 |   
335 |   return assetTypes[ext] || 'Other';
336 | }
337 | 
338 | // Get file extensions for Unity asset types
339 | function getFileExtensionsForType(type: string): string[] {
340 |   type = type.toLowerCase();
341 |   const extensionMap: Record<string, string[]> = {
342 |     'scene': ['.unity'],
343 |     'prefab': ['.prefab'],
344 |     'material': ['.mat'],
345 |     'script': ['.cs'],
346 |     'model': ['.fbx', '.obj', '.blend', '.max', '.mb', '.ma'],
347 |     'texture': ['.png', '.jpg', '.jpeg', '.tga', '.tif', '.tiff', '.psd', '.exr', '.hdr'],
348 |     'audio': ['.wav', '.mp3', '.ogg', '.aiff', '.aif'],
349 |     'animation': ['.anim'],
350 |     'animator': ['.controller'],
351 |     'shader': ['.shader', '.compute', '.cginc']
352 |   };
353 |   
354 |   return extensionMap[type] || [];
355 | }
356 | 
357 | // Handler function to process filesystem tools
358 | export async function handleFilesystemTool(name: string, args: any, projectPath: string) {
359 |   try {
360 |     switch (name) {
361 |       case "read_file": {
362 |         const parsed = ReadFileArgsSchema.safeParse(args);
363 |         if (!parsed.success) return invalidArgsResponse(parsed.error);
364 |         
365 |         const validPath = await validatePath(parsed.data.path, projectPath);
366 |         const content = await fs.readFile(validPath, "utf-8");
367 |         return { content: [{ type: "text", text: content }] };
368 |       }
369 | 
370 |       case "read_multiple_files": {
371 |         const parsed = ReadMultipleFilesArgsSchema.safeParse(args);
372 |         if (!parsed.success) return invalidArgsResponse(parsed.error);
373 |         
374 |         const results = await Promise.all(
375 |           parsed.data.paths.map(async (filePath: string) => {
376 |             try {
377 |               const validPath = await validatePath(filePath, projectPath);
378 |               const content = await fs.readFile(validPath, "utf-8");
379 |               return `${filePath}:\n${content}\n`;
380 |             } catch (error) {
381 |               return `${filePath}: Error - ${getErrorMessage(error)}`;
382 |             }
383 |           }),
384 |         );
385 |         return { content: [{ type: "text", text: results.join("\n---\n") }] };
386 |       }
387 | 
388 |       case "write_file": {
389 |         const parsed = WriteFileArgsSchema.safeParse(args);
390 |         if (!parsed.success) return invalidArgsResponse(parsed.error);
391 |         
392 |         const validPath = await validatePath(parsed.data.path, projectPath);
393 |         
394 |         // Ensure directory exists
395 |         const dirPath = path.dirname(validPath);
396 |         await fs.mkdir(dirPath, { recursive: true });
397 |         
398 |         await fs.writeFile(validPath, parsed.data.content, "utf-8");
399 |         return { 
400 |           content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }]
401 |         };
402 |       }
403 | 
404 |       case "edit_file": {
405 |         const parsed = EditFileArgsSchema.safeParse(args);
406 |         if (!parsed.success) return invalidArgsResponse(parsed.error);
407 |         
408 |         const validPath = await validatePath(parsed.data.path, projectPath);
409 |         const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun);
410 |         return { content: [{ type: "text", text: result }] };
411 |       }
412 | 
413 |       case "list_directory": {
414 |         const parsed = ListDirectoryArgsSchema.safeParse(args);
415 |         if (!parsed.success) return invalidArgsResponse(parsed.error);
416 |         
417 |         const validPath = await validatePath(parsed.data.path, projectPath);
418 |         const entries = await fs.readdir(validPath, { withFileTypes: true });
419 |         const formatted = entries
420 |           .map((entry) => formatDirectoryEntry(entry, validPath))
421 |           .join("\n");
422 |         return { content: [{ type: "text", text: formatted }] };
423 |       }
424 | 
425 |       case "directory_tree": {
426 |         const parsed = DirectoryTreeArgsSchema.safeParse(args);
427 |         if (!parsed.success) return invalidArgsResponse(parsed.error);
428 |         
429 |         const treeData = await buildDirectoryTree(parsed.data.path, projectPath, parsed.data.maxDepth);
430 |         return { content: [{ type: "text", text: JSON.stringify(treeData, null, 2) }] };
431 |       }
432 | 
433 |       case "search_files": {
434 |         const parsed = SearchFilesArgsSchema.safeParse(args);
435 |         if (!parsed.success) return invalidArgsResponse(parsed.error);
436 |         
437 |         const validPath = await validatePath(parsed.data.path, projectPath);
438 |         const results = await searchFiles(validPath, parsed.data.pattern, parsed.data.excludePatterns);
439 |         return { 
440 |           content: [{ 
441 |             type: "text", 
442 |             text: results.length > 0 
443 |               ? `Found ${results.length} results:\n${results.join("\n")}` 
444 |               : "No matches found" 
445 |           }]
446 |         };
447 |       }
448 | 
449 |       case "get_file_info": {
450 |         const parsed = GetFileInfoArgsSchema.safeParse(args);
451 |         if (!parsed.success) return invalidArgsResponse(parsed.error);
452 |         
453 |         const validPath = await validatePath(parsed.data.path, projectPath);
454 |         const info = await getFileStats(validPath);
455 |         
456 |         // Add Unity-specific info if it's an asset file
457 |         const additionalInfo: Record<string, string> = {};
458 |         if (info.isFile) {
459 |           additionalInfo.assetType = getUnityAssetType(validPath);
460 |         }
461 |         
462 |         const formattedInfo = Object.entries({ ...info, ...additionalInfo })
463 |           .map(([key, value]) => `${key}: ${value}`)
464 |           .join("\n");
465 |           
466 |         return { content: [{ type: "text", text: formattedInfo }] };
467 |       }
468 | 
469 |       case "find_assets_by_type": {
470 |         const parsed = FindAssetsByTypeArgsSchema.safeParse(args);
471 |         if (!parsed.success) return invalidArgsResponse(parsed.error);
472 |         
473 |         const assetType = parsed.data.assetType.replace(/['"]/g, '');
474 |         const searchPath = parsed.data.searchPath.replace(/['"]/g, '');
475 |         const maxDepth = parsed.data.maxDepth;
476 |         
477 |         console.error(`[Unity MCP] Finding assets of type "${assetType}" in path "${searchPath}" with maxDepth ${maxDepth}`);
478 |         
479 |         const validPath = await validatePath(searchPath, projectPath);
480 |         const results = await findAssetsByType(assetType, validPath, maxDepth, projectPath);
481 |         
482 |         return {
483 |           content: [{ 
484 |             type: "text", 
485 |             text: results.length > 0 
486 |               ? `Found ${results.length} ${assetType} assets:\n${JSON.stringify(results, null, 2)}` 
487 |               : `No "${assetType}" assets found in ${searchPath || "Assets"}` 
488 |           }]
489 |         };
490 |       }
491 | 
492 |       default:
493 |         return {
494 |           content: [{ type: "text", text: `Unknown tool: ${name}` }],
495 |           isError: true,
496 |         };
497 |     }
498 |   } catch (error) {
499 |     return {
500 |       content: [{ type: "text", text: `Error: ${getErrorMessage(error)}` }],
501 |       isError: true,
502 |     };
503 |   }
504 | }
505 | 
506 | function getErrorMessage(error: unknown): string {
507 |   return error instanceof Error ? error.message : String(error);
508 | }
509 | 
510 | function invalidArgsResponse(error: any) {
511 |   return {
512 |     content: [{ type: "text", text: `Invalid arguments: ${error}` }],
513 |     isError: true
514 |   };
515 | }
516 | 
517 | // Fixed function to use proper Dirent type
518 | function formatDirectoryEntry(entry: Dirent, basePath: string): string {
519 |   if (entry.isDirectory()) {
520 |     return `[DIR] ${entry.name}`;
521 |   } else {
522 |     // For files, detect Unity asset type
523 |     const filePath = path.join(basePath, entry.name);
524 |     const assetType = getUnityAssetType(filePath);
525 |     return `[${assetType}] ${entry.name}`;
526 |   }
527 | }
528 | 
529 | async function findAssetsByType(
530 |   assetType: string, 
531 |   searchPath: string, 
532 |   maxDepth: number, 
533 |   projectPath: string
534 | ): Promise<Array<{path: string, name: string, type: string}>> {
535 |   const results: Array<{path: string, name: string, type: string}> = [];
536 |   const extensions = getFileExtensionsForType(assetType);
537 |   
538 |   async function searchAssets(dir: string, currentDepth: number = 1) {
539 |     // Stop recursion if we've reached the maximum depth
540 |     if (maxDepth !== -1 && currentDepth > maxDepth) {
541 |       return;
542 |     }
543 |     
544 |     try {
545 |       const entries = await fs.readdir(dir, { withFileTypes: true });
546 |       
547 |       for (const entry of entries) {
548 |         const fullPath = path.join(dir, entry.name);
549 |         const relativePath = path.relative(projectPath, fullPath);
550 |         
551 |         if (entry.isDirectory()) {
552 |           // Recursively search subdirectories
553 |           await searchAssets(fullPath, currentDepth + 1);
554 |         } else {
555 |           // Check if the file matches the requested asset type
556 |           const ext = path.extname(entry.name).toLowerCase();
557 |           
558 |           if (extensions.length === 0) {
559 |             // If no extensions specified, match by Unity asset type
560 |             const fileAssetType = getUnityAssetType(fullPath);
561 |             if (fileAssetType.toLowerCase() === assetType.toLowerCase()) {
562 |               results.push({
563 |                 path: relativePath,
564 |                 name: entry.name,
565 |                 type: fileAssetType
566 |               });
567 |             }
568 |           } else if (extensions.includes(ext)) {
569 |             // Match by extension
570 |             results.push({
571 |               path: relativePath,
572 |               name: entry.name,
573 |               type: assetType
574 |             });
575 |           }
576 |         }
577 |       }
578 |     } catch (error) {
579 |       console.error(`Error accessing directory ${dir}:`, error);
580 |     }
581 |   }
582 |   
583 |   await searchAssets(searchPath);
584 |   return results;
585 | }
586 | 
587 | // This function is deprecated and now just a stub
588 | export function registerFilesystemTools(server: Server, wsHandler: WebSocketHandler) {
589 |   console.log("Filesystem tools are now registered in toolDefinitions.ts");
590 | }
591 | 
```

--------------------------------------------------------------------------------
/mcpServer/src/toolDefinitions.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | import { WebSocketHandler } from './websocketHandler.js';
  3 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
  4 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  5 | import { zodToJsonSchema } from 'zod-to-json-schema';
  6 | import {
  7 |   CallToolRequestSchema,
  8 |   ListToolsRequestSchema
  9 | } from '@modelcontextprotocol/sdk/types.js';
 10 | import path from 'path';
 11 | // Import handleFilesystemTool using ES module syntax instead of require
 12 | import { handleFilesystemTool } from './filesystemTools.js';
 13 | 
 14 | // File operation schemas - defined here to be used in tool definitions
 15 | export const ReadFileArgsSchema = z.object({
 16 |   path: z.string().describe('Path to the file to read. Can be absolute or relative to Unity project Assets folder. If empty, defaults to the Assets folder.'),
 17 | });
 18 | 
 19 | export const ReadMultipleFilesArgsSchema = z.object({
 20 |   paths: z.array(z.string()).describe('Array of file paths to read. Paths can be absolute or relative to Unity project Assets folder.'),
 21 | });
 22 | 
 23 | export const WriteFileArgsSchema = z.object({
 24 |   path: z.string().describe('Path to the file to write. Can be absolute or relative to Unity project Assets folder. If empty, defaults to the Assets folder.'),
 25 |   content: z.string().describe('Content to write to the file'),
 26 | });
 27 | 
 28 | export const EditOperation = z.object({
 29 |   oldText: z.string().describe('Text to search for - must match exactly'),
 30 |   newText: z.string().describe('Text to replace with')
 31 | });
 32 | 
 33 | export const EditFileArgsSchema = z.object({
 34 |   path: z.string().describe('Path to the file to edit. Can be absolute or relative to Unity project Assets folder. If empty, defaults to the Assets folder.'),
 35 |   edits: z.array(EditOperation).describe('Array of edit operations to apply'),
 36 |   dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')
 37 | });
 38 | 
 39 | export const ListDirectoryArgsSchema = z.object({
 40 |   path: z.string().describe('Path to the directory to list. Can be absolute or relative to Unity project Assets folder. If empty, defaults to the Assets folder. Example: "Scenes" will list all files in the Assets/Scenes directory.'),
 41 | });
 42 | 
 43 | export const DirectoryTreeArgsSchema = z.object({
 44 |   path: z.string().describe('Path to the directory to get tree of. Can be absolute or relative to Unity project Assets folder. If empty, defaults to the Assets folder. Example: "Prefabs" will show the tree for Assets/Prefabs.'),
 45 |   maxDepth: z.number().optional().default(5).describe('Maximum depth to traverse'),
 46 | });
 47 | 
 48 | export const SearchFilesArgsSchema = z.object({
 49 |   path: z.string().describe('Path to search from. Can be absolute or relative to Unity project Assets folder. If empty, defaults to the Assets folder. Example: "Scripts" will search within Assets/Scripts.'),
 50 |   pattern: z.string().describe('Pattern to search for'),
 51 |   excludePatterns: z.array(z.string()).optional().default([]).describe('Patterns to exclude')
 52 | });
 53 | 
 54 | export const GetFileInfoArgsSchema = z.object({
 55 |   path: z.string().describe('Path to the file to get info for. Can be absolute or relative to Unity project Assets folder. If empty, defaults to the Assets folder.'),
 56 | });
 57 | 
 58 | export const FindAssetsByTypeArgsSchema = z.object({
 59 |   assetType: z.string().describe('Type of assets to find (e.g., "Material", "Prefab", "Scene", "Script")'),
 60 |   searchPath: z.string().optional().default("").describe('Directory to search in. Can be absolute or relative to Unity project Assets folder. An empty string will search the entire Assets folder.'),
 61 |   maxDepth: z.number().optional().default(1).describe('Maximum depth to search. 1 means search only in the specified directory, 2 includes immediate subdirectories, and so on. Set to -1 for unlimited depth.'),
 62 | });
 63 | 
 64 | export function registerTools(server: Server, wsHandler: WebSocketHandler) {
 65 |   // Determine project path from environment variable (which now should include 'Assets')
 66 |   const projectPath = process.env.UNITY_PROJECT_PATH || path.resolve(process.cwd());
 67 |   const projectRootPath = projectPath.endsWith(`Assets${path.sep}`) 
 68 |     ? projectPath.slice(0, -7) // Remove 'Assets/'
 69 |     : projectPath;
 70 | 
 71 |   console.error(`[Unity MCP ToolDefinitions] Using project path: ${projectPath}`);
 72 |   console.error(`[Unity MCP ToolDefinitions] Using project root path: ${projectRootPath}`);
 73 | 
 74 |   // List all available tools (both Unity and filesystem tools)
 75 |   server.setRequestHandler(ListToolsRequestSchema, async () => ({
 76 |     tools: [
 77 |       // Unity Editor tools
 78 |       {
 79 |         name: 'get_current_scene_info',
 80 |         description: 'Retrieve information about the current scene in Unity Editor with configurable detail level',
 81 |         category: 'Editor State',
 82 |         tags: ['unity', 'editor', 'scene'],
 83 |         inputSchema: {
 84 |           type: 'object',
 85 |           properties: {
 86 |             detailLevel: {
 87 |               type: 'string',
 88 |               enum: ['RootObjectsOnly', 'FullHierarchy'],
 89 |               description: 'RootObjectsOnly: Returns just root GameObjects. FullHierarchy: Returns complete hierarchy with all children.',
 90 |               default: 'RootObjectsOnly'
 91 |             }
 92 |           },
 93 |           additionalProperties: false
 94 |         },
 95 |         returns: {
 96 |           type: 'object',
 97 |           description: 'Returns information about the current scene and its hierarchy based on requested detail level'
 98 |         }
 99 |       },
100 |       {
101 |         name: 'get_game_objects_info',
102 |         description: 'Retrieve detailed information about specific GameObjects in the current scene',
103 |         category: 'Editor State',
104 |         tags: ['unity', 'editor', 'gameobjects'],
105 |         inputSchema: {
106 |           type: 'object',
107 |           properties: {
108 |             instanceIDs: {
109 |               type: 'array',
110 |               items: {
111 |                 type: 'number'
112 |               },
113 |               description: 'Array of GameObject instance IDs to get information for',
114 |               minItems: 1
115 |             },
116 |             detailLevel: {
117 |               type: 'string',
118 |               enum: ['BasicInfo', 'IncludeComponents', 'IncludeChildren', 'IncludeComponentsAndChildren'],
119 |               description: 'BasicInfo: Basic GameObject information. IncludeComponents: Includes component details. IncludeChildren: Includes child GameObjects. IncludeComponentsAndChildren: Includes both components and a full hierarchy with components on children.',
120 |               default: 'IncludeComponents'
121 |             }
122 |           },
123 |           required: ['instanceIDs'],
124 |           additionalProperties: false
125 |         },
126 |         returns: {
127 |           type: 'object',
128 |           description: 'Returns detailed information about the requested GameObjects'
129 |         }
130 |       },
131 |       {
132 |         name: 'execute_editor_command',
133 |         description: 'Execute C# code directly in the Unity Editor - allows full flexibility including custom namespaces and multiple classes',
134 |         category: 'Editor Control',
135 |         tags: ['unity', 'editor', 'command', 'c#'],
136 |         inputSchema: {
137 |           type: 'object',
138 |           properties: {
139 |             code: {
140 |               type: 'string',
141 |               description: 'C# code to execute in Unity Editor. You MUST define a public class named "McpScript" with a public static method named "Execute" that returns an object. Example: "public class McpScript { public static object Execute() { /* your code here */ return result; } }". You can include any necessary namespaces, additional classes, and methods.',
142 |               minLength: 1
143 |             }
144 |           },
145 |           required: ['code'],
146 |           additionalProperties: false
147 |         },
148 |         returns: {
149 |           type: 'object',
150 |           description: 'Returns the execution result, execution time, and status'
151 |         }
152 |       },
153 |       {
154 |         name: 'get_logs',
155 |         description: 'Retrieve Unity Editor logs with filtering options',
156 |         category: 'Debugging',
157 |         tags: ['unity', 'editor', 'logs', 'debugging'],
158 |         inputSchema: {
159 |           type: 'object',
160 |           properties: {
161 |             types: {
162 |               type: 'array',
163 |               items: {
164 |                 type: 'string',
165 |                 enum: ['Log', 'Warning', 'Error', 'Exception']
166 |               },
167 |               description: 'Filter logs by type'
168 |             },
169 |             count: {
170 |               type: 'number',
171 |               description: 'Maximum number of log entries to return',
172 |               minimum: 1,
173 |               maximum: 1000
174 |             },
175 |             fields: {
176 |               type: 'array',
177 |               items: {
178 |                 type: 'string',
179 |                 enum: ['message', 'stackTrace', 'logType', 'timestamp']
180 |               },
181 |               description: 'Specify which fields to include in the output'
182 |             },
183 |             messageContains: {
184 |               type: 'string',
185 |               description: 'Filter logs by message content'
186 |             },
187 |             stackTraceContains: {
188 |               type: 'string',
189 |               description: 'Filter logs by stack trace content'
190 |             },
191 |             timestampAfter: {
192 |               type: 'string',
193 |               description: 'Filter logs after this ISO timestamp'
194 |             },
195 |             timestampBefore: {
196 |               type: 'string',
197 |               description: 'Filter logs before this ISO timestamp'
198 |             }
199 |           },
200 |           additionalProperties: false
201 |         },
202 |         returns: {
203 |           type: 'array',
204 |           description: 'Returns an array of log entries matching the specified filters'
205 |         }
206 |       },
207 |       {
208 |         name: 'verify_connection',
209 |         description: 'Verify that the MCP server has an active connection to Unity Editor',
210 |         category: 'Connection',
211 |         tags: ['unity', 'editor', 'connection'],
212 |         inputSchema: {
213 |           type: 'object',
214 |           properties: {},
215 |           additionalProperties: false
216 |         },
217 |         returns: {
218 |           type: 'object',
219 |           description: 'Returns connection status information'
220 |         }
221 |       },
222 |       {
223 |         name: 'get_editor_state',
224 |         description: 'Get the current Unity Editor state including project information',
225 |         category: 'Editor State',
226 |         tags: ['unity', 'editor', 'project'],
227 |         inputSchema: {
228 |           type: 'object',
229 |           properties: {},
230 |           additionalProperties: false
231 |         },
232 |         returns: {
233 |           type: 'object',
234 |           description: 'Returns detailed information about the current Unity Editor state, project settings, and environment'
235 |         }
236 |       },
237 |       
238 |       // Filesystem tools - defined alongside Unity tools
239 |       {
240 |         name: "read_file",
241 |         description: "Read the contents of a file from the Unity project. Paths are relative to the project's Assets folder. For example, use 'Scenes/MainScene.unity' to read Assets/Scenes/MainScene.unity.",
242 |         category: "Filesystem",
243 |         tags: ['unity', 'filesystem', 'file'],
244 |         inputSchema: zodToJsonSchema(ReadFileArgsSchema),
245 |       },
246 |       {
247 |         name: "read_multiple_files",
248 |         description: "Read the contents of multiple files from the Unity project simultaneously.",
249 |         category: "Filesystem",
250 |         tags: ['unity', 'filesystem', 'file', 'batch'],
251 |         inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema),
252 |       },
253 |       {
254 |         name: "write_file",
255 |         description: "Create a new file or completely overwrite an existing file in the Unity project.",
256 |         category: "Filesystem",
257 |         tags: ['unity', 'filesystem', 'file', 'write'],
258 |         inputSchema: zodToJsonSchema(WriteFileArgsSchema),
259 |       },
260 |       {
261 |         name: "edit_file",
262 |         description: "Make precise edits to a text file in the Unity project. Returns a git-style diff showing changes.",
263 |         category: "Filesystem",
264 |         tags: ['unity', 'filesystem', 'file', 'edit'],
265 |         inputSchema: zodToJsonSchema(EditFileArgsSchema),
266 |       },
267 |       {
268 |         name: "list_directory",
269 |         description: "Get a listing of all files and directories in a specified path in the Unity project. Paths are relative to the Assets folder unless absolute. For example, use 'Scenes' to list all files in Assets/Scenes directory. Use empty string to list the Assets folder.",
270 |         category: "Filesystem",
271 |         tags: ['unity', 'filesystem', 'directory', 'list'],
272 |         inputSchema: zodToJsonSchema(ListDirectoryArgsSchema),
273 |       },
274 |       {
275 |         name: "directory_tree",
276 |         description: "Get a recursive tree view of files and directories in the Unity project as a JSON structure.",
277 |         category: "Filesystem",
278 |         tags: ['unity', 'filesystem', 'directory', 'tree'],
279 |         inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema),
280 |       },
281 |       {
282 |         name: "search_files",
283 |         description: "Recursively search for files and directories matching a pattern in the Unity project.",
284 |         category: "Filesystem",
285 |         tags: ['unity', 'filesystem', 'search'],
286 |         inputSchema: zodToJsonSchema(SearchFilesArgsSchema),
287 |       },
288 |       {
289 |         name: "get_file_info",
290 |         description: "Retrieve detailed metadata about a file or directory in the Unity project.",
291 |         category: "Filesystem",
292 |         tags: ['unity', 'filesystem', 'file', 'metadata'],
293 |         inputSchema: zodToJsonSchema(GetFileInfoArgsSchema),
294 |       },
295 |       {
296 |         name: "find_assets_by_type",
297 |         description: "Find all Unity assets of a specified type (e.g., Material, Prefab, Scene, Script) in the project. Set searchPath to an empty string to search the entire Assets folder.",
298 |         category: "Filesystem",
299 |         tags: ['unity', 'filesystem', 'assets', 'search'],
300 |         inputSchema: zodToJsonSchema(FindAssetsByTypeArgsSchema),
301 |       },
302 |     ],
303 |   }));
304 | 
305 |   // Handle tool calls
306 |   server.setRequestHandler(CallToolRequestSchema, async (request) => {
307 |     const { name, arguments: args } = request.params;
308 | 
309 |     // Special case for verify_connection which should work even if not connected
310 |     if (name === 'verify_connection') {
311 |       try {
312 |         const isConnected = wsHandler.isConnected();
313 |         
314 |         // Always request fresh editor state if connected
315 |         if (isConnected) {
316 |           wsHandler.requestEditorState();
317 |         }
318 |         
319 |         return {
320 |           content: [{
321 |             type: 'text',
322 |             text: JSON.stringify({
323 |               connected: isConnected,
324 |               timestamp: new Date().toISOString(),
325 |               message: isConnected 
326 |                 ? 'Unity Editor is connected' 
327 |                 : 'Unity Editor is not connected. Please ensure the Unity Editor is running with the MCP plugin.'
328 |             }, null, 2)
329 |           }]
330 |         };
331 |       } catch (error) {
332 |         return {
333 |           content: [{
334 |             type: 'text',
335 |             text: JSON.stringify({
336 |               connected: false,
337 |               timestamp: new Date().toISOString(),
338 |               message: 'Error checking connection status',
339 |               error: error instanceof Error ? error.message : 'Unknown error'
340 |             }, null, 2)
341 |           }]
342 |         };
343 |       }
344 |     }
345 | 
346 |     // Check if this is a filesystem tool
347 |     const filesystemTools = [
348 |       "read_file", "read_multiple_files", "write_file", "edit_file", 
349 |       "list_directory", "directory_tree", "search_files", "get_file_info", 
350 |       "find_assets_by_type"
351 |     ];
352 |     
353 |     if (filesystemTools.includes(name)) {
354 |       try {
355 |         return await handleFilesystemTool(name, args, projectPath);
356 |       } catch (error) {
357 |         const errorMessage = error instanceof Error ? error.message : String(error);
358 |         return {
359 |           content: [{ type: "text", text: `Error: ${errorMessage}` }],
360 |           isError: true,
361 |         };
362 |       }
363 |     }
364 | 
365 |     // For all other tools (Unity-specific), verify connection first
366 |     if (!wsHandler.isConnected()) {
367 |       throw new McpError(
368 |         ErrorCode.InternalError,
369 |         'Unity Editor is not connected. Please first verify the connection using the verify_connection tool, ' +
370 |         'and ensure the Unity Editor is running with the MCP plugin and that the WebSocket connection is established.'
371 |       );
372 |     }
373 |     
374 |     switch (name) {
375 |       case 'get_editor_state': {
376 |         try {
377 |           // Always request a fresh editor state before returning
378 |           wsHandler.requestEditorState();
379 |           
380 |           // Wait a moment for the response to arrive
381 |           await new Promise(resolve => setTimeout(resolve, 1000));
382 |           
383 |           // Return the current editor state
384 |           const editorState = wsHandler.getEditorState();
385 |           
386 |           return {
387 |             content: [{
388 |               type: 'text',
389 |               text: JSON.stringify(editorState, null, 2)
390 |             }]
391 |           };
392 |         } catch (error) {
393 |           throw new McpError(
394 |             ErrorCode.InternalError,
395 |             `Failed to get editor state: ${error instanceof Error ? error.message : 'Unknown error'}`
396 |           );
397 |         }
398 |       }
399 |       
400 |       case 'get_current_scene_info': {
401 |         try {
402 |           const detailLevel = (args?.detailLevel as string) || 'RootObjectsOnly';
403 |           
404 |           // Send request to Unity and wait for response
405 |           const sceneInfo = await wsHandler.requestSceneInfo(detailLevel);
406 |           
407 |           return {
408 |             content: [{
409 |               type: 'text',
410 |               text: JSON.stringify(sceneInfo, null, 2)
411 |             }]
412 |           };
413 |         } catch (error) {
414 |           throw new McpError(
415 |             ErrorCode.InternalError,
416 |             `Failed to get scene info: ${error instanceof Error ? error.message : 'Unknown error'}`
417 |           );
418 |         }
419 |       }
420 |       
421 |       case 'get_game_objects_info': {
422 |         try {
423 |           if (!args?.instanceIDs || !Array.isArray(args.instanceIDs)) {
424 |             throw new McpError(
425 |               ErrorCode.InvalidParams,
426 |               'instanceIDs array is required'
427 |             );
428 |           }
429 |           
430 |           const instanceIDs = args.instanceIDs;
431 |           const detailLevel = (args?.detailLevel as string) || 'IncludeComponents';
432 |           
433 |           // Send request to Unity and wait for response
434 |           const gameObjectsInfo = await wsHandler.requestGameObjectsInfo(instanceIDs, detailLevel);
435 |           
436 |           return {
437 |             content: [{
438 |               type: 'text',
439 |               text: JSON.stringify(gameObjectsInfo, null, 2)
440 |             }]
441 |           };
442 |         } catch (error) {
443 |           throw new McpError(
444 |             ErrorCode.InternalError,
445 |             `Failed to get GameObject info: ${error instanceof Error ? error.message : 'Unknown error'}`
446 |           );
447 |         }
448 |       }
449 | 
450 |       case 'execute_editor_command': {
451 |         try {
452 |           if (!args?.code) {
453 |             throw new McpError(
454 |               ErrorCode.InvalidParams,
455 |               'The code parameter is required'
456 |             );
457 |           }
458 | 
459 |           const startTime = Date.now();
460 |           const result = await wsHandler.executeEditorCommand(args.code as string);
461 |           const executionTime = Date.now() - startTime;
462 | 
463 |           return {
464 |             content: [{
465 |               type: 'text',
466 |               text: JSON.stringify({
467 |                 result,
468 |                 executionTime: `${executionTime}ms`,
469 |                 status: 'success'
470 |               }, null, 2)
471 |             }]
472 |           };
473 |         } catch (error) {
474 |           if (error instanceof Error) {
475 |             if (error.message.includes('timed out')) {
476 |               throw new McpError(
477 |                 ErrorCode.InternalError,
478 |                 'Command execution timed out. This may indicate a long-running operation or an issue with the Unity Editor.'
479 |               );
480 |             }
481 |             
482 |             if (error.message.includes('NullReferenceException')) {
483 |               throw new McpError(
484 |                 ErrorCode.InvalidParams,
485 |                 'The code attempted to access a null object. Please check that all GameObject references exist.'
486 |               );
487 |             }
488 | 
489 |             if (error.message.includes('not connected')) {
490 |               throw new McpError(
491 |                 ErrorCode.InternalError,
492 |                 'Unity Editor connection was lost during command execution. Please verify the connection and try again.'
493 |               );
494 |             }
495 |           }
496 | 
497 |           throw new McpError(
498 |             ErrorCode.InternalError,
499 |             `Failed to execute command: ${error instanceof Error ? error.message : 'Unknown error'}`
500 |           );
501 |         }
502 |       }
503 | 
504 |       case 'get_logs': {
505 |         try {
506 |           const options = {
507 |             types: args?.types as string[] | undefined,
508 |             count: args?.count as number | undefined,
509 |             fields: args?.fields as string[] | undefined,
510 |             messageContains: args?.messageContains as string | undefined,
511 |             stackTraceContains: args?.stackTraceContains as string | undefined,
512 |             timestampAfter: args?.timestampAfter as string | undefined,
513 |             timestampBefore: args?.timestampBefore as string | undefined
514 |           };
515 |           
516 |           const logs = wsHandler.getLogEntries(options);
517 | 
518 |           return {
519 |             content: [{
520 |               type: 'text',
521 |               text: JSON.stringify(logs, null, 2)
522 |             }]
523 |           };
524 |         } catch (error) {
525 |           throw new McpError(
526 |             ErrorCode.InternalError,
527 |             `Failed to retrieve logs: ${error instanceof Error ? error.message : 'Unknown error'}`
528 |           );
529 |         }
530 |       }
531 | 
532 |       default:
533 |         throw new McpError(
534 |           ErrorCode.MethodNotFound,
535 |           `Unknown tool: ${name}`
536 |         );
537 |     }
538 |   });
539 | }
```
Page 1/2FirstPrevNextLast