# 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:
--------------------------------------------------------------------------------
```
node_modules
npm-debug.log
.git
.github
.vscode
*.md
build/
.env
```
--------------------------------------------------------------------------------
/mcpServer/.env.example:
--------------------------------------------------------------------------------
```
# Unity MCP Server Configuration
# WebSocket port for Unity Editor connection
MCP_WEBSOCKET_PORT=5010
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
#unity stuff
# This .gitignore file should be placed at the root of your Unity project directory
#
# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
#
.utmp/
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
# MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data
/[Mm]emoryCaptures/
# Recordings can get excessive in size
/[Rr]ecordings/
# Uncomment this line if you wish to ignore the asset store tools plugin
# /[Aa]ssets/AssetStoreTools*
# Autogenerated Jetbrains Rider plugin
/[Aa]ssets/Plugins/Editor/JetBrains*
# Visual Studio cache directory
.vs/
# Gradle cache directory
.gradle/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db
# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
*.mdb.meta
# Unity3D generated file on crash reports
sysinfo.txt
# Builds
*.apk
*.aab
*.unitypackage
*.unitypackage.meta
*.app
# Crashlytics generated file
crashlytics-build.properties
# Packed Addressables
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
# Temporary auto-generated Android Assets
/[Aa]ssets/[Ss]treamingAssets/aa.meta
/[Aa]ssets/[Ss]treamingAssets/aa/*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# 🚀 Advacned Unity MCP Integration
[](https://modelcontextprotocol.io/introduction)
[](https://smithery.ai/server/@quazaai/unitymcpintegration)
[](https://unity.com)
[](https://nodejs.org)
[](https://www.typescriptlang.org)
[](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
[](https://github.com/quazaai/UnityMCPIntegration/stargazers)
[](https://github.com/quazaai/UnityMCPIntegration/network/members)
[](https://github.com/quazaai/UnityMCPIntegration/blob/main/LICENSE)
<div align="center">
<img src="mcpInspector.png" alt="Unity MCP Inspector" width="400" align="right" style="margin-left: 20px; margin-bottom: 20px;"/>
</div>
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.
## 📚 Features
- Browse and manipulate project files directly
- Access real-time information about your Unity project
- Understand your scene hierarchy and game objects
- Execute C# code directly in the Unity Editor
- Monitor logs and errors
- Control the Editor's play mode
- Wait For Code Execution
## 🚀 Getting Started
### Prerequisites
- Unity 2021.3 or later
- Node.js 18+ (for running the MCP server)
### Installation
#### 1. Install Unity Package
You have several options to install the Unity package:
**Option A: Package Manager (Git URL)**
1. Open the Unity Package Manager (`Window > Package Manager`)
2. Click the `+` button and select `Add package from git URL...`
3. Enter the repository URL: `https://github.com/quazaai/UnityMCPIntegration.git`
4. Click `Add`
**Option B: Import Custom Package**
1. Clone this repository or [download it as a unityPackage](https://github.com/quazaai/UnityMCPIntegration/releases)
2. In Unity, go to `Assets > Import Package > Custom Package`
3. Select the `UnityMCPIntegration.unitypackage` file
#### 2. Set up the MCP Server
You have two options to run the MCP server:
**Option A: Run the server directly**
1. Navigate to the `mcpServer (likely <path-to-project>\Library\PackageCache\com.quaza.unitymcp@d2b8f1260bca\mcpServer\)` directory
2. Install dependencies:
```
npm install
```
3. Run the server:
```
node build/index.js
```
**Option B: Add to MCP Host configuration**
Add the server to your MCP Host configuration for Claude Desktop, Custom Implementation etc
```json
{
"mcpServers": {
"unity-mcp-server": {
"command": "node",
"args": [
"path-to-project>\\Library\\PackageCache\\com.quaza.unitymcp@d2b8f1260bca\\mcpServer\\mcpServer\\build\\index.js"
],
"env": {
"MCP_WEBSOCKET_PORT": "5010"
}
}
}
}
```
### Demo Video
[](https://www.youtube.com/watch?v=GxTlahBXs74)
### Installing via Smithery
To install Unity MCP Integration for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@quazaai/unitymcpintegration):
```bash
npx -y @smithery/cli install @quazaai/unitymcpintegration --client claude
```
### 🔧 Usage
#### Debugging and Monitoring
You can open the MCP Debug window in Unity to monitor the connection and test features:
1. Go to `Window > MCP Debug`
2. Use the debug window to:
- Check connection status
- Test code execution
- View logs
- Monitor events
#### Available Tools
The Unity MCP integration provides several tools to AI assistants:
##### Unity Editor Tools
- **get_editor_state**: Get comprehensive information about the Unity project and editor state
- **get_current_scene_info**: Get detailed information about the current scene
- **get_game_objects_info**: Get information about specific GameObjects in the scene
- **execute_editor_command**: Execute C# code directly in the Unity Editor
- **get_logs**: Retrieve and filter Unity console logs
- **verify_connection**: Check if there's an active connection to Unity Editor
##### Filesystem Tools
- **read_file**: Read contents of a file in your Unity project
- **read_multiple_files**: Read multiple files at once
- **write_file**: Create or overwrite a file with new content
- **edit_file**: Make targeted edits to existing files with diff preview
- **list_directory**: Get a listing of files and folders in a directory
- **directory_tree**: Get a hierarchical view of directories and files
- **search_files**: Find files matching a search pattern
- **get_file_info**: Get metadata about a specific file or directory
- **find_assets_by_type**: Find all assets of a specific type (e.g. Material, Prefab)
- **list_scripts**: Get a listing of all C# scripts in the project
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`.
## 🛠️ Architecture
The integration consists of two main components:
1. **Unity Plugin (C#)**: Resides in the Unity Editor and provides access to Editor APIs
2. **MCP Server (TypeScript/Node.js)**: Implements the MCP protocol and communicates with the Unity plugin
Communication between them happens via WebSocket, transferring JSON messages for commands and data.
## File System Access
The Unity MCP integration now includes powerful filesystem tools that allow AI assistants to:
- Browse, read, and edit files in your Unity project
- Create new files and directories
- Search for specific files or asset types
- Analyze your project structure
- Make targeted code changes with diff previews
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.
Example usages:
- Get a directory listing: `list_directory(path: "Scenes")`
- Read a script file: `read_file(path: "Scripts/Player.cs")`
- Edit a configuration file: `edit_file(path: "Resources/config.json", edits: [{oldText: "value: 10", newText: "value: 20"}], dryRun: true)`
- Find all materials: `find_assets_by_type(assetType: "Material")`
## 👥 Contributing
Contributions are welcome! Here's how you can contribute:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Commit your changes (`git commit -m 'Add some amazing feature'`)
5. Push to the branch (`git push origin feature/amazing-feature`)
6. Open a Pull Request
### Development Setup
**Unity Side**:
- Open the project in Unity
- Modify the C# scripts in the `UnityMCPConnection/Editor` directory
**Server Side**:
- Navigate to the `mcpServer` directory
- Install dependencies: `npm install`
- Make changes to the TypeScript files in the `src` directory
- Build the server: `npm run build`
- Run the server: `node build/index.js`
## 📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
## 📞 Support
If you encounter any issues or have questions, please file an issue on the GitHub repository.
```
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
```markdown
Contributor Covenant Code of Conduct
Our Pledge
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.
Our Standards
Examples of behavior that contributes to creating a positive environment include:
Using welcoming and inclusive language
Being respectful of differing viewpoints and experiences
Gracefully accepting constructive criticism
Focusing on what is best for the community
Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
The use of sexualized language or imagery and unwelcome sexual attention or advances
Trolling, insulting/derogatory comments, and personal or political attacks
Public or private harassment
Publishing others' private information, such as a physical or electronic address, without explicit permission
Other conduct which could reasonably be considered inappropriate in a professional setting
Our Responsibilities
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.
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.
Scope
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.
Enforcement
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.
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.
Attribution
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
{
"port": 5010,
"autoReconnect": false,
"globalLoggingEnabled": false,
"componentLoggingEnabled": {}
}
```
--------------------------------------------------------------------------------
/mcpServer/docker-compose.yml:
--------------------------------------------------------------------------------
```yaml
version: '3.8'
services:
unity-mcp-server:
build: .
ports:
- "${MCP_WEBSOCKET_PORT:-5010}:${MCP_WEBSOCKET_PORT:-5010}"
environment:
- MCP_WEBSOCKET_PORT=${MCP_WEBSOCKET_PORT:-5010}
restart: unless-stopped
volumes:
# Optional volume for persisting data if needed
- ./logs:/app/logs
```
--------------------------------------------------------------------------------
/mcpServer/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"useUnknownInCatchVariables": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/mcpServer/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
{}
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
(config) => ({
command: 'node',
args: ['build/index.js']
})
exampleConfig: {}
```
--------------------------------------------------------------------------------
/mcpServer/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine
WORKDIR /app
# Copy package.json and package-lock.json
COPY package*.json ./
# Install dependencies (ignore scripts if any issues with native builds)
RUN npm install --ignore-scripts
# Copy the rest of the application
COPY . .
# Build the TypeScript source
RUN npm run build
# Expose PORT if needed (optional)
# Start the MCP server
CMD ["node", "build/index.js"]
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "com.quaza.unitymcp",
"version": "0.0.1",
"displayName": "Unity MCP Integration",
"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.",
"unity": "2021.3",
"dependencies": {
},
"keywords": [
"ai",
"Model Context Protocol",
"Unity AI Tool",
"Unity MCP"
],
"author": {
"name": "Shahzad",
"url": "https://quazaai.com/"
}
}
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
## [0.0.1] - 2023-11-11
### First Release
- Initial release of Unity MCP Integration
- WebSocket-based communication between Unity Editor and MCP server
- MCP tools implementation:
- `get_editor_state` for retrieving Unity project information
- `get_current_scene_info` for scene hierarchy details
- `get_game_objects_info` for specific GameObject information
- `execute_editor_command` for executing C# code in Unity Editor
- `get_logs` for accessing Unity console logs
- Debug window accessible from Window > MCP Debug menu
- Connection status monitoring
- Support for Unity 2021.3+
- Node.js MCP server (TypeScript implementation)
- Comprehensive documentation
- MIT License
```
--------------------------------------------------------------------------------
/mcpServer/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "unity-mcp-server",
"version": "0.1.0",
"description": "MCP server for Unity integration",
"type": "module",
"bin": {
"unity-mcp-server": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsc",
"start": "node build/index.js",
"dev": "tsc --watch",
"inspector": "npx @modelcontextprotocol/inspector build/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.7.0",
"ws": "^8.16.0",
"diff": "^5.1.0",
"minimatch": "^9.0.3",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.1"
},
"devDependencies": {
"@types/node": "^20.11.24",
"@types/ws": "^8.5.10",
"@types/diff": "^5.0.8",
"@types/minimatch": "^5.1.2",
"typescript": "^5.3.3"
}
}
```
--------------------------------------------------------------------------------
/Editor/Models/MCPEditorState.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Plugins.GamePilot.Editor.MCP
{
[Serializable]
public class MCPEditorState
{
[JsonProperty("activeGameObjects")]
public string[] ActiveGameObjects { get; set; } = new string[0];
[JsonProperty("selectedObjects")]
public string[] SelectedObjects { get; set; } = new string[0];
[JsonProperty("playModeState")]
public string PlayModeState { get; set; } = "Stopped";
[JsonProperty("sceneHierarchy")]
public List<MCPGameObjectInfo> SceneHierarchy { get; set; } = new List<MCPGameObjectInfo>();
// Removed ProjectStructure property
[JsonProperty("timestamp")]
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
// Enhanced project information properties
[JsonProperty("renderPipeline")]
public string RenderPipeline { get; set; } = "Unknown";
[JsonProperty("buildTarget")]
public string BuildTarget { get; set; } = "Unknown";
[JsonProperty("projectName")]
public string ProjectName { get; set; } = "Unknown";
[JsonProperty("graphicsDeviceType")]
public string GraphicsDeviceType { get; set; } = "Unknown";
[JsonProperty("unityVersion")]
public string UnityVersion { get; set; } = "Unknown";
[JsonProperty("currentSceneName")]
public string CurrentSceneName { get; set; } = "Unknown";
[JsonProperty("currentScenePath")]
public string CurrentScenePath { get; set; } = "Unknown";
[JsonProperty("availableMenuItems")]
public List<string> AvailableMenuItems { get; set; } = new List<string>();
}
[Serializable]
public class MCPGameObjectInfo
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("path")]
public string Path { get; set; }
[JsonProperty("components")]
public string[] Components { get; set; } = new string[0];
[JsonProperty("children")]
public List<MCPGameObjectInfo> Children { get; set; } = new List<MCPGameObjectInfo>();
[JsonProperty("active")]
public bool Active { get; set; } = true;
[JsonProperty("layer")]
public int Layer { get; set; }
[JsonProperty("tag")]
public string Tag { get; set; }
}
// Removed MCPProjectStructure class
[Serializable]
public class LogEntry
{
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("stackTrace")]
public string StackTrace { get; set; }
[JsonProperty("type")]
public UnityEngine.LogType Type { get; set; }
[JsonProperty("timestamp")]
public DateTime Timestamp { get; set; }
}
}
```
--------------------------------------------------------------------------------
/Editor/Models/MCPSceneInfo.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine;
namespace Plugins.GamePilot.Editor.MCP
{
[Serializable]
public class MCPSceneInfo
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("path")]
public string Path { get; set; }
[JsonProperty("isDirty")]
public bool IsDirty { get; set; }
[JsonProperty("rootCount")]
public int RootCount { get; set; }
[JsonProperty("rootObjects")]
public List<MCPGameObjectReference> RootObjects { get; set; } = new List<MCPGameObjectReference>();
[JsonProperty("errorMessage")]
public string ErrorMessage { get; set; }
}
[Serializable]
public class MCPGameObjectReference
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("instanceID")]
public int InstanceID { get; set; }
[JsonProperty("path")]
public string Path { get; set; }
[JsonProperty("active")]
public bool Active { get; set; }
[JsonProperty("childCount")]
public int ChildCount { get; set; }
[JsonProperty("children")]
public List<MCPGameObjectReference> Children { get; set; }
}
[Serializable]
public class MCPGameObjectDetail
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("instanceID")]
public int InstanceID { get; set; }
[JsonProperty("path")]
public string Path { get; set; }
[JsonProperty("active")]
public bool Active { get; set; }
[JsonProperty("activeInHierarchy")]
public bool ActiveInHierarchy { get; set; }
[JsonProperty("tag")]
public string Tag { get; set; }
[JsonProperty("layer")]
public int Layer { get; set; }
[JsonProperty("layerName")]
public string LayerName { get; set; }
[JsonProperty("isStatic")]
public bool IsStatic { get; set; }
[JsonProperty("transform")]
public MCPTransformInfo Transform { get; set; }
[JsonProperty("components")]
public List<MCPComponentInfo> Components { get; set; }
[JsonProperty("children")]
public List<MCPGameObjectDetail> Children { get; set; }
}
[Serializable]
public class MCPTransformInfo
{
[JsonProperty("position")]
public Vector3 Position { get; set; }
[JsonProperty("rotation")]
public Vector3 Rotation { get; set; }
[JsonProperty("localPosition")]
public Vector3 LocalPosition { get; set; }
[JsonProperty("localRotation")]
public Vector3 LocalRotation { get; set; }
[JsonProperty("localScale")]
public Vector3 LocalScale { get; set; }
}
[Serializable]
public class MCPComponentInfo
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("isEnabled")]
public bool IsEnabled { get; set; }
[JsonProperty("instanceID")]
public int InstanceID { get; set; }
}
}
```
--------------------------------------------------------------------------------
/mcpServer/MCPSummary.md:
--------------------------------------------------------------------------------
```markdown
<img src="https://r2cdn.perplexity.ai/pplx-full-logo-primary-dark%402x.png" class="logo" width="120"/>
#
---
# https://modelcontextprotocol.io/introduction
https://modelcontextprotocol.io/quickstart/server
https://modelcontextprotocol.io/quickstart/client
https://modelcontextprotocol.io/docs/concepts/tools
give me summary
Here is a summary of the provided pages from the Model Context Protocol (MCP) documentation:
---
### **Introduction to MCP**
- 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].
- It enables:
- Pre-built integrations for LLMs to connect with data and tools.
- Flexibility in switching between LLM providers.
- Secure data handling within user infrastructure[^1].
- MCP follows a **client-server architecture**:
- **MCP Hosts**: Applications like Claude Desktop or IDEs that access data via MCP.
- **MCP Clients**: Protocol clients maintaining connections with servers.
- **MCP Servers**: Lightweight programs exposing specific capabilities through MCP.
- **Local Data Sources** and **Remote Services**: Securely accessed by servers[^1].
---
### **Quickstart: Server**
- 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].
- **Core Concepts**:
- **Resources**: File-like data accessible by clients.
- **Tools**: Functions callable by LLMs with user approval.
- **Prompts**: Templates for specific tasks[^2].
- Example implementation includes:
- Setting up a Python environment with the `uv` tool.
- Using the National Weather Service API to fetch weather data.
- Exposing tools like `get-alerts` and `get-forecast` for integration with hosts like Claude for Desktop[^2].
- The server is tested by configuring Claude for Desktop to recognize and interact with it via commands[^2].
---
### **Quickstart: Client**
- This guide explains building an LLM-powered chatbot client capable of connecting to MCP servers[^3].
- Steps include:
- Setting up a Python project using `uv`.
- Storing API keys securely in environment files.
- Creating a client class to manage server connections and process queries[^3].
- Key functionalities:
- Connecting to MCP servers via Python or JavaScript scripts.
- Handling queries by leveraging available server tools and integrating responses into natural language outputs using LLMs like Claude[^3].
---
### **Concepts: Tools**
- 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].
- Example use cases include automating workflows, accessing external APIs, or interacting with local/remote datasets through standardized interfaces[^1].
---
This documentation provides comprehensive guidance on building both servers and clients using MCP, enabling seamless integration between LLMs and various data sources or tools.
<div style="text-align: center">⁂</div>
[^1]: https://modelcontextprotocol.io/introduction
[^2]: https://modelcontextprotocol.io/quickstart/server
[^3]: https://modelcontextprotocol.io/quickstart/client
[^4]: https://modelcontextprotocol.io/introduction
[^5]: https://modelcontextprotocol.io/quickstart/server
[^6]: https://modelcontextprotocol.io/quickstart/client
[^7]: https://modelcontextprotocol.io/docs/concepts/tools
```
--------------------------------------------------------------------------------
/mcpServer/src/types.ts:
--------------------------------------------------------------------------------
```typescript
// MCP Server Types for Unity Integration
// Unity Editor State representation
export interface UnityEditorState {
activeGameObjects: any[];
selectedObjects: any[];
playModeState: string;
sceneHierarchy: any;
projectName?: string;
unityVersion?: string;
renderPipeline?: string;
buildTarget?: string;
graphicsDeviceType?: string;
currentSceneName?: string;
currentScenePath?: string;
timestamp?: string;
availableMenuItems?: string[];
}
// Log entry from Unity
export interface LogEntry {
message: string;
stackTrace: string;
logType: string;
timestamp: string;
}
// Scene info from Unity
export interface SceneInfoMessage {
type: 'sceneInfo';
data: {
requestId: string;
sceneInfo: any;
timestamp: string;
};
}
// Game objects details from Unity
export interface GameObjectsDetailsMessage {
type: 'gameObjectsDetails';
data: {
requestId: string;
gameObjectDetails: any[];
count: number;
timestamp: string;
};
}
// Message types from Unity to Server
export interface EditorStateMessage {
type: 'editorState';
data: UnityEditorState;
}
export interface CommandResultMessage {
type: 'commandResult';
data: any;
}
export interface LogMessage {
type: 'log';
data: LogEntry;
}
export interface PongMessage {
type: 'pong';
data: { timestamp: number };
}
// Message types from Server to Unity
export interface ExecuteEditorCommandMessage {
type: 'executeEditorCommand';
data: {
code: string;
};
}
export interface HandshakeMessage {
type: 'handshake';
data: { message: string };
}
export interface PingMessage {
type: 'ping';
data: { timestamp: number };
}
export interface RequestEditorStateMessage {
type: 'requestEditorState';
data: Record<string, never>;
}
export interface GetSceneInfoMessage {
type: 'getSceneInfo';
data: {
requestId: string;
detailLevel: string;
};
}
export interface GetGameObjectsInfoMessage {
type: 'getGameObjectsInfo';
data: {
requestId: string;
instanceIDs: number[];
detailLevel: string;
};
}
// Union type for all Unity messages
export type UnityMessage =
| EditorStateMessage
| CommandResultMessage
| LogMessage
| PongMessage
| SceneInfoMessage
| GameObjectsDetailsMessage;
// Union type for all Server messages
export type ServerMessage =
| ExecuteEditorCommandMessage
| HandshakeMessage
| PingMessage
| RequestEditorStateMessage
| GetSceneInfoMessage
| GetGameObjectsInfoMessage;
// Command result handling
export interface CommandPromise {
resolve: (data?: any) => void;
reject: (reason?: any) => void;
}
export interface TreeEntry {
name: string;
type: 'file' | 'directory';
children?: TreeEntry[];
}
export interface MCPSceneInfo {
name: string;
path: string;
rootGameObjects: any[];
buildIndex: number;
isDirty: boolean;
isLoaded: boolean;
}
export interface MCPTransformInfo {
position: { x: number, y: number, z: number };
rotation: { x: number, y: number, z: number };
localPosition: { x: number, y: number, z: number };
localRotation: { x: number, y: number, z: number };
localScale: { x: number, y: number, z: number };
}
export interface MCPComponentInfo {
type: string;
isEnabled: boolean;
instanceID: number;
}
export interface MCPGameObjectDetail {
name: string;
instanceID: number;
path: string;
active: boolean;
activeInHierarchy: boolean;
tag: string;
layer: number;
layerName: string;
isStatic: boolean;
transform: MCPTransformInfo;
components: MCPComponentInfo[];
}
export enum SceneInfoDetail {
RootObjectsOnly = 'RootObjectsOnly',
FullHierarchy = 'FullHierarchy'
}
export enum GameObjectInfoDetail {
BasicInfo = 'BasicInfo',
IncludeComponents = 'IncludeComponents',
IncludeChildren = 'IncludeChildren',
IncludeComponentsAndChildren = 'IncludeComponentsAndChildren'
}
```
--------------------------------------------------------------------------------
/Editor/MCPManager.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using UnityEditor;
using UnityEngine;
namespace Plugins.GamePilot.Editor.MCP
{
[InitializeOnLoad]
public class MCPManager
{
private static readonly string ComponentName = "MCPManager";
private static MCPConnectionManager connectionManager;
private static MCPMessageHandler messageHandler;
private static MCPDataCollector dataCollector;
private static MCPMessageSender messageSender;
private static bool isInitialized = false;
public static bool IsInitialized => isInitialized;
private static bool autoReconnect = false;
// Constructor called on editor startup due to [InitializeOnLoad]
static MCPManager()
{
// Initialize logger for this component
MCPLogger.InitializeComponent(ComponentName);
EditorApplication.delayCall += Initialize;
}
public static void Initialize()
{
if (isInitialized)
return;
MCPLogger.Log(ComponentName, "Initializing Model Context Protocol system...");
try
{
// Create components
dataCollector = new MCPDataCollector();
connectionManager = new MCPConnectionManager();
messageSender = new MCPMessageSender(connectionManager);
messageHandler = new MCPMessageHandler(dataCollector, messageSender);
// Hook up events
connectionManager.OnMessageReceived += messageHandler.HandleMessage;
connectionManager.OnConnected += OnConnected;
connectionManager.OnDisconnected += OnDisconnected;
connectionManager.OnError += OnError;
// Start connection
connectionManager.Connect();
// Register update for connection checking only
EditorApplication.update += Update;
isInitialized = true;
MCPLogger.Log(ComponentName, "Model Context Protocol system initialized successfully");
}
catch (Exception ex)
{
MCPLogger.LogException(ComponentName, ex);
Debug.LogError($"[MCP] Failed to initialize MCP system: {ex.Message}\n{ex.StackTrace}");
}
}
private static void OnConnected()
{
try
{
MCPLogger.Log(ComponentName, "Connected to MCP server");
}
catch (Exception ex)
{
MCPLogger.LogException(ComponentName, ex);
}
}
private static void OnDisconnected()
{
try
{
MCPLogger.Log(ComponentName, "Disconnected from MCP server");
}
catch (Exception ex)
{
MCPLogger.LogException(ComponentName, ex);
}
}
private static void OnError(string errorMessage)
{
MCPLogger.LogError(ComponentName, $"Connection error: {errorMessage}");
}
private static void Update()
{
try
{
// Check if connection manager needs to reconnect
if (autoReconnect)
{
connectionManager?.CheckConnection();
}
}
catch (Exception ex)
{
MCPLogger.LogException(ComponentName, ex);
}
}
// Public methods for manual control
public static void RetryConnection()
{
connectionManager?.Reconnect();
}
public static void EnableAutoReconnect(bool enable)
{
autoReconnect = enable;
MCPLogger.Log(ComponentName, $"Auto reconnect set to: {enable}");
}
public static bool IsConnected => connectionManager?.IsConnected ?? false;
public static void Shutdown()
{
if (!isInitialized) return;
try
{
MCPLogger.Log(ComponentName, "Shutting down MCP system");
// Unregister update callbacks
EditorApplication.update -= Update;
// Disconnect
connectionManager?.Disconnect();
// Cleanup
dataCollector?.Dispose();
isInitialized = false;
MCPLogger.Log(ComponentName, "System shutdown completed");
}
catch (Exception ex)
{
MCPLogger.LogException(ComponentName, ex);
}
}
}
}
```
--------------------------------------------------------------------------------
/Editor/MCPLogger.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Plugins.GamePilot.Editor.MCP
{
/// <summary>
/// Centralized logging system for MCP components that respects component-level logging settings.
/// </summary>
public static class MCPLogger
{
// Dictionary to track enabled status for each component
private static readonly Dictionary<string, bool> componentLoggingEnabled = new Dictionary<string, bool>();
// Default log state (off by default)
private static bool globalLoggingEnabled = false;
/// <summary>
/// Enable or disable logging globally for all components
/// </summary>
public static bool GlobalLoggingEnabled
{
get => globalLoggingEnabled;
set => globalLoggingEnabled = value;
}
/// <summary>
/// Initialize a component for logging
/// </summary>
/// <param name="componentName">Name of the component</param>
/// <param name="enabledByDefault">Whether logging should be enabled by default</param>
public static void InitializeComponent(string componentName, bool enabledByDefault = false)
{
if (!componentLoggingEnabled.ContainsKey(componentName))
{
componentLoggingEnabled[componentName] = enabledByDefault;
}
}
/// <summary>
/// Set logging state for a specific component
/// </summary>
/// <param name="componentName">Name of the component</param>
/// <param name="enabled">Whether logging should be enabled</param>
public static void SetComponentLoggingEnabled(string componentName, bool enabled)
{
// Make sure component exists before setting state
if (!componentLoggingEnabled.ContainsKey(componentName))
{
InitializeComponent(componentName, false);
}
componentLoggingEnabled[componentName] = enabled;
}
/// <summary>
/// Check if logging is enabled for a component
/// </summary>
/// <param name="componentName">Name of the component</param>
/// <returns>True if logging is enabled</returns>
public static bool IsLoggingEnabled(string componentName)
{
// If global logging is disabled, nothing gets logged
if (!globalLoggingEnabled) return false;
// If component isn't registered, assume it's disabled
if (!componentLoggingEnabled.ContainsKey(componentName))
{
InitializeComponent(componentName, false);
return false;
}
// Return component-specific setting
return componentLoggingEnabled[componentName];
}
/// <summary>
/// Log a message if logging is enabled for the component
/// </summary>
/// <param name="componentName">Name of the component</param>
/// <param name="message">Message to log</param>
public static void Log(string componentName, string message)
{
if (IsLoggingEnabled(componentName))
{
Debug.Log($"[MCP] [{componentName}] {message}");
}
}
/// <summary>
/// Log a warning if logging is enabled for the component
/// </summary>
/// <param name="componentName">Name of the component</param>
/// <param name="message">Message to log</param>
public static void LogWarning(string componentName, string message)
{
if (IsLoggingEnabled(componentName))
{
Debug.LogWarning($"[MCP] [{componentName}] {message}");
}
}
/// <summary>
/// Log an error if logging is enabled for the component
/// </summary>
/// <param name="componentName">Name of the component</param>
/// <param name="message">Message to log</param>
public static void LogError(string componentName, string message)
{
if (IsLoggingEnabled(componentName))
{
Debug.LogError($"[MCP] [{componentName}] {message}");
}
}
/// <summary>
/// Log an exception if logging is enabled for the component
/// </summary>
/// <param name="componentName">Name of the component</param>
/// <param name="ex">Exception to log</param>
public static void LogException(string componentName, Exception ex)
{
if (IsLoggingEnabled(componentName))
{
Debug.LogError($"[MCP] [{componentName}] Exception: {ex.Message}\n{ex.StackTrace}");
}
}
/// <summary>
/// Get all registered components
/// </summary>
public static IEnumerable<string> GetRegisteredComponents()
{
return componentLoggingEnabled.Keys;
}
/// <summary>
/// Get logging state for a component
/// </summary>
public static bool GetComponentLoggingEnabled(string componentName)
{
if (!componentLoggingEnabled.ContainsKey(componentName))
return false;
return componentLoggingEnabled[componentName];
}
}
}
```
--------------------------------------------------------------------------------
/mcpServer/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { WebSocketHandler } from './websocketHandler.js';
import { registerTools } from './toolDefinitions.js';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
class UnityMCPServer {
private server: Server;
private wsHandler: WebSocketHandler;
constructor() {
// Initialize MCP Server
this.server = new Server(
{ name: 'unity-mcp-server', version: '0.2.0' },
{ capabilities: { tools: {} } }
);
// Setup project paths and websocket
const wsPort = parseInt(process.env.MCP_WEBSOCKET_PORT || '5010');
const projectRootPath = this.setupProjectPaths();
// Initialize WebSocket Handler for Unity communication
this.wsHandler = new WebSocketHandler(wsPort);
// Register MCP tools
registerTools(this.server, this.wsHandler);
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
this.setupShutdownHandlers();
}
private setupProjectPaths(): string {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
console.error(`[Unity MCP] Server starting from directory: ${__dirname}`);
// Get the project root path (parent of Assets)
let projectRootPath = process.env.UNITY_PROJECT_PATH || this.determineUnityProjectPath(__dirname);
projectRootPath = path.normalize(projectRootPath.replace(/["']/g, ''));
// Make sure path ends with a directory separator
if (!projectRootPath.endsWith(path.sep)) {
projectRootPath += path.sep;
}
// Create the full path to the Assets folder
const projectPath = path.join(projectRootPath, 'Assets') + path.sep;
this.setupEnvironmentPath(projectRootPath, projectPath);
return projectRootPath;
}
private setupEnvironmentPath(projectRootPath: string, projectPath: string): void {
try {
if (fs.existsSync(projectPath)) {
console.error(`[Unity MCP] Using project path: ${projectPath}`);
process.env.UNITY_PROJECT_PATH = projectPath;
} else {
console.error(`[Unity MCP] WARNING: Assets folder not found at ${projectPath}`);
console.error(`[Unity MCP] Using project root instead: ${projectRootPath}`);
process.env.UNITY_PROJECT_PATH = projectRootPath;
}
} catch (error) {
console.error(`[Unity MCP] Error checking project path: ${error}`);
process.env.UNITY_PROJECT_PATH = process.cwd();
}
}
private setupShutdownHandlers(): void {
const cleanupHandler = async () => {
await this.cleanup();
process.exit(0);
};
process.on('SIGINT', cleanupHandler);
process.on('SIGTERM', cleanupHandler);
}
/**
* Determine the Unity project path based on the script location
*/
private determineUnityProjectPath(scriptDir: string): string {
scriptDir = path.normalize(scriptDir);
console.error(`[Unity MCP] Script directory: ${scriptDir}`);
// Case 1: Installed in Assets folder
const assetsMatch = /^(.+?[\/\\]Assets)[\/\\].*$/i.exec(scriptDir);
if (assetsMatch) {
const projectRoot = path.dirname(assetsMatch[1]);
console.error(`[Unity MCP] Detected installation in Assets folder: ${projectRoot}`);
return projectRoot;
}
// Case 2: Installed via Package Manager
const libraryMatch = /^(.+?[\/\\]Library)[\/\\]PackageCache[\/\\].*$/i.exec(scriptDir);
if (libraryMatch) {
const projectRoot = path.dirname(libraryMatch[1]);
console.error(`[Unity MCP] Detected installation via Package Manager: ${projectRoot}`);
const assetsPath = path.join(projectRoot, 'Assets');
if (fs.existsSync(assetsPath)) {
return projectRoot;
}
}
// Case 3: Check parent directories
for (const dir of this.getParentDirectories(scriptDir)) {
// Check if this directory is "UnityMCP"
if (path.basename(dir) === 'UnityMCP') {
console.error(`[Unity MCP] Found UnityMCP directory at: ${dir}`);
return dir;
}
// Check if this directory contains an Assets folder
const assetsDir = path.join(dir, 'Assets');
try {
if (fs.existsSync(assetsDir) && fs.statSync(assetsDir).isDirectory()) {
console.error(`[Unity MCP] Found Unity project at: ${dir}`);
return dir;
}
} catch (e) {
// Ignore errors checking directories
}
}
// Fallback
console.error('[Unity MCP] Could not detect Unity project directory. Using current directory.');
return process.cwd();
}
private getParentDirectories(filePath: string): string[] {
const result: string[] = [];
const dirs = filePath.split(path.sep);
for (let i = 1; i <= dirs.length; i++) {
result.push(dirs.slice(0, i).join(path.sep));
}
return result;
}
private async cleanup() {
console.error('Cleaning up resources...');
await this.wsHandler.close();
await this.server.close();
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('[Unity MCP] Server running and ready to accept connections');
console.error('[Unity MCP] WebSocket server listening on port', this.wsHandler.port);
}
}
// Start the server
const server = new UnityMCPServer();
server.run().catch(err => {
console.error('Fatal error in MCP server:', err);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/Editor/MCPCodeExecutor.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CSharp;
using UnityEngine;
using UnityEditor;
namespace Plugins.GamePilot.Editor.MCP
{
public class MCPCodeExecutor
{
private readonly List<string> logs = new List<string>();
private readonly List<string> errors = new List<string>();
private readonly List<string> warnings = new List<string>();
public object ExecuteCode(string code)
{
logs.Clear();
errors.Clear();
warnings.Clear();
// Add log handler to capture output during execution
Application.logMessageReceived += LogHandler;
try
{
return ExecuteCommand(code);
}
catch (Exception ex)
{
string errorMessage = $"Code execution failed: {ex.Message}\n{ex.StackTrace}";
Debug.LogError(errorMessage);
errors.Add(errorMessage);
return null;
}
finally
{
Application.logMessageReceived -= LogHandler;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
private object ExecuteCommand(string code)
{
// The code should define a class called "McpScript" with a static method "Execute"
// Less restrictive on what the code can contain (namespaces, classes, etc.)
using (var provider = new CSharpCodeProvider())
{
var options = new CompilerParameters
{
GenerateInMemory = true,
IncludeDebugInformation = true
};
// Add essential references
AddEssentialReferences(options);
// Compile the code as provided - with no wrapping
var results = provider.CompileAssemblyFromSource(options, code);
if (results.Errors.HasErrors)
{
var errorMessages = new List<string>();
foreach (CompilerError error in results.Errors)
{
errorMessages.Add($"Line {error.Line}: {error.ErrorText}");
}
throw new Exception("Compilation failed: " + string.Join("\n", errorMessages));
}
// Get the compiled assembly and execute the code via the McpScript.Execute method
var assembly = results.CompiledAssembly;
var type = assembly.GetType("McpScript");
if (type == null)
{
throw new Exception("Could not find McpScript class in compiled assembly. Make sure your code defines a public class named 'McpScript'.");
}
var method = type.GetMethod("Execute");
if (method == null)
{
throw new Exception("Could not find Execute method in McpScript class. Make sure your code includes a public static method named 'Execute'.");
}
return method.Invoke(null, null);
}
}
private void AddEssentialReferences(CompilerParameters options)
{
// Only add the most essential references to avoid conflicts
try
{
// Core Unity and .NET references
options.ReferencedAssemblies.Add(typeof(UnityEngine.Object).Assembly.Location); // UnityEngine
options.ReferencedAssemblies.Add(typeof(UnityEditor.Editor).Assembly.Location); // UnityEditor
options.ReferencedAssemblies.Add(typeof(System.Object).Assembly.Location); // mscorlib
// Add System.Core for LINQ
var systemCore = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "System.Core");
if (systemCore != null && !string.IsNullOrEmpty(systemCore.Location))
{
options.ReferencedAssemblies.Add(systemCore.Location);
}
// Add netstandard reference
var netStandardAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "netstandard");
if (netStandardAssembly != null && !string.IsNullOrEmpty(netStandardAssembly.Location))
{
options.ReferencedAssemblies.Add(netStandardAssembly.Location);
}
// Add essential Unity modules
AddUnityModule(options, "UnityEngine.CoreModule");
AddUnityModule(options, "UnityEngine.PhysicsModule");
AddUnityModule(options, "UnityEngine.UIModule");
AddUnityModule(options, "UnityEngine.InputModule");
AddUnityModule(options, "UnityEngine.AnimationModule");
AddUnityModule(options, "UnityEngine.IMGUIModule");
}
catch (Exception ex)
{
Debug.LogWarning($"Error adding assembly references: {ex.Message}");
}
}
private void AddUnityModule(CompilerParameters options, string moduleName)
{
try
{
var assembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == moduleName);
if (assembly != null && !string.IsNullOrEmpty(assembly.Location) &&
!options.ReferencedAssemblies.Contains(assembly.Location))
{
options.ReferencedAssemblies.Add(assembly.Location);
}
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to add Unity module {moduleName}: {ex.Message}");
}
}
private void LogHandler(string message, string stackTrace, LogType type)
{
switch (type)
{
case LogType.Log:
logs.Add(message);
break;
case LogType.Warning:
warnings.Add(message);
break;
case LogType.Error:
case LogType.Exception:
case LogType.Assert:
errors.Add($"{message}\n{stackTrace}");
break;
}
}
public string[] GetLogs() => logs.ToArray();
public string[] GetErrors() => errors.ToArray();
public string[] GetWarnings() => warnings.ToArray();
}
}
```
--------------------------------------------------------------------------------
/Editor/MCPMessageSender.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine;
using System.Linq;
namespace Plugins.GamePilot.Editor.MCP
{
public class MCPMessageSender
{
private readonly MCPConnectionManager connectionManager;
public MCPMessageSender(MCPConnectionManager connectionManager)
{
this.connectionManager = connectionManager ?? throw new ArgumentNullException(nameof(connectionManager));
}
public async Task SendEditorStateAsync(MCPEditorState state)
{
if (state == null) return;
if (!connectionManager.IsConnected) return;
try
{
var message = JsonConvert.SerializeObject(new
{
type = "editorState",
data = state
}, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
await connectionManager.SendMessageAsync(message);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error sending editor state: {ex.Message}");
}
}
public async Task SendLogEntryAsync(LogEntry logEntry)
{
if (logEntry == null) return;
if (!connectionManager.IsConnected) return;
try
{
var message = JsonConvert.SerializeObject(new
{
type = "log",
data = new
{
message = logEntry.Message,
stackTrace = logEntry.StackTrace,
logType = logEntry.Type.ToString(),
timestamp = logEntry.Timestamp
}
});
await connectionManager.SendMessageAsync(message);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error sending log entry: {ex.Message}");
}
}
public async Task SendCommandResultAsync(string commandId, object result,
IEnumerable<string> logs, IEnumerable<string> errors, IEnumerable<string> warnings)
{
if (!connectionManager.IsConnected) return;
try
{
var message = JsonConvert.SerializeObject(new
{
type = "commandResult",
data = new
{
commandId,
result = result,
logs = logs ?? Array.Empty<string>(),
errors = errors ?? Array.Empty<string>(),
warnings = warnings ?? Array.Empty<string>(),
executionSuccess = errors == null || !errors.GetEnumerator().MoveNext()
}
});
await connectionManager.SendMessageAsync(message);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error sending command result: {ex.Message}");
}
}
public async Task SendErrorMessageAsync(string errorCode, string errorMessage)
{
if (!connectionManager.IsConnected) return;
try
{
var message = JsonConvert.SerializeObject(new
{
type = "error",
data = new
{
code = errorCode,
message = errorMessage,
timestamp = DateTime.UtcNow
}
});
await connectionManager.SendMessageAsync(message);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error sending error message: {ex.Message}");
}
}
public async Task SendGetLogsResponseAsync(string requestId, LogEntry[] logs)
{
if (!connectionManager.IsConnected) return;
try
{
var message = JsonConvert.SerializeObject(new
{
type = "logsResponse",
data = new
{
requestId,
logs,
timestamp = DateTime.UtcNow
}
});
await connectionManager.SendMessageAsync(message);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error sending logs response: {ex.Message}");
}
}
public async Task SendSceneInfoAsync(string requestId, MCPSceneInfo sceneInfo)
{
if (!connectionManager.IsConnected) return;
try
{
var message = JsonConvert.SerializeObject(new
{
type = "sceneInfo",
data = new
{
requestId,
sceneInfo,
timestamp = DateTime.UtcNow
}
}, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
await connectionManager.SendMessageAsync(message);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error sending scene info: {ex.Message}");
}
}
public async Task SendGameObjectsDetailsAsync(string requestId, List<MCPGameObjectDetail> gameObjectDetails)
{
if (!connectionManager.IsConnected) return;
try
{
var message = JsonConvert.SerializeObject(new
{
type = "gameObjectsDetails",
data = new
{
requestId,
gameObjectDetails,
count = gameObjectDetails?.Count ?? 0,
timestamp = DateTime.UtcNow
}
}, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
await connectionManager.SendMessageAsync(message);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error sending game objects details: {ex.Message}");
}
}
// Add new method to send pong message back to the server
public async Task SendPongAsync()
{
if (!connectionManager.IsConnected) return;
try
{
var message = JsonConvert.SerializeObject(new
{
type = "pong",
data = new
{
timestamp = DateTime.UtcNow.Ticks
}
});
await connectionManager.SendMessageAsync(message);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error sending pong message: {ex.Message}");
}
}
// Add new method to handle single GameObject details request
public async Task SendGameObjectDetailsAsync(string requestId, GameObject gameObject)
{
if (!connectionManager.IsConnected || gameObject == null) return;
try
{
// Create a list with a single game object detail
var gameObjectDetail = new MCPGameObjectDetail
{
Name = gameObject.name,
InstanceID = gameObject.GetInstanceID(),
Path = GetGameObjectPath(gameObject),
Active = gameObject.activeSelf,
ActiveInHierarchy = gameObject.activeInHierarchy,
Tag = gameObject.tag,
Layer = gameObject.layer,
LayerName = LayerMask.LayerToName(gameObject.layer),
IsStatic = gameObject.isStatic,
Transform = new MCPTransformInfo
{
Position = gameObject.transform.position,
Rotation = gameObject.transform.rotation.eulerAngles,
LocalPosition = gameObject.transform.localPosition,
LocalRotation = gameObject.transform.localRotation.eulerAngles,
LocalScale = gameObject.transform.localScale
},
Components = gameObject.GetComponents<Component>()
.Where(c => c != null)
.Select(c => new MCPComponentInfo
{
Type = c.GetType().Name,
IsEnabled = GetComponentEnabled(c),
InstanceID = c.GetInstanceID()
})
.ToList()
};
var details = new List<MCPGameObjectDetail> { gameObjectDetail };
// Use the existing method to send the details
await SendGameObjectsDetailsAsync(requestId, details);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error sending game object details: {ex.Message}");
await SendErrorMessageAsync("GAME_OBJECT_DETAIL_ERROR", ex.Message);
}
}
private string GetGameObjectPath(GameObject obj)
{
if (obj == null) return string.Empty;
string path = obj.name;
var parent = obj.transform.parent;
while (parent != null)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
private bool GetComponentEnabled(Component component)
{
// Try to check if component is enabled (for components that support it)
try
{
if (component is Behaviour behaviour)
return behaviour.enabled;
if (component is Renderer renderer)
return renderer.enabled;
if (component is Collider collider)
return collider.enabled;
}
catch
{
// Ignore any exceptions
}
// Default to true for components that don't have an enabled property
return true;
}
}
}
```
--------------------------------------------------------------------------------
/Editor/MCPConnectionManager.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
namespace Plugins.GamePilot.Editor.MCP
{
public class MCPConnectionManager
{
private static readonly string ComponentName = "MCPConnectionManager";
private ClientWebSocket webSocket;
private Uri serverUri = new Uri("ws://localhost:5010"); // Changed to allow changing
private readonly CancellationTokenSource cts = new CancellationTokenSource();
private bool isConnected = false;
private float reconnectTimer = 0f;
private readonly float reconnectInterval = 5f;
private string lastErrorMessage = string.Empty;
// Statistics
private int messagesSent = 0;
private int messagesReceived = 0;
private int reconnectAttempts = 0;
// Events
public event Action<string> OnMessageReceived;
public event Action OnConnected;
public event Action OnDisconnected;
public event Action<string> OnError;
// Properly track the connection state using the WebSocket state and our own flag
public bool IsConnected => isConnected && webSocket?.State == WebSocketState.Open;
public string LastErrorMessage => lastErrorMessage;
public int MessagesSent => messagesSent;
public int MessagesReceived => messagesReceived;
public int ReconnectAttempts => reconnectAttempts;
public Uri ServerUri
{
get => serverUri;
set => serverUri = value;
}
public MCPConnectionManager()
{
MCPLogger.InitializeComponent(ComponentName);
webSocket = new ClientWebSocket();
webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
}
public async void Connect()
{
// Double check connections that look open but may be stale
if (webSocket != null && webSocket.State == WebSocketState.Open)
{
try
{
// Try to send a ping to verify connection is truly active
bool connectionIsActive = await TestConnection();
if (connectionIsActive)
{
MCPLogger.Log(ComponentName, "WebSocket already connected and active");
return;
}
else
{
MCPLogger.Log(ComponentName, "WebSocket appears open but is stale, reconnecting...");
// Fall through to reconnection logic
}
}
catch (Exception)
{
MCPLogger.Log(ComponentName, "WebSocket appears open but failed ping test, reconnecting...");
// Fall through to reconnection logic
}
}
else if (webSocket != null && webSocket.State == WebSocketState.Connecting)
{
MCPLogger.Log(ComponentName, "WebSocket is already connecting");
return;
}
// Clean up any existing socket
if (webSocket != null)
{
try
{
if (webSocket.State == WebSocketState.Open)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,
"Reconnecting", CancellationToken.None);
}
webSocket.Dispose();
}
catch (Exception ex)
{
MCPLogger.LogWarning(ComponentName, $"Error cleaning up WebSocket: {ex.Message}");
}
}
webSocket = new ClientWebSocket();
webSocket.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
try
{
MCPLogger.Log(ComponentName, $"Connecting to MCP Server at {serverUri}");
var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, timeout.Token);
await webSocket.ConnectAsync(serverUri, linkedCts.Token);
isConnected = true;
MCPLogger.Log(ComponentName, "Successfully connected to MCP Server");
OnConnected?.Invoke();
StartReceiving();
}
catch (OperationCanceledException)
{
lastErrorMessage = "Connection attempt timed out";
MCPLogger.LogError(ComponentName, lastErrorMessage);
OnError?.Invoke(lastErrorMessage);
isConnected = false;
OnDisconnected?.Invoke();
}
catch (WebSocketException we)
{
lastErrorMessage = $"WebSocket error: {we.Message}";
MCPLogger.LogError(ComponentName, lastErrorMessage);
OnError?.Invoke(lastErrorMessage);
isConnected = false;
OnDisconnected?.Invoke();
}
catch (Exception e)
{
lastErrorMessage = $"Failed to connect: {e.Message}";
MCPLogger.LogError(ComponentName, lastErrorMessage);
OnError?.Invoke(lastErrorMessage);
isConnected = false;
OnDisconnected?.Invoke();
}
}
// Test if connection is still valid with a simple ping
private async Task<bool> TestConnection()
{
try
{
// Simple ping test - send a 1-byte message
byte[] pingData = new byte[1] { 0 };
await webSocket.SendAsync(
new ArraySegment<byte>(pingData),
WebSocketMessageType.Binary,
true,
CancellationToken.None);
return true;
}
catch
{
return false;
}
}
public void Reconnect()
{
reconnectAttempts++;
MCPLogger.Log(ComponentName, "Manually reconnecting...");
reconnectTimer = 0;
Connect();
}
public void CheckConnection()
{
if (!isConnected || webSocket?.State != WebSocketState.Open)
{
reconnectTimer += UnityEngine.Time.deltaTime;
if (reconnectTimer >= reconnectInterval)
{
reconnectAttempts++;
MCPLogger.Log(ComponentName, "Attempting reconnection...");
reconnectTimer = 0f;
Connect();
}
}
}
private async void StartReceiving()
{
var buffer = new byte[8192]; // 8KB buffer
try
{
while (webSocket.State == WebSocketState.Open && !cts.IsCancellationRequested)
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
if (result.MessageType == WebSocketMessageType.Text)
{
messagesReceived++;
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
MCPLogger.Log(ComponentName, $"Received message: {message.Substring(0, Math.Min(100, message.Length))}...");
OnMessageReceived?.Invoke(message);
}
else if (result.MessageType == WebSocketMessageType.Close)
{
MCPLogger.Log(ComponentName, "Server requested connection close");
isConnected = false;
OnDisconnected?.Invoke();
break;
}
}
}
catch (OperationCanceledException)
{
// Normal cancellation, don't log as error
MCPLogger.Log(ComponentName, "WebSocket receiving was canceled");
}
catch (WebSocketException wsEx)
{
if (!cts.IsCancellationRequested)
{
lastErrorMessage = $"WebSocket error: {wsEx.Message}";
MCPLogger.LogError(ComponentName, lastErrorMessage);
OnError?.Invoke(lastErrorMessage);
}
}
catch (Exception e)
{
if (!cts.IsCancellationRequested)
{
lastErrorMessage = $"Connection error: {e.Message}";
MCPLogger.LogError(ComponentName, lastErrorMessage);
OnError?.Invoke(lastErrorMessage);
}
}
finally
{
if (isConnected)
{
isConnected = false;
OnDisconnected?.Invoke();
}
}
}
public async Task SendMessageAsync(string message)
{
if (!isConnected || webSocket?.State != WebSocketState.Open)
{
MCPLogger.LogWarning(ComponentName, "Cannot send message: not connected");
return;
}
try
{
var buffer = Encoding.UTF8.GetBytes(message);
await webSocket.SendAsync(
new ArraySegment<byte>(buffer),
WebSocketMessageType.Text,
true,
cts.Token);
messagesSent++;
MCPLogger.Log(ComponentName, $"Sent message: {message.Substring(0, Math.Min(100, message.Length))}...");
}
catch (OperationCanceledException)
{
// Normal cancellation, don't log as error
MCPLogger.Log(ComponentName, "Send operation was canceled");
}
catch (WebSocketException wsEx)
{
lastErrorMessage = $"WebSocket send error: {wsEx.Message}";
MCPLogger.LogError(ComponentName, lastErrorMessage);
OnError?.Invoke(lastErrorMessage);
// Connection might be broken, mark as disconnected
isConnected = false;
OnDisconnected?.Invoke();
}
catch (Exception e)
{
lastErrorMessage = $"Failed to send message: {e.Message}";
MCPLogger.LogError(ComponentName, lastErrorMessage);
OnError?.Invoke(lastErrorMessage);
// Connection might be broken, mark as disconnected
isConnected = false;
OnDisconnected?.Invoke();
}
}
public void Disconnect()
{
try
{
MCPLogger.Log(ComponentName, "Disconnecting from server");
// Cancel any pending operations
if (!cts.IsCancellationRequested)
{
cts.Cancel();
}
if (webSocket != null && webSocket.State == WebSocketState.Open)
{
// Begin graceful close
var closeTask = webSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Client disconnecting",
CancellationToken.None);
// Give it a moment to close gracefully
Task.WaitAny(new[] { closeTask }, 1000);
}
// Dispose resources
if (webSocket != null)
{
webSocket.Dispose();
webSocket = null;
}
}
catch (Exception e)
{
MCPLogger.LogWarning(ComponentName, $"Error during disconnect: {e.Message}");
}
finally
{
isConnected = false;
OnDisconnected?.Invoke();
}
}
~MCPConnectionManager()
{
// Ensure resources are cleaned up
Disconnect();
}
}
}
```
--------------------------------------------------------------------------------
/mcpServer/src/websocketHandler.ts:
--------------------------------------------------------------------------------
```typescript
import { WebSocketServer, WebSocket } from 'ws';
import {
UnityMessage,
UnityEditorState,
LogEntry,
CommandPromise
} from './types.js';
export class WebSocketHandler {
private wsServer!: WebSocketServer; // Add definite assignment assertion
private _port: number; // Make this a private field, not readonly
private unityConnection: WebSocket | null = null;
private editorState: UnityEditorState = {
activeGameObjects: [],
selectedObjects: [],
playModeState: 'Stopped',
sceneHierarchy: {}
};
private logBuffer: LogEntry[] = [];
private readonly maxLogBufferSize = 1000;
private commandResultPromise: CommandPromise | null = null;
private commandStartTime: number | null = null;
private lastHeartbeat: number = 0;
private connectionEstablished: boolean = false;
private pendingRequests: Record<string, {
resolve: (data?: any) => void;
reject: (reason?: any) => void;
type: string;
}> = {};
constructor(port: number = 5010) {
this._port = port; // Store in private field
this.initializeWebSocketServer(port);
}
// Add a getter to expose port as readonly
public get port(): number {
return this._port;
}
private initializeWebSocketServer(port: number): void {
try {
this.wsServer = new WebSocketServer({ port });
this.setupWebSocketServer();
console.error(`[Unity MCP] WebSocket server started on port ${this._port}`);
} catch (error) {
console.error(`[Unity MCP] ERROR starting WebSocket server on port ${port}:`, error);
this.tryAlternativePort(port);
}
}
private tryAlternativePort(originalPort: number): void {
try {
const alternativePort = originalPort + 1;
console.error(`[Unity MCP] Trying alternative port ${alternativePort}...`);
this._port = alternativePort; // Update the private field instead of readonly property
this.wsServer = new WebSocketServer({ port: alternativePort });
this.setupWebSocketServer();
console.error(`[Unity MCP] WebSocket server started on alternative port ${this._port}`);
} catch (secondError) {
console.error(`[Unity MCP] FATAL: Could not start WebSocket server:`, secondError);
throw new Error(`Failed to start WebSocket server: ${secondError}`);
}
}
private setupWebSocketServer() {
console.error(`[Unity MCP] WebSocket server starting on port ${this._port}`);
this.wsServer.on('listening', () => {
console.error('[Unity MCP] WebSocket server is listening for connections');
});
this.wsServer.on('error', (error) => {
console.error('[Unity MCP] WebSocket server error:', error);
});
this.wsServer.on('connection', this.handleNewConnection.bind(this));
}
private handleNewConnection(ws: WebSocket): void {
console.error('[Unity MCP] Unity Editor connected');
this.unityConnection = ws;
this.connectionEstablished = true;
this.lastHeartbeat = Date.now();
// Send a simple handshake message to verify connection
this.sendHandshake();
ws.on('message', (data) => this.handleIncomingMessage(data));
ws.on('error', (error) => {
console.error('[Unity MCP] WebSocket error:', error);
this.connectionEstablished = false;
});
ws.on('close', () => {
console.error('[Unity MCP] Unity Editor disconnected');
this.unityConnection = null;
this.connectionEstablished = false;
});
// Keep the automatic heartbeat for internal connection validation
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
this.sendPing();
} else {
clearInterval(pingInterval);
}
}, 30000); // Send heartbeat every 30 seconds
}
private handleIncomingMessage(data: any): void {
try {
// Update heartbeat on any message
this.lastHeartbeat = Date.now();
const message = JSON.parse(data.toString());
console.error('[Unity MCP] Received message type:', message.type);
this.handleUnityMessage(message);
} catch (error) {
console.error('[Unity MCP] Error handling message:', error);
}
}
private sendHandshake() {
this.sendToUnity({
type: 'handshake',
data: { message: 'MCP Server Connected' }
});
}
// Renamed from sendHeartbeat to sendPing for consistency with protocol
private sendPing() {
this.sendToUnity({
type: "ping",
data: { timestamp: Date.now() }
});
}
// Helper method to safely send messages to Unity
private sendToUnity(message: any): void {
try {
if (this.unityConnection && this.unityConnection.readyState === WebSocket.OPEN) {
this.unityConnection.send(JSON.stringify(message));
}
} catch (error) {
console.error(`[Unity MCP] Error sending message: ${error}`);
this.connectionEstablished = false;
}
}
private handleUnityMessage(message: UnityMessage) {
switch (message.type) {
case 'editorState':
this.editorState = message.data;
break;
case 'commandResult':
// Resolve the pending command result promise
if (this.commandResultPromise) {
this.commandResultPromise.resolve(message.data);
this.commandResultPromise = null;
this.commandStartTime = null;
}
break;
case 'log':
this.addLogEntry(message.data);
break;
case 'pong':
// Update heartbeat reception timestamp when receiving pong
this.lastHeartbeat = Date.now();
this.connectionEstablished = true;
break;
case 'sceneInfo':
case 'gameObjectsDetails':
this.handleRequestResponse(message);
break;
default:
console.error('[Unity MCP] Unknown message type:', message);
break;
}
}
private handleRequestResponse(message: UnityMessage): void {
const requestId = message.data?.requestId;
if (requestId && this.pendingRequests[requestId]) {
// Fix the type issue by checking the property exists first
if (this.pendingRequests[requestId]) {
this.pendingRequests[requestId].resolve(message.data);
delete this.pendingRequests[requestId];
}
}
}
private addLogEntry(logEntry: LogEntry) {
// Add to buffer, removing oldest if at capacity
this.logBuffer.push(logEntry);
if (this.logBuffer.length > this.maxLogBufferSize) {
this.logBuffer.shift();
}
}
public async executeEditorCommand(code: string, timeoutMs: number = 5000): Promise<any> {
if (!this.isConnected()) {
throw new Error('Unity Editor is not connected');
}
try {
// Start timing the command execution
this.commandStartTime = Date.now();
// Send the command to Unity
this.sendToUnity({
type: 'executeEditorCommand',
data: { code }
});
// Wait for result with timeout
return await Promise.race([
new Promise((resolve, reject) => {
this.commandResultPromise = { resolve, reject };
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(
`Command execution timed out after ${timeoutMs/1000} seconds`
)), timeoutMs)
)
]);
} catch (error) {
// Reset command promise state if there's an error
this.commandResultPromise = null;
this.commandStartTime = null;
throw error;
}
}
// Return the current editor state - only used by tools, doesn't request updates
public getEditorState(): UnityEditorState {
return this.editorState;
}
public getLogEntries(options: {
types?: string[],
count?: number,
fields?: string[],
messageContains?: string,
stackTraceContains?: string,
timestampAfter?: string,
timestampBefore?: string
} = {}): Partial<LogEntry>[] {
const {
types,
count = 100,
fields,
messageContains,
stackTraceContains,
timestampAfter,
timestampBefore
} = options;
// Apply all filters
let filteredLogs = this.filterLogs(types, messageContains, stackTraceContains,
timestampAfter, timestampBefore);
// Apply count limit
filteredLogs = filteredLogs.slice(-count);
// Apply field selection if specified
if (fields?.length) {
return this.selectFields(filteredLogs, fields);
}
return filteredLogs;
}
private filterLogs(types?: string[], messageContains?: string,
stackTraceContains?: string, timestampAfter?: string,
timestampBefore?: string): LogEntry[] {
return this.logBuffer.filter(log => {
// Type filter
if (types && !types.includes(log.logType)) return false;
// Message content filter
if (messageContains && !log.message.includes(messageContains)) return false;
// Stack trace content filter
if (stackTraceContains && !log.stackTrace.includes(stackTraceContains)) return false;
// Timestamp filters
if (timestampAfter && new Date(log.timestamp) < new Date(timestampAfter)) return false;
if (timestampBefore && new Date(log.timestamp) > new Date(timestampBefore)) return false;
return true;
});
}
private selectFields(logs: LogEntry[], fields: string[]): Partial<LogEntry>[] {
return logs.map(log => {
const selectedFields: Partial<LogEntry> = {};
fields.forEach(field => {
if (field in log) {
selectedFields[field as keyof LogEntry] = log[field as keyof LogEntry];
}
});
return selectedFields;
});
}
public isConnected(): boolean {
// More robust connection check
if (this.unityConnection === null || this.unityConnection.readyState !== WebSocket.OPEN) {
return false;
}
// Check if we've received messages from Unity recently
if (!this.connectionEstablished) {
return false;
}
// Check if we've received a heartbeat in the last 60 seconds
const heartbeatTimeout = 60000; // 60 seconds
if (Date.now() - this.lastHeartbeat > heartbeatTimeout) {
console.error('[Unity MCP] Connection may be stale - no recent communication');
return false;
}
return true;
}
public requestEditorState() {
this.sendToUnity({
type: 'requestEditorState',
data: {}
});
}
public async requestSceneInfo(detailLevel: string): Promise<any> {
return this.makeUnityRequest('getSceneInfo', { detailLevel }, 'sceneInfo');
}
public async requestGameObjectsInfo(instanceIDs: number[], detailLevel: string): Promise<any> {
return this.makeUnityRequest('getGameObjectsInfo', { instanceIDs, detailLevel }, 'gameObjectDetails');
}
private async makeUnityRequest(type: string, data: any, resultField: string): Promise<any> {
if (!this.isConnected()) {
throw new Error('Unity Editor is not connected');
}
const requestId = crypto.randomUUID();
data.requestId = requestId;
// Create a promise that will be resolved when we get the response
const responsePromise = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
delete this.pendingRequests[requestId];
reject(new Error(`Request for ${type} timed out`));
}, 10000); // 10 second timeout
this.pendingRequests[requestId] = {
resolve: (data) => {
clearTimeout(timeout);
resolve(data[resultField]);
},
reject,
type
};
});
// Send the request to Unity
this.sendToUnity({
type,
data
});
return responsePromise;
}
// Support for file system tools by adding a method to send generic messages
public async sendMessage(message: string | object) {
if (this.unityConnection && this.unityConnection.readyState === WebSocket.OPEN) {
const messageStr = typeof message === 'string' ? message : JSON.stringify(message);
return new Promise<void>((resolve, reject) => {
this.unityConnection!.send(messageStr, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
return Promise.resolve();
}
public async close() {
if (this.unityConnection) {
try {
this.unityConnection.close();
} catch (error) {
console.error('[Unity MCP] Error closing Unity connection:', error);
}
this.unityConnection = null;
}
return new Promise<void>((resolve) => {
try {
this.wsServer.close(() => {
console.error('[Unity MCP] WebSocket server closed');
resolve();
});
} catch (error) {
console.error('[Unity MCP] Error closing WebSocket server:', error);
resolve(); // Resolve anyway to allow the process to exit
}
});
}
}
```
--------------------------------------------------------------------------------
/Editor/MCPMessageHandler.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Linq;
namespace Plugins.GamePilot.Editor.MCP
{
public class MCPMessageHandler
{
private readonly MCPDataCollector dataCollector;
private readonly MCPCodeExecutor codeExecutor;
private readonly MCPMessageSender messageSender;
public MCPMessageHandler(MCPDataCollector dataCollector, MCPMessageSender messageSender)
{
this.dataCollector = dataCollector ?? throw new ArgumentNullException(nameof(dataCollector));
this.messageSender = messageSender ?? throw new ArgumentNullException(nameof(messageSender));
this.codeExecutor = new MCPCodeExecutor();
}
public async void HandleMessage(string messageJson)
{
if (string.IsNullOrEmpty(messageJson)) return;
try
{
Debug.Log($"[MCP] Received message: {messageJson}");
var message = JsonConvert.DeserializeObject<MCPMessage>(messageJson);
if (message == null) return;
switch (message.Type)
{
case "selectGameObject":
await HandleSelectGameObjectAsync(message.Data);
break;
case "togglePlayMode":
await HandleTogglePlayModeAsync();
break;
case "executeEditorCommand":
await HandleExecuteCommandAsync(message.Data);
break;
case "requestEditorState": // Consolidated to a single message type
await HandleRequestEditorStateAsync(message.Data);
break;
case "getLogs":
await HandleGetLogsAsync(message.Data);
break;
case "handshake":
await HandleHandshakeAsync(message.Data);
break;
case "getSceneInfo":
await HandleGetSceneInfoAsync(message.Data);
break;
case "getGameObjectsInfo":
await HandleGetGameObjectsInfoAsync(message.Data);
break;
case "ping": // Renamed from 'heartbeat' to 'ping' to match protocol
await HandlePingAsync(message.Data);
break;
default:
Debug.LogWarning($"[MCP] Unknown message type: {message.Type}");
break;
}
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error handling message: {ex.Message}\nMessage: {messageJson}");
}
}
private async Task HandleHandshakeAsync(JToken data)
{
try
{
string message = data["message"]?.ToString() ?? "Server connected";
Debug.Log($"[MCP] Handshake received: {message}");
// Send a simple acknowledgment, but don't send full editor state until requested
await messageSender.SendPongAsync();
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error handling handshake: {ex.Message}");
}
}
// Add a ping handler to respond to server heartbeats
private async Task HandlePingAsync(JToken data)
{
try
{
// Simply respond with a pong message
await messageSender.SendPongAsync();
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error handling ping: {ex.Message}");
}
}
private async Task HandleSelectGameObjectAsync(JToken data)
{
try
{
string objectPath = data["path"]?.ToString();
string requestId = data["requestId"]?.ToString();
if (string.IsNullOrEmpty(objectPath)) return;
var obj = GameObject.Find(objectPath);
if (obj != null)
{
Selection.activeGameObject = obj;
Debug.Log($"[MCP] Selected GameObject: {objectPath}");
// If requestId was provided, send back object details
if (!string.IsNullOrEmpty(requestId))
{
// Use the new method to send details for a single GameObject
await messageSender.SendGameObjectDetailsAsync(requestId, obj);
}
}
else
{
Debug.LogWarning($"[MCP] GameObject not found: {objectPath}");
if (!string.IsNullOrEmpty(requestId))
{
await messageSender.SendErrorMessageAsync("OBJECT_NOT_FOUND", $"GameObject not found: {objectPath}");
}
}
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error selecting GameObject: {ex.Message}");
}
}
private async Task HandleTogglePlayModeAsync()
{
try
{
EditorApplication.isPlaying = !EditorApplication.isPlaying;
Debug.Log($"[MCP] Toggled play mode to: {EditorApplication.isPlaying}");
// Send updated editor state after toggling play mode
var editorState = dataCollector.GetEditorState();
await messageSender.SendEditorStateAsync(editorState);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error toggling play mode: {ex.Message}");
}
}
private async Task HandleExecuteCommandAsync(JToken data)
{
try
{
// Support both old and new parameter naming
string commandId = data["commandId"]?.ToString() ?? data["id"]?.ToString() ?? Guid.NewGuid().ToString();
string code = data["code"]?.ToString();
if (string.IsNullOrEmpty(code))
{
Debug.LogWarning("[MCP] Received empty code to execute");
await messageSender.SendErrorMessageAsync("EMPTY_CODE", "Received empty code to execute");
return;
}
Debug.Log($"[MCP] Executing command: {commandId}\n{code}");
var result = codeExecutor.ExecuteCode(code);
// Send back the results
await messageSender.SendCommandResultAsync(
commandId,
result,
codeExecutor.GetLogs(),
codeExecutor.GetErrors(),
codeExecutor.GetWarnings()
);
Debug.Log($"[MCP] Command execution completed");
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error executing command: {ex.Message}");
}
}
// Renamed from HandleGetEditorStateAsync to HandleRequestEditorStateAsync for clarity
private async Task HandleRequestEditorStateAsync(JToken data)
{
try
{
// Get current editor state with enhanced project info
var editorState = GetEnhancedEditorState();
// Send it to the server
await messageSender.SendEditorStateAsync(editorState);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error getting editor state: {ex.Message}");
}
}
// New method to get enhanced editor state with more project information
private MCPEditorState GetEnhancedEditorState()
{
// Get base editor state from data collector
var state = dataCollector.GetEditorState();
// Add additional project information
EnhanceEditorStateWithProjectInfo(state);
return state;
}
// Add additional project information to the editor state
private void EnhanceEditorStateWithProjectInfo(MCPEditorState state)
{
try
{
// Add information about current rendering pipeline
state.RenderPipeline = GetCurrentRenderPipeline();
// Add current build target platform
state.BuildTarget = EditorUserBuildSettings.activeBuildTarget.ToString();
// Add project name
state.ProjectName = Application.productName;
// Add graphics API info
state.GraphicsDeviceType = SystemInfo.graphicsDeviceType.ToString();
// Add Unity version
state.UnityVersion = Application.unityVersion;
// Add current scene name
var currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
state.CurrentSceneName = currentScene.name;
state.CurrentScenePath = currentScene.path;
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error enhancing editor state: {ex.Message}");
}
}
// Helper to determine current render pipeline
private string GetCurrentRenderPipeline()
{
if (UnityEngine.Rendering.GraphicsSettings.defaultRenderPipeline == null)
return "Built-in Render Pipeline";
var pipelineType = UnityEngine.Rendering.GraphicsSettings.defaultRenderPipeline.GetType().Name;
// Try to make the name more user-friendly
if (pipelineType.Contains("Universal"))
return "Universal Render Pipeline (URP)";
else if (pipelineType.Contains("HD"))
return "High Definition Render Pipeline (HDRP)";
else if (pipelineType.Contains("Lightweight"))
return "Lightweight Render Pipeline (LWRP)";
else
return pipelineType;
}
private async Task HandleGetLogsAsync(JToken data)
{
try
{
string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString();
int count = data["count"]?.Value<int>() ?? 50;
// Get logs from collector
var logs = dataCollector.GetRecentLogs(count);
// Send logs back to server
await messageSender.SendGetLogsResponseAsync(requestId, logs);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error getting logs: {ex.Message}");
}
}
private async Task HandleGetSceneInfoAsync(JToken data)
{
try
{
string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString();
string detailLevelStr = data["detailLevel"]?.ToString() ?? "RootObjectsOnly";
// Parse the detail level
SceneInfoDetail detailLevel;
if (!Enum.TryParse(detailLevelStr, true, out detailLevel))
{
detailLevel = SceneInfoDetail.RootObjectsOnly;
}
// Get scene info
var sceneInfo = dataCollector.GetCurrentSceneInfo(detailLevel);
// Send it to the server
await messageSender.SendSceneInfoAsync(requestId, sceneInfo);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error handling getSceneInfo: {ex.Message}");
await messageSender.SendErrorMessageAsync("SCENE_INFO_ERROR", ex.Message);
}
}
private async Task HandleGetGameObjectsInfoAsync(JToken data)
{
try
{
string requestId = data["requestId"]?.ToString() ?? Guid.NewGuid().ToString();
string detailLevelStr = data["detailLevel"]?.ToString() ?? "BasicInfo";
// Get the list of instance IDs
int[] instanceIDs;
if (data["instanceIDs"] != null && data["instanceIDs"].Type == JTokenType.Array)
{
instanceIDs = data["instanceIDs"].ToObject<int[]>();
}
else
{
await messageSender.SendErrorMessageAsync("INVALID_PARAMS", "instanceIDs array is required");
return;
}
// Parse the detail level
GameObjectInfoDetail detailLevel;
if (!Enum.TryParse(detailLevelStr, true, out detailLevel))
{
detailLevel = GameObjectInfoDetail.BasicInfo;
}
// Get game object details
var gameObjectDetails = dataCollector.GetGameObjectsInfo(instanceIDs, detailLevel);
// Send to server
await messageSender.SendGameObjectsDetailsAsync(requestId, gameObjectDetails);
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error handling getGameObjectsInfo: {ex.Message}");
await messageSender.SendErrorMessageAsync("GAME_OBJECT_INFO_ERROR", ex.Message);
}
}
}
internal class MCPMessage
{
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("data")]
public JToken Data { get; set; }
}
}
```
--------------------------------------------------------------------------------
/Editor/MCPDataCollector.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace Plugins.GamePilot.Editor.MCP
{
public enum SceneInfoDetail
{
RootObjectsOnly,
FullHierarchy
}
public enum GameObjectInfoDetail
{
BasicInfo,
IncludeComponents,
IncludeChildren,
IncludeComponentsAndChildren // New option to include both components and children
}
public class MCPDataCollector : IDisposable
{
private readonly Queue<LogEntry> logBuffer = new Queue<LogEntry>();
private readonly int maxLogBufferSize = 1000;
private bool isLoggingEnabled = true;
public MCPDataCollector()
{
// Start capturing logs
Application.logMessageReceived += HandleLogMessage;
}
public void Dispose()
{
// Unsubscribe to prevent memory leaks
Application.logMessageReceived -= HandleLogMessage;
}
private void HandleLogMessage(string message, string stackTrace, LogType type)
{
if (!isLoggingEnabled) return;
var logEntry = new LogEntry
{
Message = message,
StackTrace = stackTrace,
Type = type,
Timestamp = DateTime.UtcNow
};
lock (logBuffer)
{
logBuffer.Enqueue(logEntry);
while (logBuffer.Count > maxLogBufferSize)
{
logBuffer.Dequeue();
}
}
}
public bool IsLoggingEnabled
{
get => isLoggingEnabled;
set
{
if (isLoggingEnabled == value) return;
isLoggingEnabled = value;
if (value)
{
Application.logMessageReceived += HandleLogMessage;
}
else
{
Application.logMessageReceived -= HandleLogMessage;
}
}
}
public LogEntry[] GetRecentLogs(int count = 50)
{
lock (logBuffer)
{
return logBuffer.Reverse().Take(count).Reverse().ToArray();
}
}
public MCPEditorState GetEditorState()
{
var state = new MCPEditorState
{
ActiveGameObjects = GetActiveGameObjects(),
SelectedObjects = GetSelectedObjects(),
PlayModeState = EditorApplication.isPlaying ? "Playing" : "Stopped",
SceneHierarchy = GetSceneHierarchy(),
Timestamp = DateTime.UtcNow
};
return state;
}
private string[] GetActiveGameObjects()
{
try
{
var foundObjects = GameObject.FindObjectsByType<GameObject>(FindObjectsSortMode.None);
return foundObjects.Where(o => o != null).Select(obj => obj.name).ToArray();
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error getting active GameObjects: {ex.Message}");
return new string[0];
}
}
private string[] GetSelectedObjects()
{
try
{
return Selection.gameObjects.Where(o => o != null).Select(obj => obj.name).ToArray();
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error getting selected objects: {ex.Message}");
return new string[0];
}
}
private List<MCPGameObjectInfo> GetSceneHierarchy()
{
var hierarchy = new List<MCPGameObjectInfo>();
try
{
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
if (scene.IsValid())
{
var rootObjects = scene.GetRootGameObjects();
foreach (var root in rootObjects.Where(o => o != null))
{
hierarchy.Add(GetGameObjectHierarchy(root));
}
}
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error getting scene hierarchy: {ex.Message}");
}
return hierarchy;
}
private MCPGameObjectInfo GetGameObjectHierarchy(GameObject obj)
{
if (obj == null) return null;
try
{
var info = new MCPGameObjectInfo
{
Name = obj.name,
Path = GetGameObjectPath(obj),
Components = obj.GetComponents<Component>()
.Where(c => c != null)
.Select(c => c.GetType().Name)
.ToArray(),
Children = new List<MCPGameObjectInfo>(),
Active = obj.activeSelf,
Layer = obj.layer,
Tag = obj.tag
};
var transform = obj.transform;
for (int i = 0; i < transform.childCount; i++)
{
var childTransform = transform.GetChild(i);
if (childTransform != null && childTransform.gameObject != null)
{
var childInfo = GetGameObjectHierarchy(childTransform.gameObject);
if (childInfo != null)
{
info.Children.Add(childInfo);
}
}
}
return info;
}
catch (Exception ex)
{
Debug.LogWarning($"[MCP] Error processing GameObject {obj.name}: {ex.Message}");
return new MCPGameObjectInfo { Name = obj.name, Path = GetGameObjectPath(obj) };
}
}
private string GetGameObjectPath(GameObject obj)
{
if (obj == null) return string.Empty;
try
{
string path = obj.name;
Transform parent = obj.transform.parent;
while (parent != null)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
catch (Exception)
{
return obj.name;
}
}
// New method to get current scene information based on requested detail level
public MCPSceneInfo GetCurrentSceneInfo(SceneInfoDetail detailLevel)
{
try
{
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
var sceneInfo = new MCPSceneInfo
{
Name = scene.name,
Path = scene.path,
IsDirty = scene.isDirty,
RootCount = scene.rootCount
};
if (scene.IsValid())
{
var rootObjects = scene.GetRootGameObjects();
// Map scene objects based on detail level requested
switch (detailLevel)
{
case SceneInfoDetail.RootObjectsOnly:
sceneInfo.RootObjects = rootObjects
.Where(o => o != null)
.Select(o => new MCPGameObjectReference
{
Name = o.name,
InstanceID = o.GetInstanceID(),
Path = GetGameObjectPath(o),
Active = o.activeSelf,
ChildCount = o.transform.childCount
})
.ToList();
break;
case SceneInfoDetail.FullHierarchy:
sceneInfo.RootObjects = rootObjects
.Where(o => o != null)
.Select(o => GetGameObjectReferenceWithChildren(o))
.ToList();
break;
}
}
return sceneInfo;
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error getting scene info: {ex.Message}");
return new MCPSceneInfo { Name = "Error", ErrorMessage = ex.Message };
}
}
// Helper method to create a game object reference with children
private MCPGameObjectReference GetGameObjectReferenceWithChildren(GameObject obj)
{
if (obj == null) return null;
var reference = new MCPGameObjectReference
{
Name = obj.name,
InstanceID = obj.GetInstanceID(),
Path = GetGameObjectPath(obj),
Active = obj.activeSelf,
ChildCount = obj.transform.childCount,
Children = new List<MCPGameObjectReference>()
};
// Add all children
var transform = obj.transform;
for (int i = 0; i < transform.childCount; i++)
{
var childTransform = transform.GetChild(i);
if (childTransform != null && childTransform.gameObject != null)
{
var childRef = GetGameObjectReferenceWithChildren(childTransform.gameObject);
if (childRef != null)
{
reference.Children.Add(childRef);
}
}
}
return reference;
}
// Get detailed information about specific game objects
public List<MCPGameObjectDetail> GetGameObjectsInfo(int[] objectInstanceIDs, GameObjectInfoDetail detailLevel)
{
var results = new List<MCPGameObjectDetail>();
try
{
foreach (var id in objectInstanceIDs)
{
var obj = EditorUtility.InstanceIDToObject(id) as GameObject;
if (obj != null)
{
results.Add(GetGameObjectDetail(obj, detailLevel));
}
}
}
catch (Exception ex)
{
Debug.LogError($"[MCP] Error getting game object details: {ex.Message}");
}
return results;
}
// Helper method to get detailed info about a game object
private MCPGameObjectDetail GetGameObjectDetail(GameObject obj, GameObjectInfoDetail detailLevel)
{
var detail = new MCPGameObjectDetail
{
Name = obj.name,
InstanceID = obj.GetInstanceID(),
Path = GetGameObjectPath(obj),
Active = obj.activeSelf,
ActiveInHierarchy = obj.activeInHierarchy,
Tag = obj.tag,
Layer = obj.layer,
LayerName = LayerMask.LayerToName(obj.layer),
IsStatic = obj.isStatic,
Transform = new MCPTransformInfo
{
Position = obj.transform.position,
Rotation = obj.transform.rotation.eulerAngles,
LocalPosition = obj.transform.localPosition,
LocalRotation = obj.transform.localRotation.eulerAngles,
LocalScale = obj.transform.localScale
}
};
// Include components if requested
if (detailLevel == GameObjectInfoDetail.IncludeComponents ||
detailLevel == GameObjectInfoDetail.IncludeComponentsAndChildren)
{
detail.Components = obj.GetComponents<Component>()
.Where(c => c != null)
.Select(c => new MCPComponentInfo
{
Type = c.GetType().Name,
IsEnabled = GetComponentEnabled(c),
InstanceID = c.GetInstanceID()
})
.ToList();
}
// Include children if requested
if (detailLevel == GameObjectInfoDetail.IncludeChildren ||
detailLevel == GameObjectInfoDetail.IncludeComponentsAndChildren)
{
detail.Children = new List<MCPGameObjectDetail>();
var transform = obj.transform;
for (int i = 0; i < transform.childCount; i++)
{
var childTransform = transform.GetChild(i);
if (childTransform != null && childTransform.gameObject != null)
{
// When including both components and children, make sure to include components for children too
GameObjectInfoDetail childDetailLevel = detailLevel == GameObjectInfoDetail.IncludeComponentsAndChildren
? GameObjectInfoDetail.IncludeComponentsAndChildren
: GameObjectInfoDetail.BasicInfo;
var childDetail = GetGameObjectDetail(
childTransform.gameObject,
childDetailLevel);
detail.Children.Add(childDetail);
}
}
}
return detail;
}
// Helper for getting component enabled state
private bool GetComponentEnabled(Component component)
{
// Try to check if component is enabled (for components that support it)
try
{
if (component is Behaviour behaviour)
return behaviour.enabled;
if (component is Renderer renderer)
return renderer.enabled;
if (component is Collider collider)
return collider.enabled;
}
catch
{
// Ignore any exceptions
}
// Default to true for components that don't have an enabled property
return true;
}
}
}
```
--------------------------------------------------------------------------------
/mcpServer/src/filesystemTools.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { WebSocketHandler } from './websocketHandler.js';
import fs from 'fs/promises';
import { Dirent } from 'fs'; // Import Dirent instead of DirEnt
import path from 'path';
import { createTwoFilesPatch } from 'diff';
import { minimatch } from 'minimatch';
import {
ReadFileArgsSchema,
ReadMultipleFilesArgsSchema,
WriteFileArgsSchema,
EditFileArgsSchema,
ListDirectoryArgsSchema,
DirectoryTreeArgsSchema,
SearchFilesArgsSchema,
GetFileInfoArgsSchema,
FindAssetsByTypeArgsSchema
} from './toolDefinitions.js';
// Interface definitions
interface FileInfo {
size: number;
created: Date;
modified: Date;
accessed: Date;
isDirectory: boolean;
isFile: boolean;
permissions: string;
}
interface TreeEntry {
name: string;
type: 'file' | 'directory';
children?: TreeEntry[];
}
// Helper functions
async function validatePath(requestedPath: string, assetRootPath: string): Promise<string> {
// If path is empty or just quotes, use the asset root path directly
if (!requestedPath || requestedPath.trim() === '' || requestedPath.trim() === '""' || requestedPath.trim() === "''") {
console.error(`[Unity MCP] Using asset root path: ${assetRootPath}`);
return assetRootPath;
}
// Clean the path to remove any unexpected quotes or escape characters
let cleanPath = requestedPath.replace(/['"\\]/g, '');
// Handle empty path after cleaning
if (!cleanPath || cleanPath.trim() === '') {
return assetRootPath;
}
// Normalize path to handle both Windows and Unix-style paths
const normalized = path.normalize(cleanPath);
// Resolve the path (absolute or relative)
let absolute = resolvePathToAssetRoot(normalized, assetRootPath);
const resolvedPath = path.resolve(absolute);
// Ensure we don't escape out of the Unity project folder
validatePathSecurity(resolvedPath, assetRootPath, requestedPath);
return resolvedPath;
}
function resolvePathToAssetRoot(pathToResolve: string, assetRootPath: string): string {
if (path.isAbsolute(pathToResolve)) {
console.error(`[Unity MCP] Absolute path requested: ${pathToResolve}`);
// If the absolute path is outside the project, try alternative resolutions
if (!pathToResolve.startsWith(assetRootPath)) {
// Try 1: Treat as relative path
const tryRelative = path.join(assetRootPath, pathToResolve);
try {
fs.access(tryRelative);
console.error(`[Unity MCP] Treating as relative path: ${tryRelative}`);
return tryRelative;
} catch {
// Try 2: Try to extract path relative to Assets if it contains "Assets"
if (pathToResolve.includes('Assets')) {
const assetsIndex = pathToResolve.indexOf('Assets');
const relativePath = pathToResolve.substring(assetsIndex + 7); // +7 to skip "Assets/"
const newPath = path.join(assetRootPath, relativePath);
console.error(`[Unity MCP] Trying via Assets path: ${newPath}`);
try {
fs.access(newPath);
return newPath;
} catch { /* Use original if all else fails */ }
}
}
}
return pathToResolve;
} else {
// For relative paths, join with asset root path
return path.join(assetRootPath, pathToResolve);
}
}
function validatePathSecurity(resolvedPath: string, assetRootPath: string, requestedPath: string): void {
if (!resolvedPath.startsWith(assetRootPath) && requestedPath.trim() !== '') {
console.error(`[Unity MCP] Access denied: Path ${requestedPath} is outside the project directory`);
console.error(`[Unity MCP] Resolved to: ${resolvedPath}`);
console.error(`[Unity MCP] Expected to be within: ${assetRootPath}`);
throw new Error(`Access denied: Path ${requestedPath} is outside the Unity project directory`);
}
}
async function getFileStats(filePath: string): Promise<FileInfo> {
const stats = await fs.stat(filePath);
return {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
permissions: stats.mode.toString(8).slice(-3),
};
}
async function searchFiles(
rootPath: string,
pattern: string,
excludePatterns: string[] = []
): Promise<string[]> {
const results: string[] = [];
async function search(currentPath: string) {
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
try {
// Check if path matches any exclude pattern
const relativePath = path.relative(rootPath, fullPath);
if (isPathExcluded(relativePath, excludePatterns)) continue;
if (entry.name.toLowerCase().includes(pattern.toLowerCase())) {
results.push(fullPath);
}
if (entry.isDirectory()) {
await search(fullPath);
}
} catch (error) {
// Skip invalid paths during search
continue;
}
}
}
await search(rootPath);
return results;
}
function isPathExcluded(relativePath: string, excludePatterns: string[]): boolean {
return excludePatterns.some(pattern => {
const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`;
return minimatch(relativePath, globPattern, { dot: true });
});
}
function normalizeLineEndings(text: string): string {
return text.replace(/\r\n/g, '\n');
}
function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string {
// Ensure consistent line endings for diff
const normalizedOriginal = normalizeLineEndings(originalContent);
const normalizedNew = normalizeLineEndings(newContent);
return createTwoFilesPatch(
filepath,
filepath,
normalizedOriginal,
normalizedNew,
'original',
'modified'
);
}
async function applyFileEdits(
filePath: string,
edits: Array<{oldText: string, newText: string}>,
dryRun = false
): Promise<string> {
// Read file content and normalize line endings
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
// Apply edits sequentially
let modifiedContent = content;
for (const edit of edits) {
const normalizedOld = normalizeLineEndings(edit.oldText);
const normalizedNew = normalizeLineEndings(edit.newText);
// If exact match exists, use it
if (modifiedContent.includes(normalizedOld)) {
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
continue;
}
// Try line-by-line matching with whitespace flexibility
modifiedContent = applyFlexibleLineEdit(modifiedContent, normalizedOld, normalizedNew);
}
// Create unified diff
const diff = createUnifiedDiff(content, modifiedContent, filePath);
const formattedDiff = formatDiff(diff);
if (!dryRun) {
await fs.writeFile(filePath, modifiedContent, 'utf-8');
}
return formattedDiff;
}
function applyFlexibleLineEdit(content: string, oldText: string, newText: string): string {
const oldLines = oldText.split('\n');
const contentLines = content.split('\n');
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
const potentialMatch = contentLines.slice(i, i + oldLines.length);
// Compare lines with normalized whitespace
const isMatch = oldLines.every((oldLine, j) => {
const contentLine = potentialMatch[j];
return oldLine.trim() === contentLine.trim();
});
if (isMatch) {
// Preserve indentation
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
const newLines = newText.split('\n').map((line, j) => {
if (j === 0) return originalIndent + line.trimStart();
const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || '';
const newIndent = line.match(/^\s*/)?.[0] || '';
if (oldIndent && newIndent) {
const relativeIndent = newIndent.length - oldIndent.length;
return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart();
}
return line;
});
contentLines.splice(i, oldLines.length, ...newLines);
return contentLines.join('\n');
}
}
throw new Error(`Could not find exact match for edit:\n${oldText}`);
}
function formatDiff(diff: string): string {
let numBackticks = 3;
while (diff.includes('`'.repeat(numBackticks))) {
numBackticks++;
}
return `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
}
async function buildDirectoryTree(currentPath: string, assetRootPath: string, maxDepth: number = 5, currentDepth: number = 0): Promise<TreeEntry[]> {
if (currentDepth >= maxDepth) {
return [{ name: "...", type: "directory" }];
}
const validPath = await validatePath(currentPath, assetRootPath);
const entries = await fs.readdir(validPath, { withFileTypes: true });
const result: TreeEntry[] = [];
for (const entry of entries) {
const entryData: TreeEntry = {
name: entry.name,
type: entry.isDirectory() ? 'directory' : 'file'
};
if (entry.isDirectory()) {
const subPath = path.join(currentPath, entry.name);
entryData.children = await buildDirectoryTree(subPath, assetRootPath, maxDepth, currentDepth + 1);
}
result.push(entryData);
}
return result;
}
// Function to recognize Unity asset types based on file extension
function getUnityAssetType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
// Common Unity asset types
const assetTypes: Record<string, string> = {
'.unity': 'Scene',
'.prefab': 'Prefab',
'.mat': 'Material',
'.fbx': 'Model',
'.cs': 'Script',
'.anim': 'Animation',
'.controller': 'Animator Controller',
'.asset': 'ScriptableObject',
'.png': 'Texture',
'.jpg': 'Texture',
'.jpeg': 'Texture',
'.tga': 'Texture',
'.wav': 'Audio',
'.mp3': 'Audio',
'.ogg': 'Audio',
'.shader': 'Shader',
'.compute': 'Compute Shader',
'.ttf': 'Font',
'.otf': 'Font',
'.physicMaterial': 'Physics Material',
'.mask': 'Avatar Mask',
'.playable': 'Playable',
'.mixer': 'Audio Mixer',
'.renderTexture': 'Render Texture',
'.lighting': 'Lighting Settings',
'.shadervariants': 'Shader Variants',
'.spriteatlas': 'Sprite Atlas',
'.guiskin': 'GUI Skin',
'.flare': 'Flare',
'.brush': 'Brush',
'.overrideController': 'Animator Override Controller',
'.preset': 'Preset',
'.terrainlayer': 'Terrain Layer',
'.signal': 'Signal',
'.signalasset': 'Signal Asset',
'.giparams': 'Global Illumination Parameters',
'.cubemap': 'Cubemap',
};
return assetTypes[ext] || 'Other';
}
// Get file extensions for Unity asset types
function getFileExtensionsForType(type: string): string[] {
type = type.toLowerCase();
const extensionMap: Record<string, string[]> = {
'scene': ['.unity'],
'prefab': ['.prefab'],
'material': ['.mat'],
'script': ['.cs'],
'model': ['.fbx', '.obj', '.blend', '.max', '.mb', '.ma'],
'texture': ['.png', '.jpg', '.jpeg', '.tga', '.tif', '.tiff', '.psd', '.exr', '.hdr'],
'audio': ['.wav', '.mp3', '.ogg', '.aiff', '.aif'],
'animation': ['.anim'],
'animator': ['.controller'],
'shader': ['.shader', '.compute', '.cginc']
};
return extensionMap[type] || [];
}
// Handler function to process filesystem tools
export async function handleFilesystemTool(name: string, args: any, projectPath: string) {
try {
switch (name) {
case "read_file": {
const parsed = ReadFileArgsSchema.safeParse(args);
if (!parsed.success) return invalidArgsResponse(parsed.error);
const validPath = await validatePath(parsed.data.path, projectPath);
const content = await fs.readFile(validPath, "utf-8");
return { content: [{ type: "text", text: content }] };
}
case "read_multiple_files": {
const parsed = ReadMultipleFilesArgsSchema.safeParse(args);
if (!parsed.success) return invalidArgsResponse(parsed.error);
const results = await Promise.all(
parsed.data.paths.map(async (filePath: string) => {
try {
const validPath = await validatePath(filePath, projectPath);
const content = await fs.readFile(validPath, "utf-8");
return `${filePath}:\n${content}\n`;
} catch (error) {
return `${filePath}: Error - ${getErrorMessage(error)}`;
}
}),
);
return { content: [{ type: "text", text: results.join("\n---\n") }] };
}
case "write_file": {
const parsed = WriteFileArgsSchema.safeParse(args);
if (!parsed.success) return invalidArgsResponse(parsed.error);
const validPath = await validatePath(parsed.data.path, projectPath);
// Ensure directory exists
const dirPath = path.dirname(validPath);
await fs.mkdir(dirPath, { recursive: true });
await fs.writeFile(validPath, parsed.data.content, "utf-8");
return {
content: [{ type: "text", text: `Successfully wrote to ${parsed.data.path}` }]
};
}
case "edit_file": {
const parsed = EditFileArgsSchema.safeParse(args);
if (!parsed.success) return invalidArgsResponse(parsed.error);
const validPath = await validatePath(parsed.data.path, projectPath);
const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun);
return { content: [{ type: "text", text: result }] };
}
case "list_directory": {
const parsed = ListDirectoryArgsSchema.safeParse(args);
if (!parsed.success) return invalidArgsResponse(parsed.error);
const validPath = await validatePath(parsed.data.path, projectPath);
const entries = await fs.readdir(validPath, { withFileTypes: true });
const formatted = entries
.map((entry) => formatDirectoryEntry(entry, validPath))
.join("\n");
return { content: [{ type: "text", text: formatted }] };
}
case "directory_tree": {
const parsed = DirectoryTreeArgsSchema.safeParse(args);
if (!parsed.success) return invalidArgsResponse(parsed.error);
const treeData = await buildDirectoryTree(parsed.data.path, projectPath, parsed.data.maxDepth);
return { content: [{ type: "text", text: JSON.stringify(treeData, null, 2) }] };
}
case "search_files": {
const parsed = SearchFilesArgsSchema.safeParse(args);
if (!parsed.success) return invalidArgsResponse(parsed.error);
const validPath = await validatePath(parsed.data.path, projectPath);
const results = await searchFiles(validPath, parsed.data.pattern, parsed.data.excludePatterns);
return {
content: [{
type: "text",
text: results.length > 0
? `Found ${results.length} results:\n${results.join("\n")}`
: "No matches found"
}]
};
}
case "get_file_info": {
const parsed = GetFileInfoArgsSchema.safeParse(args);
if (!parsed.success) return invalidArgsResponse(parsed.error);
const validPath = await validatePath(parsed.data.path, projectPath);
const info = await getFileStats(validPath);
// Add Unity-specific info if it's an asset file
const additionalInfo: Record<string, string> = {};
if (info.isFile) {
additionalInfo.assetType = getUnityAssetType(validPath);
}
const formattedInfo = Object.entries({ ...info, ...additionalInfo })
.map(([key, value]) => `${key}: ${value}`)
.join("\n");
return { content: [{ type: "text", text: formattedInfo }] };
}
case "find_assets_by_type": {
const parsed = FindAssetsByTypeArgsSchema.safeParse(args);
if (!parsed.success) return invalidArgsResponse(parsed.error);
const assetType = parsed.data.assetType.replace(/['"]/g, '');
const searchPath = parsed.data.searchPath.replace(/['"]/g, '');
const maxDepth = parsed.data.maxDepth;
console.error(`[Unity MCP] Finding assets of type "${assetType}" in path "${searchPath}" with maxDepth ${maxDepth}`);
const validPath = await validatePath(searchPath, projectPath);
const results = await findAssetsByType(assetType, validPath, maxDepth, projectPath);
return {
content: [{
type: "text",
text: results.length > 0
? `Found ${results.length} ${assetType} assets:\n${JSON.stringify(results, null, 2)}`
: `No "${assetType}" assets found in ${searchPath || "Assets"}`
}]
};
}
default:
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true,
};
}
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${getErrorMessage(error)}` }],
isError: true,
};
}
}
function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function invalidArgsResponse(error: any) {
return {
content: [{ type: "text", text: `Invalid arguments: ${error}` }],
isError: true
};
}
// Fixed function to use proper Dirent type
function formatDirectoryEntry(entry: Dirent, basePath: string): string {
if (entry.isDirectory()) {
return `[DIR] ${entry.name}`;
} else {
// For files, detect Unity asset type
const filePath = path.join(basePath, entry.name);
const assetType = getUnityAssetType(filePath);
return `[${assetType}] ${entry.name}`;
}
}
async function findAssetsByType(
assetType: string,
searchPath: string,
maxDepth: number,
projectPath: string
): Promise<Array<{path: string, name: string, type: string}>> {
const results: Array<{path: string, name: string, type: string}> = [];
const extensions = getFileExtensionsForType(assetType);
async function searchAssets(dir: string, currentDepth: number = 1) {
// Stop recursion if we've reached the maximum depth
if (maxDepth !== -1 && currentDepth > maxDepth) {
return;
}
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = path.relative(projectPath, fullPath);
if (entry.isDirectory()) {
// Recursively search subdirectories
await searchAssets(fullPath, currentDepth + 1);
} else {
// Check if the file matches the requested asset type
const ext = path.extname(entry.name).toLowerCase();
if (extensions.length === 0) {
// If no extensions specified, match by Unity asset type
const fileAssetType = getUnityAssetType(fullPath);
if (fileAssetType.toLowerCase() === assetType.toLowerCase()) {
results.push({
path: relativePath,
name: entry.name,
type: fileAssetType
});
}
} else if (extensions.includes(ext)) {
// Match by extension
results.push({
path: relativePath,
name: entry.name,
type: assetType
});
}
}
}
} catch (error) {
console.error(`Error accessing directory ${dir}:`, error);
}
}
await searchAssets(searchPath);
return results;
}
// This function is deprecated and now just a stub
export function registerFilesystemTools(server: Server, wsHandler: WebSocketHandler) {
console.log("Filesystem tools are now registered in toolDefinitions.ts");
}
```
--------------------------------------------------------------------------------
/mcpServer/src/toolDefinitions.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { WebSocketHandler } from './websocketHandler.js';
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { zodToJsonSchema } from 'zod-to-json-schema';
import {
CallToolRequestSchema,
ListToolsRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import path from 'path';
// Import handleFilesystemTool using ES module syntax instead of require
import { handleFilesystemTool } from './filesystemTools.js';
// File operation schemas - defined here to be used in tool definitions
export const ReadFileArgsSchema = z.object({
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.'),
});
export const ReadMultipleFilesArgsSchema = z.object({
paths: z.array(z.string()).describe('Array of file paths to read. Paths can be absolute or relative to Unity project Assets folder.'),
});
export const WriteFileArgsSchema = z.object({
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.'),
content: z.string().describe('Content to write to the file'),
});
export const EditOperation = z.object({
oldText: z.string().describe('Text to search for - must match exactly'),
newText: z.string().describe('Text to replace with')
});
export const EditFileArgsSchema = z.object({
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.'),
edits: z.array(EditOperation).describe('Array of edit operations to apply'),
dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')
});
export const ListDirectoryArgsSchema = z.object({
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.'),
});
export const DirectoryTreeArgsSchema = z.object({
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.'),
maxDepth: z.number().optional().default(5).describe('Maximum depth to traverse'),
});
export const SearchFilesArgsSchema = z.object({
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.'),
pattern: z.string().describe('Pattern to search for'),
excludePatterns: z.array(z.string()).optional().default([]).describe('Patterns to exclude')
});
export const GetFileInfoArgsSchema = z.object({
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.'),
});
export const FindAssetsByTypeArgsSchema = z.object({
assetType: z.string().describe('Type of assets to find (e.g., "Material", "Prefab", "Scene", "Script")'),
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.'),
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.'),
});
export function registerTools(server: Server, wsHandler: WebSocketHandler) {
// Determine project path from environment variable (which now should include 'Assets')
const projectPath = process.env.UNITY_PROJECT_PATH || path.resolve(process.cwd());
const projectRootPath = projectPath.endsWith(`Assets${path.sep}`)
? projectPath.slice(0, -7) // Remove 'Assets/'
: projectPath;
console.error(`[Unity MCP ToolDefinitions] Using project path: ${projectPath}`);
console.error(`[Unity MCP ToolDefinitions] Using project root path: ${projectRootPath}`);
// List all available tools (both Unity and filesystem tools)
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
// Unity Editor tools
{
name: 'get_current_scene_info',
description: 'Retrieve information about the current scene in Unity Editor with configurable detail level',
category: 'Editor State',
tags: ['unity', 'editor', 'scene'],
inputSchema: {
type: 'object',
properties: {
detailLevel: {
type: 'string',
enum: ['RootObjectsOnly', 'FullHierarchy'],
description: 'RootObjectsOnly: Returns just root GameObjects. FullHierarchy: Returns complete hierarchy with all children.',
default: 'RootObjectsOnly'
}
},
additionalProperties: false
},
returns: {
type: 'object',
description: 'Returns information about the current scene and its hierarchy based on requested detail level'
}
},
{
name: 'get_game_objects_info',
description: 'Retrieve detailed information about specific GameObjects in the current scene',
category: 'Editor State',
tags: ['unity', 'editor', 'gameobjects'],
inputSchema: {
type: 'object',
properties: {
instanceIDs: {
type: 'array',
items: {
type: 'number'
},
description: 'Array of GameObject instance IDs to get information for',
minItems: 1
},
detailLevel: {
type: 'string',
enum: ['BasicInfo', 'IncludeComponents', 'IncludeChildren', 'IncludeComponentsAndChildren'],
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.',
default: 'IncludeComponents'
}
},
required: ['instanceIDs'],
additionalProperties: false
},
returns: {
type: 'object',
description: 'Returns detailed information about the requested GameObjects'
}
},
{
name: 'execute_editor_command',
description: 'Execute C# code directly in the Unity Editor - allows full flexibility including custom namespaces and multiple classes',
category: 'Editor Control',
tags: ['unity', 'editor', 'command', 'c#'],
inputSchema: {
type: 'object',
properties: {
code: {
type: 'string',
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.',
minLength: 1
}
},
required: ['code'],
additionalProperties: false
},
returns: {
type: 'object',
description: 'Returns the execution result, execution time, and status'
}
},
{
name: 'get_logs',
description: 'Retrieve Unity Editor logs with filtering options',
category: 'Debugging',
tags: ['unity', 'editor', 'logs', 'debugging'],
inputSchema: {
type: 'object',
properties: {
types: {
type: 'array',
items: {
type: 'string',
enum: ['Log', 'Warning', 'Error', 'Exception']
},
description: 'Filter logs by type'
},
count: {
type: 'number',
description: 'Maximum number of log entries to return',
minimum: 1,
maximum: 1000
},
fields: {
type: 'array',
items: {
type: 'string',
enum: ['message', 'stackTrace', 'logType', 'timestamp']
},
description: 'Specify which fields to include in the output'
},
messageContains: {
type: 'string',
description: 'Filter logs by message content'
},
stackTraceContains: {
type: 'string',
description: 'Filter logs by stack trace content'
},
timestampAfter: {
type: 'string',
description: 'Filter logs after this ISO timestamp'
},
timestampBefore: {
type: 'string',
description: 'Filter logs before this ISO timestamp'
}
},
additionalProperties: false
},
returns: {
type: 'array',
description: 'Returns an array of log entries matching the specified filters'
}
},
{
name: 'verify_connection',
description: 'Verify that the MCP server has an active connection to Unity Editor',
category: 'Connection',
tags: ['unity', 'editor', 'connection'],
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false
},
returns: {
type: 'object',
description: 'Returns connection status information'
}
},
{
name: 'get_editor_state',
description: 'Get the current Unity Editor state including project information',
category: 'Editor State',
tags: ['unity', 'editor', 'project'],
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false
},
returns: {
type: 'object',
description: 'Returns detailed information about the current Unity Editor state, project settings, and environment'
}
},
// Filesystem tools - defined alongside Unity tools
{
name: "read_file",
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.",
category: "Filesystem",
tags: ['unity', 'filesystem', 'file'],
inputSchema: zodToJsonSchema(ReadFileArgsSchema),
},
{
name: "read_multiple_files",
description: "Read the contents of multiple files from the Unity project simultaneously.",
category: "Filesystem",
tags: ['unity', 'filesystem', 'file', 'batch'],
inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema),
},
{
name: "write_file",
description: "Create a new file or completely overwrite an existing file in the Unity project.",
category: "Filesystem",
tags: ['unity', 'filesystem', 'file', 'write'],
inputSchema: zodToJsonSchema(WriteFileArgsSchema),
},
{
name: "edit_file",
description: "Make precise edits to a text file in the Unity project. Returns a git-style diff showing changes.",
category: "Filesystem",
tags: ['unity', 'filesystem', 'file', 'edit'],
inputSchema: zodToJsonSchema(EditFileArgsSchema),
},
{
name: "list_directory",
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.",
category: "Filesystem",
tags: ['unity', 'filesystem', 'directory', 'list'],
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema),
},
{
name: "directory_tree",
description: "Get a recursive tree view of files and directories in the Unity project as a JSON structure.",
category: "Filesystem",
tags: ['unity', 'filesystem', 'directory', 'tree'],
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema),
},
{
name: "search_files",
description: "Recursively search for files and directories matching a pattern in the Unity project.",
category: "Filesystem",
tags: ['unity', 'filesystem', 'search'],
inputSchema: zodToJsonSchema(SearchFilesArgsSchema),
},
{
name: "get_file_info",
description: "Retrieve detailed metadata about a file or directory in the Unity project.",
category: "Filesystem",
tags: ['unity', 'filesystem', 'file', 'metadata'],
inputSchema: zodToJsonSchema(GetFileInfoArgsSchema),
},
{
name: "find_assets_by_type",
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.",
category: "Filesystem",
tags: ['unity', 'filesystem', 'assets', 'search'],
inputSchema: zodToJsonSchema(FindAssetsByTypeArgsSchema),
},
],
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Special case for verify_connection which should work even if not connected
if (name === 'verify_connection') {
try {
const isConnected = wsHandler.isConnected();
// Always request fresh editor state if connected
if (isConnected) {
wsHandler.requestEditorState();
}
return {
content: [{
type: 'text',
text: JSON.stringify({
connected: isConnected,
timestamp: new Date().toISOString(),
message: isConnected
? 'Unity Editor is connected'
: 'Unity Editor is not connected. Please ensure the Unity Editor is running with the MCP plugin.'
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: JSON.stringify({
connected: false,
timestamp: new Date().toISOString(),
message: 'Error checking connection status',
error: error instanceof Error ? error.message : 'Unknown error'
}, null, 2)
}]
};
}
}
// Check if this is a filesystem tool
const filesystemTools = [
"read_file", "read_multiple_files", "write_file", "edit_file",
"list_directory", "directory_tree", "search_files", "get_file_info",
"find_assets_by_type"
];
if (filesystemTools.includes(name)) {
try {
return await handleFilesystemTool(name, args, projectPath);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${errorMessage}` }],
isError: true,
};
}
}
// For all other tools (Unity-specific), verify connection first
if (!wsHandler.isConnected()) {
throw new McpError(
ErrorCode.InternalError,
'Unity Editor is not connected. Please first verify the connection using the verify_connection tool, ' +
'and ensure the Unity Editor is running with the MCP plugin and that the WebSocket connection is established.'
);
}
switch (name) {
case 'get_editor_state': {
try {
// Always request a fresh editor state before returning
wsHandler.requestEditorState();
// Wait a moment for the response to arrive
await new Promise(resolve => setTimeout(resolve, 1000));
// Return the current editor state
const editorState = wsHandler.getEditorState();
return {
content: [{
type: 'text',
text: JSON.stringify(editorState, null, 2)
}]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to get editor state: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
case 'get_current_scene_info': {
try {
const detailLevel = (args?.detailLevel as string) || 'RootObjectsOnly';
// Send request to Unity and wait for response
const sceneInfo = await wsHandler.requestSceneInfo(detailLevel);
return {
content: [{
type: 'text',
text: JSON.stringify(sceneInfo, null, 2)
}]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to get scene info: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
case 'get_game_objects_info': {
try {
if (!args?.instanceIDs || !Array.isArray(args.instanceIDs)) {
throw new McpError(
ErrorCode.InvalidParams,
'instanceIDs array is required'
);
}
const instanceIDs = args.instanceIDs;
const detailLevel = (args?.detailLevel as string) || 'IncludeComponents';
// Send request to Unity and wait for response
const gameObjectsInfo = await wsHandler.requestGameObjectsInfo(instanceIDs, detailLevel);
return {
content: [{
type: 'text',
text: JSON.stringify(gameObjectsInfo, null, 2)
}]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to get GameObject info: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
case 'execute_editor_command': {
try {
if (!args?.code) {
throw new McpError(
ErrorCode.InvalidParams,
'The code parameter is required'
);
}
const startTime = Date.now();
const result = await wsHandler.executeEditorCommand(args.code as string);
const executionTime = Date.now() - startTime;
return {
content: [{
type: 'text',
text: JSON.stringify({
result,
executionTime: `${executionTime}ms`,
status: 'success'
}, null, 2)
}]
};
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('timed out')) {
throw new McpError(
ErrorCode.InternalError,
'Command execution timed out. This may indicate a long-running operation or an issue with the Unity Editor.'
);
}
if (error.message.includes('NullReferenceException')) {
throw new McpError(
ErrorCode.InvalidParams,
'The code attempted to access a null object. Please check that all GameObject references exist.'
);
}
if (error.message.includes('not connected')) {
throw new McpError(
ErrorCode.InternalError,
'Unity Editor connection was lost during command execution. Please verify the connection and try again.'
);
}
}
throw new McpError(
ErrorCode.InternalError,
`Failed to execute command: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
case 'get_logs': {
try {
const options = {
types: args?.types as string[] | undefined,
count: args?.count as number | undefined,
fields: args?.fields as string[] | undefined,
messageContains: args?.messageContains as string | undefined,
stackTraceContains: args?.stackTraceContains as string | undefined,
timestampAfter: args?.timestampAfter as string | undefined,
timestampBefore: args?.timestampBefore as string | undefined
};
const logs = wsHandler.getLogEntries(options);
return {
content: [{
type: 'text',
text: JSON.stringify(logs, null, 2)
}]
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Failed to retrieve logs: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
});
}
```
--------------------------------------------------------------------------------
/Editor/UI/MCPDebugWindow.cs:
--------------------------------------------------------------------------------
```csharp
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
namespace Plugins.GamePilot.Editor.MCP
{
[Serializable]
public class MCPDebugSettings
{
public int port = 5010;
public bool autoReconnect = false;
public bool globalLoggingEnabled = false;
public Dictionary<string, bool> componentLoggingEnabled = new Dictionary<string, bool>();
}
public class MCPDebugWindow : EditorWindow
{
[SerializeField]
private VisualTreeAsset uxml;
[SerializeField]
private StyleSheet uss;
[SerializeField]
private VisualTreeAsset m_VisualTreeAsset = default;
private Label connectionStatusLabel;
private Button connectButton;
private Button disconnectButton;
private Toggle autoReconnectToggle;
private TextField serverPortField;
// Component logging toggles
private Dictionary<string, Toggle> logToggles = new Dictionary<string, Toggle>();
// Connection info labels
private Label lastErrorLabel;
private Label connectionTimeLabel;
// Statistics elements
private Label messagesSentLabel;
private Label messagesReceivedLabel;
private Label reconnectAttemptsLabel;
// Statistics counters
private int messagesSent = 0;
private int messagesReceived = 0;
private int reconnectAttempts = 0;
private DateTime? connectionStartTime = null;
// Settings
private MCPDebugSettings settings;
private string settingsPath;
[MenuItem("Window/MCP Debug")]
public static void ShowWindow()
{
MCPDebugWindow wnd = GetWindow<MCPDebugWindow>();
wnd.titleContent = new GUIContent("MCP Debug");
wnd.minSize = new Vector2(400, 500);
}
private void OnEnable()
{
// Get the path to save settings
settingsPath = GetSettingsPath();
// Load or create settings
LoadSettings();
}
private string GetSettingsPath()
{
// Get the script location
var script = MonoScript.FromScriptableObject(this);
var scriptPath = AssetDatabase.GetAssetPath(script);
var directoryPath = Path.GetDirectoryName(scriptPath);
// Create settings path in the same directory
return Path.Combine(directoryPath, "MCPDebugSettings.json");
}
private void LoadSettings()
{
settings = new MCPDebugSettings();
try
{
// Check if settings file exists
if (File.Exists(settingsPath))
{
string json = File.ReadAllText(settingsPath);
settings = JsonConvert.DeserializeObject<MCPDebugSettings>(json);
Debug.Log($"[MCP] [MCPDebugWindow] Loaded settings from {settingsPath}");
}
else
{
// Create default settings
settings = new MCPDebugSettings();
SaveSettings();
Debug.Log($"[MCP] [MCPDebugWindow] Created default settings at {settingsPath}");
}
}
catch (Exception ex)
{
Debug.LogError($"[MCP] [MCPDebugWindow] Error loading settings: {ex.Message}");
settings = new MCPDebugSettings();
}
// Apply settings to MCPLogger
MCPLogger.GlobalLoggingEnabled = settings.globalLoggingEnabled;
// Apply component logging settings
foreach (var pair in settings.componentLoggingEnabled)
{
MCPLogger.SetComponentLoggingEnabled(pair.Key, pair.Value);
}
}
private void SaveSettings()
{
try
{
// Save settings using Newtonsoft.Json which supports dictionaries directly
string json = JsonConvert.SerializeObject(settings, Formatting.Indented);
File.WriteAllText(settingsPath, json);
Debug.Log($"[MCP] [MCPDebugWindow] Saved settings to {settingsPath}");
}
catch (Exception ex)
{
Debug.LogError($"[MCP] [MCPDebugWindow] Error saving settings: {ex.Message}");
}
}
public void CreateGUI()
{
VisualElement root = rootVisualElement;
if (uxml != null)
{
uxml.CloneTree(root);
}
else
{
Debug.LogError("VisualTreeAsset not found. Please check the path.");
}
if (uss != null)
{
root.styleSheets.Add(uss);
} else
{
Debug.LogError("StyleSheet not found. Please check the path.");
}
// Get UI elements
connectionStatusLabel = root.Q<Label>("connection-status");
connectButton = root.Q<Button>("connect-button");
disconnectButton = root.Q<Button>("disconnect-button");
autoReconnectToggle = root.Q<Toggle>("auto-reconnect-toggle");
serverPortField = root.Q<TextField>("server-port-field");
lastErrorLabel = root.Q<Label>("last-error-value");
connectionTimeLabel = root.Q<Label>("connection-time-value");
messagesSentLabel = root.Q<Label>("messages-sent-value");
messagesReceivedLabel = root.Q<Label>("messages-received-value");
reconnectAttemptsLabel = root.Q<Label>("reconnect-attempts-value");
// Apply settings to UI
serverPortField.value = settings.port.ToString();
autoReconnectToggle.value = settings.autoReconnect;
// Setup UI events
connectButton.clicked += OnConnectClicked;
disconnectButton.clicked += OnDisconnectClicked;
autoReconnectToggle.RegisterValueChangedCallback(OnAutoReconnectChanged);
serverPortField.RegisterValueChangedCallback(OnPortChanged);
// Setup component logging toggles
SetupComponentLoggingToggles(root);
// Initialize UI with current state
UpdateUIFromState();
// Register for updates
EditorApplication.update += OnEditorUpdate;
}
private void OnPortChanged(ChangeEvent<string> evt)
{
if (int.TryParse(evt.newValue, out int port) && port >= 1 && port <= 65535)
{
settings.port = port;
SaveSettings();
}
}
private void CreateFallbackUI(VisualElement root)
{
// Create a simple fallback UI if UXML fails to load
root.Add(new Label("MCP Debug Window - UXML not found") { style = { fontSize = 16, marginBottom = 10 } });
// Removed serverUrlField - only using port field as requested
serverPortField = new TextField("Port (Default: 5010)") { value = "5010" };
root.Add(serverPortField);
var connectButton = new Button(OnConnectClicked) { text = "Connect" };
root.Add(connectButton);
var disconnectButton = new Button(OnDisconnectClicked) { text = "Disconnect" };
root.Add(disconnectButton);
var autoReconnectToggle = new Toggle("Auto Reconnect");
autoReconnectToggle.RegisterValueChangedCallback(OnAutoReconnectChanged);
root.Add(autoReconnectToggle);
connectionStatusLabel = new Label("Status: Not Connected");
root.Add(connectionStatusLabel);
}
private void SetupComponentLoggingToggles(VisualElement root)
{
var loggingContainer = root.Q<VisualElement>("logging-container");
// Register MCPDebugWindow as a component for logging
MCPLogger.InitializeComponent("MCPDebugWindow", settings.componentLoggingEnabled.ContainsKey("MCPDebugWindow") ?
settings.componentLoggingEnabled["MCPDebugWindow"] : false);
// Global logging toggle
var globalToggle = new Toggle("Enable All Logging");
globalToggle.value = settings.globalLoggingEnabled;
globalToggle.RegisterValueChangedCallback(evt => {
settings.globalLoggingEnabled = evt.newValue;
MCPLogger.GlobalLoggingEnabled = evt.newValue;
SaveSettings();
// First make sure all components are properly initialized before updating UI
EnsureComponentsInitialized();
// Update all component toggles to show they're enabled/disabled
foreach (var componentName in MCPLogger.GetRegisteredComponents())
{
if (logToggles.TryGetValue(componentName, out var toggle))
{
// Don't disable the toggle UI, just update its interactable state
toggle.SetEnabled(true);
}
}
});
loggingContainer.Add(globalToggle);
// Add a separator
var separator = new VisualElement();
separator.style.height = 1;
separator.style.marginTop = 5;
separator.style.marginBottom = 5;
separator.style.backgroundColor = new Color(0.3f, 0.3f, 0.3f);
loggingContainer.Add(separator);
// Ensure all components are initialized
EnsureComponentsInitialized();
// Create toggles for standard components
string[] standardComponents = {
"MCPManager",
"MCPConnectionManager",
"MCPDataCollector",
"MCPMessageHandler",
"MCPCodeExecutor",
"MCPMessageSender",
"MCPDebugWindow" // Add the debug window itself
};
foreach (string componentName in standardComponents)
{
bool isEnabled = settings.componentLoggingEnabled.ContainsKey(componentName) ?
settings.componentLoggingEnabled[componentName] : false;
CreateLoggingToggle(loggingContainer, componentName, $"Enable {componentName} logging", isEnabled);
}
// Add any additional registered components not in our standard list
foreach (var componentName in MCPLogger.GetRegisteredComponents())
{
if (!logToggles.ContainsKey(componentName))
{
bool isEnabled = settings.componentLoggingEnabled.ContainsKey(componentName) ?
settings.componentLoggingEnabled[componentName] : false;
CreateLoggingToggle(loggingContainer, componentName, $"Enable {componentName} logging", isEnabled);
}
}
}
// Make sure all components are initialized in the logger
private void EnsureComponentsInitialized()
{
string[] standardComponents = {
"MCPManager",
"MCPConnectionManager",
"MCPDataCollector",
"MCPMessageHandler",
"MCPCodeExecutor",
"MCPMessageSender",
"MCPDebugWindow"
};
foreach (string componentName in standardComponents)
{
MCPLogger.InitializeComponent(componentName, false);
}
}
private void CreateLoggingToggle(VisualElement container, string componentName, string label, bool initialValue)
{
var toggle = new Toggle(label);
toggle.value = initialValue;
// Make all toggles interactive, they'll work based on global enabled state
toggle.SetEnabled(true);
toggle.RegisterValueChangedCallback(evt => OnLoggingToggleChanged(componentName, evt.newValue));
container.Add(toggle);
logToggles[componentName] = toggle;
}
private void OnLoggingToggleChanged(string componentName, bool enabled)
{
MCPLogger.SetComponentLoggingEnabled(componentName, enabled);
settings.componentLoggingEnabled[componentName] = enabled;
SaveSettings();
}
private void OnConnectClicked()
{
// Always use localhost for the WebSocket URL
string serverUrl = "ws://localhost";
// Get the server port from the text field
string portText = serverPortField.value;
// If port is empty, default to 5010
if (string.IsNullOrWhiteSpace(portText))
{
portText = "5010";
serverPortField.value = portText;
}
// Validate port format
if (!int.TryParse(portText, out int port) || port < 1 || port > 65535)
{
EditorUtility.DisplayDialog("Invalid Port",
"Please enter a valid port number between 1 and 65535.", "OK");
return;
}
// Save the port setting
settings.port = port;
SaveSettings();
try {
// Create the WebSocket URL with the specified port
Uri uri = new Uri($"{serverUrl}:{port}");
// If we have access to the ConnectionManager, try to update its server URI
var connectionManager = GetConnectionManager();
if (connectionManager != null)
{
// Use reflection to set the serverUri field if it exists
var serverUriField = typeof(MCPConnectionManager).GetField("serverUri",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance);
if (serverUriField != null)
{
serverUriField.SetValue(connectionManager, uri);
}
}
// Initiate manual connection
if (MCPManager.IsInitialized)
{
MCPManager.RetryConnection();
connectionStartTime = DateTime.Now;
UpdateUIFromState();
}
else
{
MCPManager.Initialize();
connectionStartTime = DateTime.Now;
UpdateUIFromState();
}
}
catch (UriFormatException)
{
EditorUtility.DisplayDialog("Invalid URL",
"The URL format is invalid.", "OK");
}
catch (Exception ex)
{
EditorUtility.DisplayDialog("Connection Error",
$"Error connecting to server: {ex.Message}", "OK");
}
}
private void OnDisconnectClicked()
{
if (MCPManager.IsInitialized)
{
MCPManager.Shutdown();
connectionStartTime = null;
UpdateUIFromState();
}
}
private void OnAutoReconnectChanged(ChangeEvent<bool> evt)
{
settings.autoReconnect = evt.newValue;
SaveSettings();
if (MCPManager.IsInitialized)
{
MCPManager.EnableAutoReconnect(evt.newValue);
}
}
private void OnEditorUpdate()
{
// Update connection status and statistics
UpdateUIFromState();
}
private void UpdateUIFromState()
{
bool isInitialized = MCPManager.IsInitialized;
bool isConnected = MCPManager.IsConnected;
// Only log status if logging is enabled
if (MCPLogger.IsLoggingEnabled("MCPDebugWindow"))
{
Debug.Log($"[MCP] [MCPDebugWindow] Status check: IsInitialized={isInitialized}, IsConnected={isConnected}");
}
// Update status label
if (!isInitialized)
{
connectionStatusLabel.text = "Not Initialized";
connectionStatusLabel.RemoveFromClassList("status-connected");
connectionStatusLabel.RemoveFromClassList("status-connecting");
connectionStatusLabel.AddToClassList("status-disconnected");
}
else if (isConnected)
{
connectionStatusLabel.text = "Connected";
connectionStatusLabel.RemoveFromClassList("status-disconnected");
connectionStatusLabel.RemoveFromClassList("status-connecting");
connectionStatusLabel.AddToClassList("status-connected");
// If we're in the connected state, make sure connectionStartTime is set
// This ensures the timer works properly
if (!connectionStartTime.HasValue)
{
connectionStartTime = DateTime.Now;
}
}
else
{
connectionStatusLabel.text = "Disconnected";
connectionStatusLabel.RemoveFromClassList("status-connected");
connectionStatusLabel.RemoveFromClassList("status-connecting");
connectionStatusLabel.AddToClassList("status-disconnected");
// Reset connection time when disconnected
connectionStartTime = null;
}
// Update button states
connectButton.SetEnabled(!isConnected);
disconnectButton.SetEnabled(isInitialized);
serverPortField.SetEnabled(!isConnected); // Only allow port changes when disconnected
// Update connection time if connected
if (connectionStartTime.HasValue && isConnected)
{
TimeSpan duration = DateTime.Now - connectionStartTime.Value;
connectionTimeLabel.text = $"{duration.Hours:00}:{duration.Minutes:00}:{duration.Seconds:00}";
}
else
{
connectionTimeLabel.text = "00:00:00";
}
// Update statistics if available
if (isInitialized)
{
// Get connection statistics
var connectionManager = GetConnectionManager();
if (connectionManager != null)
{
messagesSentLabel.text = connectionManager.MessagesSent.ToString();
messagesReceivedLabel.text = connectionManager.MessagesReceived.ToString();
reconnectAttemptsLabel.text = connectionManager.ReconnectAttempts.ToString();
lastErrorLabel.text = !string.IsNullOrEmpty(connectionManager.LastErrorMessage)
? connectionManager.LastErrorMessage : "None";
}
else
{
messagesSentLabel.text = "0";
messagesReceivedLabel.text = "0";
reconnectAttemptsLabel.text = "0";
lastErrorLabel.text = "None";
}
}
}
// Helper to access connection manager through reflection if needed
private MCPConnectionManager GetConnectionManager()
{
if (!MCPManager.IsInitialized)
return null;
// Try to access the connection manager using reflection
try
{
var managerType = typeof(MCPManager);
var field = managerType.GetField("connectionManager",
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Static);
if (field != null)
{
return field.GetValue(null) as MCPConnectionManager;
}
}
catch (Exception ex)
{
Debug.LogError($"Error accessing connection manager: {ex.Message}");
}
return null;
}
private void OnDisable()
{
// Unregister from editor updates
EditorApplication.update -= OnEditorUpdate;
// Save settings one last time when window is closed
SaveSettings();
}
}
}
```