Merge branch 'master' of https://github.com/justinpbarnett/unity-mcp
commit
44530de8da
|
|
@ -31,3 +31,4 @@ CONTRIBUTING.md.meta
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
.aider*
|
.aider*
|
||||||
|
.DS_Store*
|
||||||
122
README.md
122
README.md
|
|
@ -1,12 +1,22 @@
|
||||||
# Unity MCP ✨
|
# Unity MCP ✨
|
||||||
|
|
||||||
**Connect your Unity Editor to LLMs using the Model Context Protocol.**
|
|
||||||
|
[](https://unity.com/releases/editor/archive)
|
||||||
|
[](https://www.python.org)
|
||||||
|
[](https://modelcontextprotocol.io/introduction)
|
||||||
|

|
||||||
|

|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Create your Unity apps with LLMs!**
|
||||||
|
|
||||||
Unity MCP acts as a bridge, allowing AI assistants (like Claude, Cursor) to interact directly with your Unity Editor via a local **MCP (Model Context Protocol) Client**. Give your LLM tools to manage assets, control scenes, edit scripts, and automate tasks within Unity.
|
Unity MCP acts as a bridge, allowing AI assistants (like Claude, Cursor) to interact directly with your Unity Editor via a local **MCP (Model Context Protocol) Client**. Give your LLM tools to manage assets, control scenes, edit scripts, and automate tasks within Unity.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## <picture><source media="(prefers-color-scheme: dark)" srcset="https://github.com/justinpbarnett/unity-mcp/assets/11047284/c279675a-dd58-406b-9613-5b16b5c6bb63"><source media="(prefers-color-scheme: light)" srcset="https://github.com/justinpbarnett/unity-mcp/assets/11047284/b54f891d-961b-4048-a9c4-3af46e2a52fc"><img alt="UnityMCP Workflow" width="100%" style="max-width: 600px; display: block; margin-left: auto; margin-right: auto;"></picture>
|
|
||||||
|
|
||||||
## Key Features 🚀
|
## Key Features 🚀
|
||||||
|
|
||||||
|
|
@ -15,8 +25,8 @@ Unity MCP acts as a bridge, allowing AI assistants (like Claude, Cursor) to inte
|
||||||
* **🤖 Automation:** Automate repetitive Unity workflows.
|
* **🤖 Automation:** Automate repetitive Unity workflows.
|
||||||
* **🧩 Extensible:** Designed to work with various MCP Clients.
|
* **🧩 Extensible:** Designed to work with various MCP Clients.
|
||||||
|
|
||||||
<details>
|
<details open>
|
||||||
<summary><strong>Expand for Available Tools...</strong></summary>
|
<summary><strong> Available Tools </strong></summary>
|
||||||
|
|
||||||
Your LLM can use functions like:
|
Your LLM can use functions like:
|
||||||
|
|
||||||
|
|
@ -25,6 +35,7 @@ Unity MCP acts as a bridge, allowing AI assistants (like Claude, Cursor) to inte
|
||||||
* `manage_editor`: Controls and queries the editor's state and settings.
|
* `manage_editor`: Controls and queries the editor's state and settings.
|
||||||
* `manage_scene`: Manages scenes (load, save, create, get hierarchy, etc.).
|
* `manage_scene`: Manages scenes (load, save, create, get hierarchy, etc.).
|
||||||
* `manage_asset`: Performs asset operations (import, create, modify, delete, etc.).
|
* `manage_asset`: Performs asset operations (import, create, modify, delete, etc.).
|
||||||
|
* `manage_shader`: Performs shader CRUD operations (create, read, modify, delete).
|
||||||
* `manage_gameobject`: Manages GameObjects: create, modify, delete, find, and component operations.
|
* `manage_gameobject`: Manages GameObjects: create, modify, delete, find, and component operations.
|
||||||
* `execute_menu_item`: Executes a menu item via its path (e.g., "File/Save Project").
|
* `execute_menu_item`: Executes a menu item via its path (e.g., "File/Save Project").
|
||||||
</details>
|
</details>
|
||||||
|
|
@ -48,8 +59,6 @@ Unity MCP connects your tools using two components:
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Click to view required software...</strong></summary>
|
|
||||||
|
|
||||||
* **Git CLI:** For cloning the server code. [Download Git](https://git-scm.com/downloads)
|
* **Git CLI:** For cloning the server code. [Download Git](https://git-scm.com/downloads)
|
||||||
* **Python:** Version 3.12 or newer. [Download Python](https://www.python.org/downloads/)
|
* **Python:** Version 3.12 or newer. [Download Python](https://www.python.org/downloads/)
|
||||||
|
|
@ -61,9 +70,31 @@ Unity MCP connects your tools using two components:
|
||||||
```
|
```
|
||||||
* **An MCP Client:**
|
* **An MCP Client:**
|
||||||
* [Claude Desktop](https://claude.ai/download)
|
* [Claude Desktop](https://claude.ai/download)
|
||||||
|
* [Claude Code](https://github.com/anthropics/claude-code)
|
||||||
* [Cursor](https://www.cursor.com/en/downloads)
|
* [Cursor](https://www.cursor.com/en/downloads)
|
||||||
|
* [Visual Studio Code Copilot](https://code.visualstudio.com/docs/copilot/overview)
|
||||||
* *(Others may work with manual config)*
|
* *(Others may work with manual config)*
|
||||||
</details>
|
* <details> <summary><strong>[Optional] Roslyn for Advanced Script Validation</strong></summary>
|
||||||
|
|
||||||
|
For **Strict** validation level that catches undefined namespaces, types, and methods:
|
||||||
|
|
||||||
|
**Method 1: NuGet for Unity (Recommended)**
|
||||||
|
1. Install [NuGetForUnity](https://github.com/GlitchEnzo/NuGetForUnity)
|
||||||
|
2. Go to `Window > NuGet Package Manager`
|
||||||
|
3. Search for `Microsoft.CodeAnalysis.CSharp` and install the package
|
||||||
|
5. Go to `Player Settings > Scripting Define Symbols`
|
||||||
|
6. Add `USE_ROSLYN`
|
||||||
|
7. Restart Unity
|
||||||
|
|
||||||
|
**Method 2: Manual DLL Installation**
|
||||||
|
1. Download Microsoft.CodeAnalysis.CSharp.dll and dependencies from [NuGet](https://www.nuget.org/packages/Microsoft.CodeAnalysis.CSharp/)
|
||||||
|
2. Place DLLs in `Assets/Plugins/` folder
|
||||||
|
3. Ensure .NET compatibility settings are correct
|
||||||
|
4. Add `USE_ROSLYN` to Scripting Define Symbols
|
||||||
|
5. Restart Unity
|
||||||
|
|
||||||
|
**Note:** Without Roslyn, script validation falls back to basic structural checks. Roslyn enables full C# compiler diagnostics with precise error reporting.</details>
|
||||||
|
|
||||||
|
|
||||||
### Step 1: Install the Unity Package (Bridge)
|
### Step 1: Install the Unity Package (Bridge)
|
||||||
|
|
||||||
|
|
@ -81,10 +112,12 @@ Unity MCP connects your tools using two components:
|
||||||
|
|
||||||
Connect your MCP Client (Claude, Cursor, etc.) to the Python server you installed in Step 1.
|
Connect your MCP Client (Claude, Cursor, etc.) to the Python server you installed in Step 1.
|
||||||
|
|
||||||
**Option A: Auto-Configure (Recommended for Claude/Cursor)**
|
<img width="609" alt="image" src="https://github.com/user-attachments/assets/cef3a639-4677-4fd8-84e7-2d82a04d55bb" />
|
||||||
|
|
||||||
|
**Option A: Auto-Configure (Recommended for Claude/Cursor/VSC Copilot)**
|
||||||
|
|
||||||
1. In Unity, go to `Window > Unity MCP`.
|
1. In Unity, go to `Window > Unity MCP`.
|
||||||
2. Click `Auto Configure Claude` or `Auto Configure Cursor`.
|
2. Click `Auto Configure` on the IDE you uses.
|
||||||
3. Look for a green status indicator 🟢 and "Connected". *(This attempts to modify the MCP Client's config file automatically)*.
|
3. Look for a green status indicator 🟢 and "Connected". *(This attempts to modify the MCP Client's config file automatically)*.
|
||||||
|
|
||||||
**Option B: Manual Configuration**
|
**Option B: Manual Configuration**
|
||||||
|
|
@ -163,6 +196,20 @@ If Auto-Configure fails or you use a different client:
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
**Option C: Claude Code Registration**
|
||||||
|
|
||||||
|
If you're using Claude Code, you can register the MCP server using these commands:
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
|
```bash
|
||||||
|
claude mcp add UnityMCP -- uv --directory /[PATH_TO]/UnityMCP/UnityMcpServer/src run server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```bash
|
||||||
|
claude mcp add UnityMCP -- "C:/Users/USERNAME/AppData/Roaming/Python/Python313/Scripts/uv.exe" --directory "C:/Users/USERNAME/AppData/Local/Programs/UnityMCP/UnityMcpServer/src" run server.py
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Usage ▶️
|
## Usage ▶️
|
||||||
|
|
@ -173,8 +220,41 @@ If Auto-Configure fails or you use a different client:
|
||||||
|
|
||||||
3. **Interact!** Unity tools should now be available in your MCP Client.
|
3. **Interact!** Unity tools should now be available in your MCP Client.
|
||||||
|
|
||||||
Example Prompt: `Create a 3D player controller.`
|
Example Prompt: `Create a 3D player controller`, `Create a yellow and bridge sun`, `Create a cool shader and apply it on a cube`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Dev Plans (Besides PR) 📝
|
||||||
|
|
||||||
|
### 🔴 High Priority
|
||||||
|
- [ ] **Asset Generation Improvements** - Enhanced server request handling and asset pipeline optimization
|
||||||
|
- [ ] **Code Generation Enhancements** - Improved generated code quality and error handling
|
||||||
|
- [ ] **Robust Error Handling** - Comprehensive error messages, recovery mechanisms, and graceful degradation
|
||||||
|
- [ ] **Remote Connection Support** - Enable seamless remote connection between Unity host and MCP server
|
||||||
|
- [ ] **Documentation Expansion** - Complete tutorials for custom tool creation and API reference
|
||||||
|
|
||||||
|
### 🟡 Medium Priority
|
||||||
|
- [ ] **Custom Tool Creation GUI** - Visual interface for users to create and configure their own MCP tools
|
||||||
|
- [ ] **Advanced Logging System** - Logging with filtering, export, and debugging capabilities
|
||||||
|
|
||||||
|
### 🟢 Low Priority
|
||||||
|
- [ ] **Mobile Platform Support** - Extended toolset for mobile development workflows and platform-specific features
|
||||||
|
- [ ] **Easier Tool Setup**
|
||||||
|
- [ ] **Plugin Marketplace** - Community-driven tool sharing and distribution platform
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><strong>✅ Completed Features<strong></summary>
|
||||||
|
|
||||||
|
- [x] **Shader Generation** - Generate shaders using CGProgram template
|
||||||
|
- [x] **Advanced Script Validation** - Multi-level validation with semantic analysis, namespace/type checking, and Unity best practices (Will need Roslyn Installed, see [Prerequisite](#prerequisites)).
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 🔬 Research & Exploration
|
||||||
|
- [ ] **AI-Powered Asset Generation** - Integration with AI tools for automatic 3D models, textures, and animations
|
||||||
|
- [ ] **Real-time Collaboration** - Live editing sessions between multiple developers *(Currently in progress)*
|
||||||
|
- [ ] **Analytics Dashboard** - Usage analytics, project insights, and performance metrics
|
||||||
|
- [ ] **Voice Commands** - Voice-controlled Unity operations for accessibility
|
||||||
|
- [ ] **AR/VR Tool Integration** - Extended support for immersive development workflows
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -227,7 +307,15 @@ Help make Unity MCP better!
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
Still stuck? [Open an Issue](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgithub.com%2Fjustinpbarnett%2Funity-mcp%2Fissues).
|
Still stuck? [Open an Issue](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgithub.com%2Fjustinpbarnett%2Funity-mcp%2Fissues) or [Join the Discord](https://discord.gg/vhTUxXaqYr)!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contact 👋
|
||||||
|
|
||||||
|
- **justinpbarnett:** [X/Twitter](https://www.google.com/url?sa=E&q=https%3A%2F%2Fx.com%2Fjustinpbarnett)
|
||||||
|
- **scriptwonder**: [Email](mailto:swu85@ur.rochester.edu), [LinkedIn](https://www.linkedin.com/in/shutong-wu-214043172/)
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -235,15 +323,13 @@ Still stuck? [Open an Issue](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgit
|
||||||
|
|
||||||
MIT License. See [LICENSE](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgithub.com%2Fjustinpbarnett%2Funity-mcp%2Fblob%2Fmaster%2FLICENSE) file.
|
MIT License. See [LICENSE](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgithub.com%2Fjustinpbarnett%2Funity-mcp%2Fblob%2Fmaster%2FLICENSE) file.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contact 👋
|
|
||||||
|
|
||||||
- **X/Twitter:** [@justinpbarnett](https://www.google.com/url?sa=E&q=https%3A%2F%2Fx.com%2Fjustinpbarnett)
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Acknowledgments 🙏
|
## Acknowledgments 🙏
|
||||||
|
|
||||||
Thanks to the contributors and the Unity team.
|
Thanks to the contributors and the Unity team.
|
||||||
|
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#justinpbarnett/unity-mcp&Date)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,20 @@ namespace UnityMcpBridge.Editor.Data
|
||||||
configStatus = "Not Configured",
|
configStatus = "Not Configured",
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
|
{
|
||||||
|
name = "Claude Code",
|
||||||
|
windowsConfigPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
".claude.json"
|
||||||
|
),
|
||||||
|
linuxConfigPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
".claude.json"
|
||||||
|
),
|
||||||
|
mcpType = McpTypes.ClaudeCode,
|
||||||
|
configStatus = "Not Configured",
|
||||||
|
},
|
||||||
|
new()
|
||||||
{
|
{
|
||||||
name = "Cursor",
|
name = "Cursor",
|
||||||
windowsConfigPath = Path.Combine(
|
windowsConfigPath = Path.Combine(
|
||||||
|
|
@ -43,6 +57,26 @@ namespace UnityMcpBridge.Editor.Data
|
||||||
mcpType = McpTypes.Cursor,
|
mcpType = McpTypes.Cursor,
|
||||||
configStatus = "Not Configured",
|
configStatus = "Not Configured",
|
||||||
},
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
name = "VSCode GitHub Copilot",
|
||||||
|
windowsConfigPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"Code",
|
||||||
|
"User",
|
||||||
|
"settings.json"
|
||||||
|
),
|
||||||
|
linuxConfigPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"Code",
|
||||||
|
"User",
|
||||||
|
"settings.json"
|
||||||
|
),
|
||||||
|
mcpType = McpTypes.VSCode,
|
||||||
|
configStatus = "Not Configured",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize status enums after construction
|
// Initialize status enums after construction
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles serialization of GameObjects and Components for MCP responses.
|
/// Handles serialization of GameObjects and Components for MCP responses.
|
||||||
/// Includes reflection helpers and caching for performance.
|
/// Includes reflection helpers and caching for performance.
|
||||||
/// </summary> tew
|
/// </summary>
|
||||||
public static class GameObjectSerializer
|
public static class GameObjectSerializer
|
||||||
{
|
{
|
||||||
// --- Data Serialization ---
|
// --- Data Serialization ---
|
||||||
|
|
@ -422,7 +422,7 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
// Catch potential errors during JToken conversion or addition to dictionary
|
// Catch potential errors during JToken conversion or addition to dictionary
|
||||||
// Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping.");
|
Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -505,7 +505,7 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
// Helper to create JToken using the output serializer
|
// Helper to create JToken using the output serializer
|
||||||
private static JToken CreateTokenFromValue(object value, Type type)
|
private static JToken CreateTokenFromValue(object value, Type type)
|
||||||
{
|
{
|
||||||
if (value == null) return JValue.CreateNull();
|
if (value == null) return JValue.CreateNull();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -514,12 +514,12 @@ namespace UnityMcpBridge.Editor.Helpers
|
||||||
}
|
}
|
||||||
catch (JsonSerializationException e)
|
catch (JsonSerializationException e)
|
||||||
{
|
{
|
||||||
// Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field.");
|
Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field.");
|
||||||
return null; // Indicate serialization failure
|
return null; // Indicate serialization failure
|
||||||
}
|
}
|
||||||
catch (Exception e) // Catch other unexpected errors
|
catch (Exception e) // Catch other unexpected errors
|
||||||
{
|
{
|
||||||
// Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field.");
|
Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field.");
|
||||||
return null; // Indicate serialization failure
|
return null; // Indicate serialization failure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace UnityMcpBridge.Editor.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages dynamic port allocation and persistent storage for Unity MCP Bridge
|
||||||
|
/// </summary>
|
||||||
|
public static class PortManager
|
||||||
|
{
|
||||||
|
private const int DefaultPort = 6400;
|
||||||
|
private const int MaxPortAttempts = 100;
|
||||||
|
private const string RegistryFileName = "unity-mcp-port.json";
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class PortConfig
|
||||||
|
{
|
||||||
|
public int unity_port;
|
||||||
|
public string created_date;
|
||||||
|
public string project_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the port to use - either from storage or discover a new one
|
||||||
|
/// Will try stored port first, then fallback to discovering new port
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Port number to use</returns>
|
||||||
|
public static int GetPortWithFallback()
|
||||||
|
{
|
||||||
|
// Try to load stored port first
|
||||||
|
int storedPort = LoadStoredPort();
|
||||||
|
if (storedPort > 0 && IsPortAvailable(storedPort))
|
||||||
|
{
|
||||||
|
Debug.Log($"Using stored port {storedPort}");
|
||||||
|
return storedPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no stored port or stored port is unavailable, find a new one
|
||||||
|
int newPort = FindAvailablePort();
|
||||||
|
SavePort(newPort);
|
||||||
|
return newPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discover and save a new available port (used by Auto-Connect button)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>New available port</returns>
|
||||||
|
public static int DiscoverNewPort()
|
||||||
|
{
|
||||||
|
int newPort = FindAvailablePort();
|
||||||
|
SavePort(newPort);
|
||||||
|
Debug.Log($"Discovered and saved new port: {newPort}");
|
||||||
|
return newPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find an available port starting from the default port
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Available port number</returns>
|
||||||
|
private static int FindAvailablePort()
|
||||||
|
{
|
||||||
|
// Always try default port first
|
||||||
|
if (IsPortAvailable(DefaultPort))
|
||||||
|
{
|
||||||
|
Debug.Log($"Using default port {DefaultPort}");
|
||||||
|
return DefaultPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log($"Default port {DefaultPort} is in use, searching for alternative...");
|
||||||
|
|
||||||
|
// Search for alternatives
|
||||||
|
for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++)
|
||||||
|
{
|
||||||
|
if (IsPortAvailable(port))
|
||||||
|
{
|
||||||
|
Debug.Log($"Found available port {port}");
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a specific port is available
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="port">Port to check</param>
|
||||||
|
/// <returns>True if port is available</returns>
|
||||||
|
public static bool IsPortAvailable(int port)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var testListener = new TcpListener(IPAddress.Loopback, port);
|
||||||
|
testListener.Start();
|
||||||
|
testListener.Stop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save port to persistent storage
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="port">Port to save</param>
|
||||||
|
private static void SavePort(int port)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var portConfig = new PortConfig
|
||||||
|
{
|
||||||
|
unity_port = port,
|
||||||
|
created_date = DateTime.UtcNow.ToString("O"),
|
||||||
|
project_path = Application.dataPath
|
||||||
|
};
|
||||||
|
|
||||||
|
string registryDir = GetRegistryDirectory();
|
||||||
|
Directory.CreateDirectory(registryDir);
|
||||||
|
|
||||||
|
string registryFile = Path.Combine(registryDir, RegistryFileName);
|
||||||
|
string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented);
|
||||||
|
File.WriteAllText(registryFile, json);
|
||||||
|
|
||||||
|
Debug.Log($"Saved port {port} to storage");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Could not save port to storage: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load port from persistent storage
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Stored port number, or 0 if not found</returns>
|
||||||
|
private static int LoadStoredPort()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string registryFile = Path.Combine(GetRegistryDirectory(), RegistryFileName);
|
||||||
|
|
||||||
|
if (!File.Exists(registryFile))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
string json = File.ReadAllText(registryFile);
|
||||||
|
var portConfig = JsonConvert.DeserializeObject<PortConfig>(json);
|
||||||
|
|
||||||
|
return portConfig?.unity_port ?? 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Could not load port from storage: {ex.Message}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the current stored port configuration
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Port configuration if exists, null otherwise</returns>
|
||||||
|
public static PortConfig GetStoredPortConfig()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string registryFile = Path.Combine(GetRegistryDirectory(), RegistryFileName);
|
||||||
|
|
||||||
|
if (!File.Exists(registryFile))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string json = File.ReadAllText(registryFile);
|
||||||
|
return JsonConvert.DeserializeObject<PortConfig>(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Could not load port config: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetRegistryDirectory()
|
||||||
|
{
|
||||||
|
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a1b2c3d4e5f6789012345678901234ab
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -4,6 +4,8 @@ namespace UnityMcpBridge.Editor.Models
|
||||||
{
|
{
|
||||||
ClaudeDesktop,
|
ClaudeDesktop,
|
||||||
Cursor,
|
Cursor,
|
||||||
|
VSCode,
|
||||||
|
ClaudeCode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
{ "HandleManageAsset", ManageAsset.HandleCommand },
|
{ "HandleManageAsset", ManageAsset.HandleCommand },
|
||||||
{ "HandleReadConsole", ReadConsole.HandleCommand },
|
{ "HandleReadConsole", ReadConsole.HandleCommand },
|
||||||
{ "HandleExecuteMenuItem", ExecuteMenuItem.HandleCommand },
|
{ "HandleExecuteMenuItem", ExecuteMenuItem.HandleCommand },
|
||||||
|
{ "HandleManageShader", ManageShader.HandleCommand},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -66,13 +66,15 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static object ExecuteItem(JObject @params)
|
private static object ExecuteItem(JObject @params)
|
||||||
{
|
{
|
||||||
string menuPath = @params["menu_path"]?.ToString();
|
// Try both naming conventions: snake_case and camelCase
|
||||||
|
string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString();
|
||||||
|
|
||||||
// string alias = @params["alias"]?.ToString(); // TODO: Implement alias mapping based on refactor plan requirements.
|
// string alias = @params["alias"]?.ToString(); // TODO: Implement alias mapping based on refactor plan requirements.
|
||||||
// JObject parameters = @params["parameters"] as JObject; // TODO: Investigate parameter passing (often not directly supported by ExecuteMenuItem).
|
// JObject parameters = @params["parameters"] as JObject; // TODO: Investigate parameter passing (often not directly supported by ExecuteMenuItem).
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(menuPath))
|
if (string.IsNullOrWhiteSpace(menuPath))
|
||||||
{
|
{
|
||||||
return Response.Error("Required parameter 'menu_path' is missing or empty.");
|
return Response.Error("Required parameter 'menu_path' or 'menuPath' is missing or empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate against blacklist
|
// Validate against blacklist
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@ using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityMcpBridge.Editor.Helpers; // For Response class
|
using UnityMcpBridge.Editor.Helpers; // For Response class
|
||||||
|
|
||||||
|
#if UNITY_6000_0_OR_NEWER
|
||||||
|
using PhysicsMaterialType = UnityEngine.PhysicsMaterial;
|
||||||
|
using PhysicsMaterialCombine = UnityEngine.PhysicsMaterialCombine;
|
||||||
|
#else
|
||||||
|
using PhysicsMaterialType = UnityEngine.PhysicMaterial;
|
||||||
|
using PhysicsMaterialCombine = UnityEngine.PhysicMaterialCombine;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace UnityMcpBridge.Editor.Tools
|
namespace UnityMcpBridge.Editor.Tools
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -177,6 +185,14 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
AssetDatabase.CreateAsset(mat, fullPath);
|
AssetDatabase.CreateAsset(mat, fullPath);
|
||||||
newAsset = mat;
|
newAsset = mat;
|
||||||
}
|
}
|
||||||
|
else if (lowerAssetType == "physicsmaterial")
|
||||||
|
{
|
||||||
|
PhysicsMaterialType pmat = new PhysicsMaterialType();
|
||||||
|
if (properties != null)
|
||||||
|
ApplyPhysicsMaterialProperties(pmat, properties);
|
||||||
|
AssetDatabase.CreateAsset(pmat, fullPath);
|
||||||
|
newAsset = pmat;
|
||||||
|
}
|
||||||
else if (lowerAssetType == "scriptableobject")
|
else if (lowerAssetType == "scriptableobject")
|
||||||
{
|
{
|
||||||
string scriptClassName = properties?["scriptClass"]?.ToString();
|
string scriptClassName = properties?["scriptClass"]?.ToString();
|
||||||
|
|
@ -891,6 +907,30 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py
|
||||||
|
{
|
||||||
|
string propName = "_Color";
|
||||||
|
try {
|
||||||
|
if (colorArr.Count >= 3)
|
||||||
|
{
|
||||||
|
Color newColor = new Color(
|
||||||
|
colorArr[0].ToObject<float>(),
|
||||||
|
colorArr[1].ToObject<float>(),
|
||||||
|
colorArr[2].ToObject<float>(),
|
||||||
|
colorArr.Count > 3 ? colorArr[3].ToObject<float>() : 1.0f
|
||||||
|
);
|
||||||
|
if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor)
|
||||||
|
{
|
||||||
|
mat.SetColor(propName, newColor);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Debug.LogWarning(
|
||||||
|
$"Error parsing color property '{propName}': {ex.Message}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Example: Set float property
|
// Example: Set float property
|
||||||
if (properties["float"] is JObject floatProps)
|
if (properties["float"] is JObject floatProps)
|
||||||
|
|
@ -948,6 +988,77 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
return modified;
|
return modified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies properties from JObject to a PhysicsMaterial.
|
||||||
|
/// </summary>
|
||||||
|
private static bool ApplyPhysicsMaterialProperties(PhysicsMaterialType pmat, JObject properties)
|
||||||
|
{
|
||||||
|
if (pmat == null || properties == null)
|
||||||
|
return false;
|
||||||
|
bool modified = false;
|
||||||
|
|
||||||
|
// Example: Set dynamic friction
|
||||||
|
if (properties["dynamicFriction"]?.Type == JTokenType.Float)
|
||||||
|
{
|
||||||
|
float dynamicFriction = properties["dynamicFriction"].ToObject<float>();
|
||||||
|
pmat.dynamicFriction = dynamicFriction;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Set static friction
|
||||||
|
if (properties["staticFriction"]?.Type == JTokenType.Float)
|
||||||
|
{
|
||||||
|
float staticFriction = properties["staticFriction"].ToObject<float>();
|
||||||
|
pmat.staticFriction = staticFriction;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Set bounciness
|
||||||
|
if (properties["bounciness"]?.Type == JTokenType.Float)
|
||||||
|
{
|
||||||
|
float bounciness = properties["bounciness"].ToObject<float>();
|
||||||
|
pmat.bounciness = bounciness;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> averageList = new List<String> { "ave", "Ave", "average", "Average" };
|
||||||
|
List<String> multiplyList = new List<String> { "mul", "Mul", "mult", "Mult", "multiply", "Multiply" };
|
||||||
|
List<String> minimumList = new List<String> { "min", "Min", "minimum", "Minimum" };
|
||||||
|
List<String> maximumList = new List<String> { "max", "Max", "maximum", "Maximum" };
|
||||||
|
|
||||||
|
// Example: Set friction combine
|
||||||
|
if (properties["frictionCombine"]?.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
string frictionCombine = properties["frictionCombine"].ToString();
|
||||||
|
if (averageList.Contains(frictionCombine))
|
||||||
|
pmat.frictionCombine = PhysicsMaterialCombine.Average;
|
||||||
|
else if (multiplyList.Contains(frictionCombine))
|
||||||
|
pmat.frictionCombine = PhysicsMaterialCombine.Multiply;
|
||||||
|
else if (minimumList.Contains(frictionCombine))
|
||||||
|
pmat.frictionCombine = PhysicsMaterialCombine.Minimum;
|
||||||
|
else if (maximumList.Contains(frictionCombine))
|
||||||
|
pmat.frictionCombine = PhysicsMaterialCombine.Maximum;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Set bounce combine
|
||||||
|
if (properties["bounceCombine"]?.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
string bounceCombine = properties["bounceCombine"].ToString();
|
||||||
|
if (averageList.Contains(bounceCombine))
|
||||||
|
pmat.bounceCombine = PhysicsMaterialCombine.Average;
|
||||||
|
else if (multiplyList.Contains(bounceCombine))
|
||||||
|
pmat.bounceCombine = PhysicsMaterialCombine.Multiply;
|
||||||
|
else if (minimumList.Contains(bounceCombine))
|
||||||
|
pmat.bounceCombine = PhysicsMaterialCombine.Minimum;
|
||||||
|
else if (maximumList.Contains(bounceCombine))
|
||||||
|
pmat.bounceCombine = PhysicsMaterialCombine.Maximum;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return modified;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generic helper to set properties on any UnityEngine.Object using reflection.
|
/// Generic helper to set properties on any UnityEngine.Object using reflection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ using UnityEditor.SceneManagement;
|
||||||
using UnityEditorInternal;
|
using UnityEditorInternal;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.SceneManagement;
|
using UnityEngine.SceneManagement;
|
||||||
using UnityMcpBridge.Editor.Helpers; // For Response class AND GameObjectSerializer
|
using UnityMcpBridge.Editor.Helpers; // For Response class
|
||||||
using UnityMcpBridge.Runtime.Serialization; // <<< Keep for Converters access? Might not be needed here directly
|
using UnityMcpBridge.Runtime.Serialization;
|
||||||
|
|
||||||
namespace UnityMcpBridge.Editor.Tools
|
namespace UnityMcpBridge.Editor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -23,10 +23,6 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
|
|
||||||
public static object HandleCommand(JObject @params)
|
public static object HandleCommand(JObject @params)
|
||||||
{
|
{
|
||||||
// --- DEBUG --- Log the raw parameter value ---
|
|
||||||
// JToken rawIncludeFlag = @params["includeNonPublicSerialized"];
|
|
||||||
// Debug.Log($"[HandleCommand Debug] Raw includeNonPublicSerialized parameter: Type={rawIncludeFlag?.Type.ToString() ?? "Null"}, Value={rawIncludeFlag?.ToString() ?? "N/A"}");
|
|
||||||
// --- END DEBUG ---
|
|
||||||
|
|
||||||
string action = @params["action"]?.ToString().ToLower();
|
string action = @params["action"]?.ToString().ToLower();
|
||||||
if (string.IsNullOrEmpty(action))
|
if (string.IsNullOrEmpty(action))
|
||||||
|
|
@ -219,17 +215,22 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
$"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' does not end with .prefab. Assuming it's missing and appending."
|
$"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' does not end with .prefab. Assuming it's missing and appending."
|
||||||
);
|
);
|
||||||
prefabPath += ".prefab";
|
prefabPath += ".prefab";
|
||||||
|
// Note: This path might still not exist, AssetDatabase.LoadAssetAtPath will handle that.
|
||||||
}
|
}
|
||||||
|
// The logic above now handles finding or assuming the .prefab extension.
|
||||||
|
|
||||||
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
||||||
if (prefabAsset != null)
|
if (prefabAsset != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Instantiate the prefab, initially place it at the root
|
||||||
|
// Parent will be set later if specified
|
||||||
newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject;
|
newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject;
|
||||||
|
|
||||||
if (newGo == null)
|
if (newGo == null)
|
||||||
{
|
{
|
||||||
|
// This might happen if the asset exists but isn't a valid GameObject prefab somehow
|
||||||
Debug.LogError(
|
Debug.LogError(
|
||||||
$"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject."
|
$"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject."
|
||||||
);
|
);
|
||||||
|
|
@ -237,12 +238,12 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
$"Failed to instantiate prefab at '{prefabPath}'."
|
$"Failed to instantiate prefab at '{prefabPath}'."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Name the instance based on the 'name' parameter, not the prefab's default name
|
||||||
if (!string.IsNullOrEmpty(name))
|
if (!string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
newGo.name = name;
|
newGo.name = name;
|
||||||
}
|
}
|
||||||
|
// Register Undo for prefab instantiation
|
||||||
Undo.RegisterCreatedObjectUndo(
|
Undo.RegisterCreatedObjectUndo(
|
||||||
newGo,
|
newGo,
|
||||||
$"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'"
|
$"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'"
|
||||||
|
|
@ -260,9 +261,12 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Only return error if prefabPath was specified but not found.
|
||||||
|
// If prefabPath was empty/null, we proceed to create primitive/empty.
|
||||||
Debug.LogWarning(
|
Debug.LogWarning(
|
||||||
$"[ManageGameObject.Create] Prefab asset not found at path: '{prefabPath}'. Will proceed to create new object if specified."
|
$"[ManageGameObject.Create] Prefab asset not found at path: '{prefabPath}'. Will proceed to create new object if specified."
|
||||||
);
|
);
|
||||||
|
// Do not return error here, allow fallback to primitive/empty creation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,6 +281,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
PrimitiveType type = (PrimitiveType)
|
PrimitiveType type = (PrimitiveType)
|
||||||
Enum.Parse(typeof(PrimitiveType), primitiveType, true);
|
Enum.Parse(typeof(PrimitiveType), primitiveType, true);
|
||||||
newGo = GameObject.CreatePrimitive(type);
|
newGo = GameObject.CreatePrimitive(type);
|
||||||
|
// Set name *after* creation for primitives
|
||||||
if (!string.IsNullOrEmpty(name))
|
if (!string.IsNullOrEmpty(name))
|
||||||
newGo.name = name;
|
newGo.name = name;
|
||||||
else
|
else
|
||||||
|
|
@ -309,18 +314,21 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
newGo = new GameObject(name);
|
newGo = new GameObject(name);
|
||||||
createdNewObject = true;
|
createdNewObject = true;
|
||||||
}
|
}
|
||||||
|
// Record creation for Undo *only* if we created a new object
|
||||||
if (createdNewObject)
|
if (createdNewObject)
|
||||||
{
|
{
|
||||||
Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'");
|
Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// --- Common Setup (Parent, Transform, Tag, Components) - Applied AFTER object exists ---
|
||||||
if (newGo == null)
|
if (newGo == null)
|
||||||
{
|
{
|
||||||
|
// Should theoretically not happen if logic above is correct, but safety check.
|
||||||
return Response.Error("Failed to create or instantiate the GameObject.");
|
return Response.Error("Failed to create or instantiate the GameObject.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record potential changes to the existing prefab instance or the new GO
|
||||||
|
// Record transform separately in case parent changes affect it
|
||||||
Undo.RecordObject(newGo.transform, "Set GameObject Transform");
|
Undo.RecordObject(newGo.transform, "Set GameObject Transform");
|
||||||
Undo.RecordObject(newGo, "Set GameObject Properties");
|
Undo.RecordObject(newGo, "Set GameObject Properties");
|
||||||
|
|
||||||
|
|
@ -352,6 +360,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
// Set Tag (added for create action)
|
// Set Tag (added for create action)
|
||||||
if (!string.IsNullOrEmpty(tag))
|
if (!string.IsNullOrEmpty(tag))
|
||||||
{
|
{
|
||||||
|
// Similar logic as in ModifyGameObject for setting/creating tags
|
||||||
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
|
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -448,13 +457,16 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
if (createdNewObject && saveAsPrefab)
|
if (createdNewObject && saveAsPrefab)
|
||||||
{
|
{
|
||||||
string finalPrefabPath = prefabPath; // Use a separate variable for saving path
|
string finalPrefabPath = prefabPath; // Use a separate variable for saving path
|
||||||
|
// This check should now happen *before* attempting to save
|
||||||
if (string.IsNullOrEmpty(finalPrefabPath))
|
if (string.IsNullOrEmpty(finalPrefabPath))
|
||||||
{
|
{
|
||||||
|
// Clean up the created object before returning error
|
||||||
UnityEngine.Object.DestroyImmediate(newGo);
|
UnityEngine.Object.DestroyImmediate(newGo);
|
||||||
return Response.Error(
|
return Response.Error(
|
||||||
"'prefabPath' is required when 'saveAsPrefab' is true and creating a new object."
|
"'prefabPath' is required when 'saveAsPrefab' is true and creating a new object."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Ensure the *saving* path ends with .prefab
|
||||||
if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
Debug.Log(
|
Debug.Log(
|
||||||
|
|
@ -465,6 +477,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Ensure directory exists using the final saving path
|
||||||
string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath);
|
string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath);
|
||||||
if (
|
if (
|
||||||
!string.IsNullOrEmpty(directoryPath)
|
!string.IsNullOrEmpty(directoryPath)
|
||||||
|
|
@ -477,7 +490,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
$"[ManageGameObject.Create] Created directory for prefab: {directoryPath}"
|
$"[ManageGameObject.Create] Created directory for prefab: {directoryPath}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Use SaveAsPrefabAssetAndConnect with the final saving path
|
||||||
finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(
|
finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(
|
||||||
newGo,
|
newGo,
|
||||||
finalPrefabPath,
|
finalPrefabPath,
|
||||||
|
|
@ -486,6 +499,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
|
|
||||||
if (finalInstance == null)
|
if (finalInstance == null)
|
||||||
{
|
{
|
||||||
|
// Destroy the original if saving failed somehow (shouldn't usually happen if path is valid)
|
||||||
UnityEngine.Object.DestroyImmediate(newGo);
|
UnityEngine.Object.DestroyImmediate(newGo);
|
||||||
return Response.Error(
|
return Response.Error(
|
||||||
$"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions."
|
$"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions."
|
||||||
|
|
@ -494,16 +508,21 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
Debug.Log(
|
Debug.Log(
|
||||||
$"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected."
|
$"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected."
|
||||||
);
|
);
|
||||||
|
// Mark the new prefab asset as dirty? Not usually necessary, SaveAsPrefabAsset handles it.
|
||||||
|
// EditorUtility.SetDirty(finalInstance); // Instance is handled by SaveAsPrefabAssetAndConnect
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
// Clean up the instance if prefab saving fails
|
||||||
UnityEngine.Object.DestroyImmediate(newGo); // Destroy the original attempt
|
UnityEngine.Object.DestroyImmediate(newGo); // Destroy the original attempt
|
||||||
return Response.Error($"Error saving prefab '{finalPrefabPath}': {e.Message}");
|
return Response.Error($"Error saving prefab '{finalPrefabPath}': {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Select the instance in the scene (either prefab instance or newly created/saved one)
|
||||||
Selection.activeGameObject = finalInstance;
|
Selection.activeGameObject = finalInstance;
|
||||||
|
|
||||||
|
// Determine appropriate success message using the potentially updated or original path
|
||||||
string messagePrefabPath =
|
string messagePrefabPath =
|
||||||
finalInstance == null
|
finalInstance == null
|
||||||
? originalPrefabPath
|
? originalPrefabPath
|
||||||
|
|
@ -529,6 +548,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the new serializer helper
|
// Use the new serializer helper
|
||||||
|
//return Response.Success(successMessage, GetGameObjectData(finalInstance));
|
||||||
return Response.Success(successMessage, Helpers.GameObjectSerializer.GetGameObjectData(finalInstance));
|
return Response.Success(successMessage, Helpers.GameObjectSerializer.GetGameObjectData(finalInstance));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -546,6 +566,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record state for Undo *before* modifications
|
||||||
Undo.RecordObject(targetGo.transform, "Modify GameObject Transform");
|
Undo.RecordObject(targetGo.transform, "Modify GameObject Transform");
|
||||||
Undo.RecordObject(targetGo, "Modify GameObject Properties");
|
Undo.RecordObject(targetGo, "Modify GameObject Properties");
|
||||||
|
|
||||||
|
|
@ -564,6 +585,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
if (parentToken != null)
|
if (parentToken != null)
|
||||||
{
|
{
|
||||||
GameObject newParentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path");
|
GameObject newParentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path");
|
||||||
|
// Check for hierarchy loops
|
||||||
if (
|
if (
|
||||||
newParentGo == null
|
newParentGo == null
|
||||||
&& !(
|
&& !(
|
||||||
|
|
@ -600,8 +622,11 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
|
|
||||||
// Change Tag (using consolidated 'tag' parameter)
|
// Change Tag (using consolidated 'tag' parameter)
|
||||||
string tag = @params["tag"]?.ToString();
|
string tag = @params["tag"]?.ToString();
|
||||||
|
// Only attempt to change tag if a non-null tag is provided and it's different from the current one.
|
||||||
|
// Allow setting an empty string to remove the tag (Unity uses "Untagged").
|
||||||
if (tag != null && targetGo.tag != tag)
|
if (tag != null && targetGo.tag != tag)
|
||||||
{
|
{
|
||||||
|
// Ensure the tag is not empty, if empty, it means "Untagged" implicitly
|
||||||
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
|
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -610,6 +635,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
catch (UnityException ex)
|
catch (UnityException ex)
|
||||||
{
|
{
|
||||||
|
// Check if the error is specifically because the tag doesn't exist
|
||||||
if (ex.Message.Contains("is not defined"))
|
if (ex.Message.Contains("is not defined"))
|
||||||
{
|
{
|
||||||
Debug.LogWarning(
|
Debug.LogWarning(
|
||||||
|
|
@ -617,7 +643,12 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
);
|
);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Attempt to create the tag using internal utility
|
||||||
InternalEditorUtility.AddTag(tagToSet);
|
InternalEditorUtility.AddTag(tagToSet);
|
||||||
|
// Wait a frame maybe? Not strictly necessary but sometimes helps editor updates.
|
||||||
|
// yield return null; // Cannot yield here, editor script limitation
|
||||||
|
|
||||||
|
// Retry setting the tag immediately after creation
|
||||||
targetGo.tag = tagToSet;
|
targetGo.tag = tagToSet;
|
||||||
modified = true;
|
modified = true;
|
||||||
Debug.Log(
|
Debug.Log(
|
||||||
|
|
@ -626,6 +657,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
catch (Exception innerEx)
|
catch (Exception innerEx)
|
||||||
{
|
{
|
||||||
|
// Handle failure during tag creation or the second assignment attempt
|
||||||
Debug.LogError(
|
Debug.LogError(
|
||||||
$"[ManageGameObject] Failed to create or assign tag '{tagToSet}' after attempting creation: {innerEx.Message}"
|
$"[ManageGameObject] Failed to create or assign tag '{tagToSet}' after attempting creation: {innerEx.Message}"
|
||||||
);
|
);
|
||||||
|
|
@ -636,6 +668,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// If the exception was for a different reason, return the original error
|
||||||
return Response.Error($"Failed to set tag to '{tagToSet}': {ex.Message}.");
|
return Response.Error($"Failed to set tag to '{tagToSet}': {ex.Message}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -681,12 +714,14 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Component Modifications ---
|
// --- Component Modifications ---
|
||||||
|
// Note: These might need more specific Undo recording per component
|
||||||
|
|
||||||
// Remove Components
|
// Remove Components
|
||||||
if (@params["componentsToRemove"] is JArray componentsToRemoveArray)
|
if (@params["componentsToRemove"] is JArray componentsToRemoveArray)
|
||||||
{
|
{
|
||||||
foreach (var compToken in componentsToRemoveArray)
|
foreach (var compToken in componentsToRemoveArray)
|
||||||
{
|
{
|
||||||
|
// ... (parsing logic as in CreateGameObject) ...
|
||||||
string typeName = compToken.ToString();
|
string typeName = compToken.ToString();
|
||||||
if (!string.IsNullOrEmpty(typeName))
|
if (!string.IsNullOrEmpty(typeName))
|
||||||
{
|
{
|
||||||
|
|
@ -746,7 +781,11 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
|
|
||||||
if (!modified)
|
if (!modified)
|
||||||
{
|
{
|
||||||
// Use the new serializer helper
|
// Use the new serializer helper
|
||||||
|
// return Response.Success(
|
||||||
|
// $"No modifications applied to GameObject '{targetGo.name}'.",
|
||||||
|
// GetGameObjectData(targetGo));
|
||||||
|
|
||||||
return Response.Success(
|
return Response.Success(
|
||||||
$"No modifications applied to GameObject '{targetGo.name}'.",
|
$"No modifications applied to GameObject '{targetGo.name}'.",
|
||||||
Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
|
Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
|
||||||
|
|
@ -754,11 +793,15 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
|
|
||||||
EditorUtility.SetDirty(targetGo); // Mark scene as dirty
|
EditorUtility.SetDirty(targetGo); // Mark scene as dirty
|
||||||
// Use the new serializer helper
|
// Use the new serializer helper
|
||||||
return Response.Success(
|
return Response.Success(
|
||||||
$"GameObject '{targetGo.name}' modified successfully.",
|
$"GameObject '{targetGo.name}' modified successfully.",
|
||||||
Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
|
Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
|
||||||
);
|
);
|
||||||
|
// return Response.Success(
|
||||||
|
// $"GameObject '{targetGo.name}' modified successfully.",
|
||||||
|
// GetGameObjectData(targetGo));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object DeleteGameObject(JToken targetToken, string searchMethod)
|
private static object DeleteGameObject(JToken targetToken, string searchMethod)
|
||||||
|
|
@ -821,11 +864,12 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the new serializer helper
|
// Use the new serializer helper
|
||||||
|
//var results = foundObjects.Select(go => GetGameObjectData(go)).ToList();
|
||||||
var results = foundObjects.Select(go => Helpers.GameObjectSerializer.GetGameObjectData(go)).ToList();
|
var results = foundObjects.Select(go => Helpers.GameObjectSerializer.GetGameObjectData(go)).ToList();
|
||||||
return Response.Success($"Found {results.Count} GameObject(s).", results);
|
return Response.Success($"Found {results.Count} GameObject(s).", results);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object GetComponentsFromTarget(string target, string searchMethod, bool includeNonPublicSerialized)
|
private static object GetComponentsFromTarget(string target, string searchMethod, bool includeNonPublicSerialized = true)
|
||||||
{
|
{
|
||||||
GameObject targetGo = FindObjectInternal(target, searchMethod);
|
GameObject targetGo = FindObjectInternal(target, searchMethod);
|
||||||
if (targetGo == null)
|
if (targetGo == null)
|
||||||
|
|
@ -1443,6 +1487,8 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
Debug.LogWarning(
|
Debug.LogWarning(
|
||||||
$"[ManageGameObject] Could not set property '{propName}' on component '{compName}' ('{targetComponent.GetType().Name}'). Property might not exist, be read-only, or type mismatch."
|
$"[ManageGameObject] Could not set property '{propName}' on component '{compName}' ('{targetComponent.GetType().Name}'). Property might not exist, be read-only, or type mismatch."
|
||||||
);
|
);
|
||||||
|
// Optionally return an error here instead of just logging
|
||||||
|
// return Response.Error($"Could not set property '{propName}' on component '{compName}'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -1450,6 +1496,8 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
Debug.LogError(
|
Debug.LogError(
|
||||||
$"[ManageGameObject] Error setting property '{propName}' on '{compName}': {e.Message}"
|
$"[ManageGameObject] Error setting property '{propName}' on '{compName}': {e.Message}"
|
||||||
);
|
);
|
||||||
|
// Optionally return an error here
|
||||||
|
// return Response.Error($"Error setting property '{propName}' on '{compName}': {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EditorUtility.SetDirty(targetComponent);
|
EditorUtility.SetDirty(targetComponent);
|
||||||
|
|
@ -1488,6 +1536,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Handle special case for materials with dot notation (material.property)
|
// Handle special case for materials with dot notation (material.property)
|
||||||
|
// Examples: material.color, sharedMaterial.color, materials[0].color
|
||||||
if (memberName.Contains('.') || memberName.Contains('['))
|
if (memberName.Contains('.') || memberName.Contains('['))
|
||||||
{
|
{
|
||||||
// Pass the inputSerializer down for nested conversions
|
// Pass the inputSerializer down for nested conversions
|
||||||
|
|
@ -1538,7 +1587,8 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets a nested property using dot notation (e.g., "material.color") or array access (e.g., "materials[0]")
|
/// Sets a nested property using dot notation (e.g., "material.color") or array access (e.g., "materials[0]")
|
||||||
/// </summary>
|
/// </summary>
|
||||||
// Pass the input serializer for conversions
|
// Pass the input serializer for conversions
|
||||||
|
//Using the serializer helper
|
||||||
private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer)
|
private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -1560,6 +1610,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
bool isArray = false;
|
bool isArray = false;
|
||||||
int arrayIndex = -1;
|
int arrayIndex = -1;
|
||||||
|
|
||||||
|
// Check if this part contains array indexing
|
||||||
if (part.Contains("["))
|
if (part.Contains("["))
|
||||||
{
|
{
|
||||||
int startBracket = part.IndexOf('[');
|
int startBracket = part.IndexOf('[');
|
||||||
|
|
@ -1577,7 +1628,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Get the property/field
|
||||||
PropertyInfo propInfo = currentType.GetProperty(part, flags);
|
PropertyInfo propInfo = currentType.GetProperty(part, flags);
|
||||||
FieldInfo fieldInfo = null;
|
FieldInfo fieldInfo = null;
|
||||||
if (propInfo == null)
|
if (propInfo == null)
|
||||||
|
|
@ -1592,11 +1643,12 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the value
|
||||||
currentObject =
|
currentObject =
|
||||||
propInfo != null
|
propInfo != null
|
||||||
? propInfo.GetValue(currentObject)
|
? propInfo.GetValue(currentObject)
|
||||||
: fieldInfo.GetValue(currentObject);
|
: fieldInfo.GetValue(currentObject);
|
||||||
|
//Need to stop if current property is null
|
||||||
if (currentObject == null)
|
if (currentObject == null)
|
||||||
{
|
{
|
||||||
Debug.LogWarning(
|
Debug.LogWarning(
|
||||||
|
|
@ -1604,7 +1656,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// If this part was an array or list, access the specific index
|
||||||
if (isArray)
|
if (isArray)
|
||||||
{
|
{
|
||||||
if (currentObject is Material[])
|
if (currentObject is Material[])
|
||||||
|
|
@ -1653,32 +1705,32 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
{
|
{
|
||||||
// Try converting to known types that SetColor/SetVector accept
|
// Try converting to known types that SetColor/SetVector accept
|
||||||
if (jArray.Count == 4) {
|
if (jArray.Count == 4) {
|
||||||
try { Color color = value.ToObject<Color>(inputSerializer); material.SetColor(finalPart, color); return true; } catch {}
|
try { Color color = value.ToObject<Color>(inputSerializer); material.SetColor(finalPart, color); return true; } catch { }
|
||||||
try { Vector4 vec = value.ToObject<Vector4>(inputSerializer); material.SetVector(finalPart, vec); return true; } catch {}
|
try { Vector4 vec = value.ToObject<Vector4>(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { }
|
||||||
} else if (jArray.Count == 3) {
|
} else if (jArray.Count == 3) {
|
||||||
try { Color color = value.ToObject<Color>(inputSerializer); material.SetColor(finalPart, color); return true; } catch {} // ToObject handles conversion to Color
|
try { Color color = value.ToObject<Color>(inputSerializer); material.SetColor(finalPart, color); return true; } catch { } // ToObject handles conversion to Color
|
||||||
} else if (jArray.Count == 2) {
|
} else if (jArray.Count == 2) {
|
||||||
try { Vector2 vec = value.ToObject<Vector2>(inputSerializer); material.SetVector(finalPart, vec); return true; } catch {}
|
try { Vector2 vec = value.ToObject<Vector2>(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer)
|
else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer)
|
||||||
{
|
{
|
||||||
try { material.SetFloat(finalPart, value.ToObject<float>(inputSerializer)); return true; } catch {}
|
try { material.SetFloat(finalPart, value.ToObject<float>(inputSerializer)); return true; } catch { }
|
||||||
}
|
}
|
||||||
else if (value.Type == JTokenType.Boolean)
|
else if (value.Type == JTokenType.Boolean)
|
||||||
{
|
{
|
||||||
try { material.SetFloat(finalPart, value.ToObject<bool>(inputSerializer) ? 1f : 0f); return true; } catch {}
|
try { material.SetFloat(finalPart, value.ToObject<bool>(inputSerializer) ? 1f : 0f); return true; } catch { }
|
||||||
}
|
}
|
||||||
else if (value.Type == JTokenType.String)
|
else if (value.Type == JTokenType.String)
|
||||||
{
|
{
|
||||||
// Try converting to Texture using the serializer/converter
|
// Try converting to Texture using the serializer/converter
|
||||||
try {
|
try {
|
||||||
Texture texture = value.ToObject<Texture>(inputSerializer);
|
Texture texture = value.ToObject<Texture>(inputSerializer);
|
||||||
if (texture != null) {
|
if (texture != null) {
|
||||||
material.SetTexture(finalPart, texture);
|
material.SetTexture(finalPart, texture);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.LogWarning(
|
Debug.LogWarning(
|
||||||
|
|
@ -1698,7 +1750,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
finalPropInfo.SetValue(currentObject, convertedValue);
|
finalPropInfo.SetValue(currentObject, convertedValue);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Debug.LogWarning($"[SetNestedProperty] Final conversion failed for property '{finalPart}' (Type: {finalPropInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}");
|
Debug.LogWarning($"[SetNestedProperty] Final conversion failed for property '{finalPart}' (Type: {finalPropInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1707,16 +1759,16 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags);
|
FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags);
|
||||||
if (finalFieldInfo != null)
|
if (finalFieldInfo != null)
|
||||||
{
|
{
|
||||||
// Use the inputSerializer for conversion
|
// Use the inputSerializer for conversion
|
||||||
object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType, inputSerializer);
|
object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType, inputSerializer);
|
||||||
if (convertedValue != null || value.Type == JTokenType.Null)
|
if (convertedValue != null || value.Type == JTokenType.Null)
|
||||||
{
|
{
|
||||||
finalFieldInfo.SetValue(currentObject, convertedValue);
|
finalFieldInfo.SetValue(currentObject, convertedValue);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Debug.LogWarning($"[SetNestedProperty] Final conversion failed for field '{finalPart}' (Type: {finalFieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}");
|
Debug.LogWarning($"[SetNestedProperty] Final conversion failed for field '{finalPart}' (Type: {finalFieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1742,6 +1794,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string[] SplitPropertyPath(string path)
|
private static string[] SplitPropertyPath(string path)
|
||||||
{
|
{
|
||||||
|
// Handle complex paths with both dots and array indexers
|
||||||
List<string> parts = new List<string>();
|
List<string> parts = new List<string>();
|
||||||
int startIndex = 0;
|
int startIndex = 0;
|
||||||
bool inBrackets = false;
|
bool inBrackets = false;
|
||||||
|
|
@ -1760,6 +1813,7 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
else if (c == '.' && !inBrackets)
|
else if (c == '.' && !inBrackets)
|
||||||
{
|
{
|
||||||
|
// Found a dot separator outside of brackets
|
||||||
parts.Add(path.Substring(startIndex, i - startIndex));
|
parts.Add(path.Substring(startIndex, i - startIndex));
|
||||||
startIndex = i + 1;
|
startIndex = i + 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,43 @@ using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityMcpBridge.Editor.Helpers;
|
using UnityMcpBridge.Editor.Helpers;
|
||||||
|
|
||||||
|
#if USE_ROSLYN
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
using UnityEditor.Compilation;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
namespace UnityMcpBridge.Editor.Tools
|
namespace UnityMcpBridge.Editor.Tools
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles CRUD operations for C# scripts within the Unity project.
|
/// Handles CRUD operations for C# scripts within the Unity project.
|
||||||
|
///
|
||||||
|
/// ROSLYN INSTALLATION GUIDE:
|
||||||
|
/// To enable advanced syntax validation with Roslyn compiler services:
|
||||||
|
///
|
||||||
|
/// 1. Install Microsoft.CodeAnalysis.CSharp NuGet package:
|
||||||
|
/// - Open Package Manager in Unity
|
||||||
|
/// - Follow the instruction on https://github.com/GlitchEnzo/NuGetForUnity
|
||||||
|
///
|
||||||
|
/// 2. Open NuGet Package Manager and Install Microsoft.CodeAnalysis.CSharp:
|
||||||
|
///
|
||||||
|
/// 3. Alternative: Manual DLL installation:
|
||||||
|
/// - Download Microsoft.CodeAnalysis.CSharp.dll and dependencies
|
||||||
|
/// - Place in Assets/Plugins/ folder
|
||||||
|
/// - Ensure .NET compatibility settings are correct
|
||||||
|
///
|
||||||
|
/// 4. Define USE_ROSLYN symbol:
|
||||||
|
/// - Go to Player Settings > Scripting Define Symbols
|
||||||
|
/// - Add "USE_ROSLYN" to enable Roslyn-based validation
|
||||||
|
///
|
||||||
|
/// 5. Restart Unity after installation
|
||||||
|
///
|
||||||
|
/// Note: Without Roslyn, the system falls back to basic structural validation.
|
||||||
|
/// Roslyn provides full C# compiler diagnostics with line numbers and detailed error messages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ManageScript
|
public static class ManageScript
|
||||||
{
|
{
|
||||||
|
|
@ -168,12 +201,18 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
contents = GenerateDefaultScriptContent(name, scriptType, namespaceName);
|
contents = GenerateDefaultScriptContent(name, scriptType, namespaceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate syntax (basic check)
|
// Validate syntax with detailed error reporting using GUI setting
|
||||||
if (!ValidateScriptSyntax(contents))
|
ValidationLevel validationLevel = GetValidationLevelFromGUI();
|
||||||
|
bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors);
|
||||||
|
if (!isValid)
|
||||||
{
|
{
|
||||||
// Optionally return a specific error or warning about syntax
|
string errorMessage = "Script validation failed:\n" + string.Join("\n", validationErrors);
|
||||||
// return Response.Error("Provided script content has potential syntax errors.");
|
return Response.Error(errorMessage);
|
||||||
Debug.LogWarning($"Potential syntax error in script being created: {name}");
|
}
|
||||||
|
else if (validationErrors != null && validationErrors.Length > 0)
|
||||||
|
{
|
||||||
|
// Log warnings but don't block creation
|
||||||
|
Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors));
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -243,11 +282,18 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
return Response.Error("Content is required for the 'update' action.");
|
return Response.Error("Content is required for the 'update' action.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate syntax (basic check)
|
// Validate syntax with detailed error reporting using GUI setting
|
||||||
if (!ValidateScriptSyntax(contents))
|
ValidationLevel validationLevel = GetValidationLevelFromGUI();
|
||||||
|
bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors);
|
||||||
|
if (!isValid)
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"Potential syntax error in script being updated: {name}");
|
string errorMessage = "Script validation failed:\n" + string.Join("\n", validationErrors);
|
||||||
// Consider if this should be a hard error or just a warning
|
return Response.Error(errorMessage);
|
||||||
|
}
|
||||||
|
else if (validationErrors != null && validationErrors.Length > 0)
|
||||||
|
{
|
||||||
|
// Log warnings but don't block update
|
||||||
|
Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors));
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -361,27 +407,624 @@ namespace UnityMcpBridge.Editor.Tools
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs a very basic syntax validation (checks for balanced braces).
|
/// Gets the validation level from the GUI settings
|
||||||
/// TODO: Implement more robust syntax checking if possible.
|
/// </summary>
|
||||||
|
private static ValidationLevel GetValidationLevelFromGUI()
|
||||||
|
{
|
||||||
|
string savedLevel = EditorPrefs.GetString("UnityMCP_ScriptValidationLevel", "standard");
|
||||||
|
return savedLevel.ToLower() switch
|
||||||
|
{
|
||||||
|
"basic" => ValidationLevel.Basic,
|
||||||
|
"standard" => ValidationLevel.Standard,
|
||||||
|
"comprehensive" => ValidationLevel.Comprehensive,
|
||||||
|
"strict" => ValidationLevel.Strict,
|
||||||
|
_ => ValidationLevel.Standard // Default fallback
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates C# script syntax using multiple validation layers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool ValidateScriptSyntax(string contents)
|
private static bool ValidateScriptSyntax(string contents)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(contents))
|
return ValidateScriptSyntax(contents, ValidationLevel.Standard, out _);
|
||||||
return true; // Empty is technically valid?
|
}
|
||||||
|
|
||||||
int braceBalance = 0;
|
/// <summary>
|
||||||
foreach (char c in contents)
|
/// Advanced syntax validation with detailed diagnostics and configurable strictness.
|
||||||
|
/// </summary>
|
||||||
|
private static bool ValidateScriptSyntax(string contents, ValidationLevel level, out string[] errors)
|
||||||
|
{
|
||||||
|
var errorList = new System.Collections.Generic.List<string>();
|
||||||
|
errors = null;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(contents))
|
||||||
{
|
{
|
||||||
if (c == '{')
|
return true; // Empty content is valid
|
||||||
braceBalance++;
|
|
||||||
else if (c == '}')
|
|
||||||
braceBalance--;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return braceBalance == 0;
|
// Basic structural validation
|
||||||
// This is extremely basic. A real C# parser/compiler check would be ideal
|
if (!ValidateBasicStructure(contents, errorList))
|
||||||
// but is complex to implement directly here.
|
{
|
||||||
|
errors = errorList.ToArray();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if USE_ROSLYN
|
||||||
|
// Advanced Roslyn-based validation
|
||||||
|
if (!ValidateScriptSyntaxRoslyn(contents, level, errorList))
|
||||||
|
{
|
||||||
|
errors = errorList.ToArray();
|
||||||
|
return level != ValidationLevel.Standard; //TODO: Allow standard to run roslyn right now, might formalize it in the future
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Unity-specific validation
|
||||||
|
if (level >= ValidationLevel.Standard)
|
||||||
|
{
|
||||||
|
ValidateScriptSyntaxUnity(contents, errorList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic analysis for common issues
|
||||||
|
if (level >= ValidationLevel.Comprehensive)
|
||||||
|
{
|
||||||
|
ValidateSemanticRules(contents, errorList);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if USE_ROSLYN
|
||||||
|
// Full semantic compilation validation for Strict level
|
||||||
|
if (level == ValidationLevel.Strict)
|
||||||
|
{
|
||||||
|
if (!ValidateScriptSemantics(contents, errorList))
|
||||||
|
{
|
||||||
|
errors = errorList.ToArray();
|
||||||
|
return false; // Strict level fails on any semantic errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
errors = errorList.ToArray();
|
||||||
|
return errorList.Count == 0 || (level != ValidationLevel.Strict && !errorList.Any(e => e.StartsWith("ERROR:")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validation strictness levels
|
||||||
|
/// </summary>
|
||||||
|
private enum ValidationLevel
|
||||||
|
{
|
||||||
|
Basic, // Only syntax errors
|
||||||
|
Standard, // Syntax + Unity best practices
|
||||||
|
Comprehensive, // All checks + semantic analysis
|
||||||
|
Strict // Treat all issues as errors
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates basic code structure (braces, quotes, comments)
|
||||||
|
/// </summary>
|
||||||
|
private static bool ValidateBasicStructure(string contents, System.Collections.Generic.List<string> errors)
|
||||||
|
{
|
||||||
|
bool isValid = true;
|
||||||
|
int braceBalance = 0;
|
||||||
|
int parenBalance = 0;
|
||||||
|
int bracketBalance = 0;
|
||||||
|
bool inStringLiteral = false;
|
||||||
|
bool inCharLiteral = false;
|
||||||
|
bool inSingleLineComment = false;
|
||||||
|
bool inMultiLineComment = false;
|
||||||
|
bool escaped = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < contents.Length; i++)
|
||||||
|
{
|
||||||
|
char c = contents[i];
|
||||||
|
char next = i + 1 < contents.Length ? contents[i + 1] : '\0';
|
||||||
|
|
||||||
|
// Handle escape sequences
|
||||||
|
if (escaped)
|
||||||
|
{
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '\\' && (inStringLiteral || inCharLiteral))
|
||||||
|
{
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle comments
|
||||||
|
if (!inStringLiteral && !inCharLiteral)
|
||||||
|
{
|
||||||
|
if (c == '/' && next == '/' && !inMultiLineComment)
|
||||||
|
{
|
||||||
|
inSingleLineComment = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '/' && next == '*' && !inSingleLineComment)
|
||||||
|
{
|
||||||
|
inMultiLineComment = true;
|
||||||
|
i++; // Skip next character
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '*' && next == '/' && inMultiLineComment)
|
||||||
|
{
|
||||||
|
inMultiLineComment = false;
|
||||||
|
i++; // Skip next character
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == '\n')
|
||||||
|
{
|
||||||
|
inSingleLineComment = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inSingleLineComment || inMultiLineComment)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Handle string and character literals
|
||||||
|
if (c == '"' && !inCharLiteral)
|
||||||
|
{
|
||||||
|
inStringLiteral = !inStringLiteral;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '\'' && !inStringLiteral)
|
||||||
|
{
|
||||||
|
inCharLiteral = !inCharLiteral;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inStringLiteral || inCharLiteral)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Count brackets and braces
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case '{': braceBalance++; break;
|
||||||
|
case '}': braceBalance--; break;
|
||||||
|
case '(': parenBalance++; break;
|
||||||
|
case ')': parenBalance--; break;
|
||||||
|
case '[': bracketBalance++; break;
|
||||||
|
case ']': bracketBalance--; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for negative balances (closing without opening)
|
||||||
|
if (braceBalance < 0)
|
||||||
|
{
|
||||||
|
errors.Add("ERROR: Unmatched closing brace '}'");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
if (parenBalance < 0)
|
||||||
|
{
|
||||||
|
errors.Add("ERROR: Unmatched closing parenthesis ')'");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
if (bracketBalance < 0)
|
||||||
|
{
|
||||||
|
errors.Add("ERROR: Unmatched closing bracket ']'");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check final balances
|
||||||
|
if (braceBalance != 0)
|
||||||
|
{
|
||||||
|
errors.Add($"ERROR: Unbalanced braces (difference: {braceBalance})");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
if (parenBalance != 0)
|
||||||
|
{
|
||||||
|
errors.Add($"ERROR: Unbalanced parentheses (difference: {parenBalance})");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
if (bracketBalance != 0)
|
||||||
|
{
|
||||||
|
errors.Add($"ERROR: Unbalanced brackets (difference: {bracketBalance})");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
if (inStringLiteral)
|
||||||
|
{
|
||||||
|
errors.Add("ERROR: Unterminated string literal");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
if (inCharLiteral)
|
||||||
|
{
|
||||||
|
errors.Add("ERROR: Unterminated character literal");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
if (inMultiLineComment)
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Unterminated multi-line comment");
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if USE_ROSLYN
|
||||||
|
/// <summary>
|
||||||
|
/// Cached compilation references for performance
|
||||||
|
/// </summary>
|
||||||
|
private static System.Collections.Generic.List<MetadataReference> _cachedReferences = null;
|
||||||
|
private static DateTime _cacheTime = DateTime.MinValue;
|
||||||
|
private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates syntax using Roslyn compiler services
|
||||||
|
/// </summary>
|
||||||
|
private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List<string> errors)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var syntaxTree = CSharpSyntaxTree.ParseText(contents);
|
||||||
|
var diagnostics = syntaxTree.GetDiagnostics();
|
||||||
|
|
||||||
|
bool hasErrors = false;
|
||||||
|
foreach (var diagnostic in diagnostics)
|
||||||
|
{
|
||||||
|
string severity = diagnostic.Severity.ToString().ToUpper();
|
||||||
|
string message = $"{severity}: {diagnostic.GetMessage()}";
|
||||||
|
|
||||||
|
if (diagnostic.Severity == DiagnosticSeverity.Error)
|
||||||
|
{
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include warnings in comprehensive mode
|
||||||
|
if (level >= ValidationLevel.Standard || diagnostic.Severity == DiagnosticSeverity.Error) //Also use Standard for now
|
||||||
|
{
|
||||||
|
var location = diagnostic.Location.GetLineSpan();
|
||||||
|
if (location.IsValid)
|
||||||
|
{
|
||||||
|
message += $" (Line {location.StartLinePosition.Line + 1})";
|
||||||
|
}
|
||||||
|
errors.Add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !hasErrors;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors.Add($"ERROR: Roslyn validation failed: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates script semantics using full compilation context to catch namespace, type, and method resolution errors
|
||||||
|
/// </summary>
|
||||||
|
private static bool ValidateScriptSemantics(string contents, System.Collections.Generic.List<string> errors)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get compilation references with caching
|
||||||
|
var references = GetCompilationReferences();
|
||||||
|
if (references == null || references.Count == 0)
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Could not load compilation references for semantic validation");
|
||||||
|
return true; // Don't fail if we can't get references
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create syntax tree
|
||||||
|
var syntaxTree = CSharpSyntaxTree.ParseText(contents);
|
||||||
|
|
||||||
|
// Create compilation with full context
|
||||||
|
var compilation = CSharpCompilation.Create(
|
||||||
|
"TempValidation",
|
||||||
|
new[] { syntaxTree },
|
||||||
|
references,
|
||||||
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get semantic diagnostics - this catches all the issues you mentioned!
|
||||||
|
var diagnostics = compilation.GetDiagnostics();
|
||||||
|
|
||||||
|
bool hasErrors = false;
|
||||||
|
foreach (var diagnostic in diagnostics)
|
||||||
|
{
|
||||||
|
if (diagnostic.Severity == DiagnosticSeverity.Error)
|
||||||
|
{
|
||||||
|
hasErrors = true;
|
||||||
|
var location = diagnostic.Location.GetLineSpan();
|
||||||
|
string locationInfo = location.IsValid ?
|
||||||
|
$" (Line {location.StartLinePosition.Line + 1}, Column {location.StartLinePosition.Character + 1})" : "";
|
||||||
|
|
||||||
|
// Include diagnostic ID for better error identification
|
||||||
|
string diagnosticId = !string.IsNullOrEmpty(diagnostic.Id) ? $" [{diagnostic.Id}]" : "";
|
||||||
|
errors.Add($"ERROR: {diagnostic.GetMessage()}{diagnosticId}{locationInfo}");
|
||||||
|
}
|
||||||
|
else if (diagnostic.Severity == DiagnosticSeverity.Warning)
|
||||||
|
{
|
||||||
|
var location = diagnostic.Location.GetLineSpan();
|
||||||
|
string locationInfo = location.IsValid ?
|
||||||
|
$" (Line {location.StartLinePosition.Line + 1}, Column {location.StartLinePosition.Character + 1})" : "";
|
||||||
|
|
||||||
|
string diagnosticId = !string.IsNullOrEmpty(diagnostic.Id) ? $" [{diagnostic.Id}]" : "";
|
||||||
|
errors.Add($"WARNING: {diagnostic.GetMessage()}{diagnosticId}{locationInfo}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !hasErrors;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errors.Add($"ERROR: Semantic validation failed: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets compilation references with caching for performance
|
||||||
|
/// </summary>
|
||||||
|
private static System.Collections.Generic.List<MetadataReference> GetCompilationReferences()
|
||||||
|
{
|
||||||
|
// Check cache validity
|
||||||
|
if (_cachedReferences != null && DateTime.Now - _cacheTime < CacheExpiry)
|
||||||
|
{
|
||||||
|
return _cachedReferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var references = new System.Collections.Generic.List<MetadataReference>();
|
||||||
|
|
||||||
|
// Core .NET assemblies
|
||||||
|
references.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); // mscorlib/System.Private.CoreLib
|
||||||
|
references.Add(MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location)); // System.Linq
|
||||||
|
references.Add(MetadataReference.CreateFromFile(typeof(System.Collections.Generic.List<>).Assembly.Location)); // System.Collections
|
||||||
|
|
||||||
|
// Unity assemblies
|
||||||
|
try
|
||||||
|
{
|
||||||
|
references.Add(MetadataReference.CreateFromFile(typeof(UnityEngine.Debug).Assembly.Location)); // UnityEngine
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Could not load UnityEngine assembly: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
try
|
||||||
|
{
|
||||||
|
references.Add(MetadataReference.CreateFromFile(typeof(UnityEditor.Editor).Assembly.Location)); // UnityEditor
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Could not load UnityEditor assembly: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Unity project assemblies
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assemblies = CompilationPipeline.GetAssemblies();
|
||||||
|
foreach (var assembly in assemblies)
|
||||||
|
{
|
||||||
|
if (File.Exists(assembly.outputPath))
|
||||||
|
{
|
||||||
|
references.Add(MetadataReference.CreateFromFile(assembly.outputPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Could not load Unity project assemblies: {ex.Message}");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Cache the results
|
||||||
|
_cachedReferences = references;
|
||||||
|
_cacheTime = DateTime.Now;
|
||||||
|
|
||||||
|
return references;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Failed to get compilation references: {ex.Message}");
|
||||||
|
return new System.Collections.Generic.List<MetadataReference>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List<string> errors)
|
||||||
|
{
|
||||||
|
// Fallback when Roslyn is not available
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates Unity-specific coding rules and best practices
|
||||||
|
/// //TODO: Naive Unity Checks and not really yield any results, need to be improved
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateScriptSyntaxUnity(string contents, System.Collections.Generic.List<string> errors)
|
||||||
|
{
|
||||||
|
// Check for common Unity anti-patterns
|
||||||
|
if (contents.Contains("FindObjectOfType") && contents.Contains("Update()"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: FindObjectOfType in Update() can cause performance issues");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contents.Contains("GameObject.Find") && contents.Contains("Update()"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: GameObject.Find in Update() can cause performance issues");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for proper MonoBehaviour usage
|
||||||
|
if (contents.Contains(": MonoBehaviour") && !contents.Contains("using UnityEngine"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: MonoBehaviour requires 'using UnityEngine;'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for SerializeField usage
|
||||||
|
if (contents.Contains("[SerializeField]") && !contents.Contains("using UnityEngine"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: SerializeField requires 'using UnityEngine;'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for proper coroutine usage
|
||||||
|
if (contents.Contains("StartCoroutine") && !contents.Contains("IEnumerator"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: StartCoroutine typically requires IEnumerator methods");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Update without FixedUpdate for physics
|
||||||
|
if (contents.Contains("Rigidbody") && contents.Contains("Update()") && !contents.Contains("FixedUpdate()"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Consider using FixedUpdate() for Rigidbody operations");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for missing null checks on Unity objects
|
||||||
|
if (contents.Contains("GetComponent<") && !contents.Contains("!= null"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Consider null checking GetComponent results");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for proper event function signatures
|
||||||
|
if (contents.Contains("void Start(") && !contents.Contains("void Start()"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Start() should not have parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contents.Contains("void Update(") && !contents.Contains("void Update()"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Update() should not have parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for inefficient string operations
|
||||||
|
if (contents.Contains("Update()") && contents.Contains("\"") && contents.Contains("+"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: String concatenation in Update() can cause garbage collection issues");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates semantic rules and common coding issues
|
||||||
|
/// </summary>
|
||||||
|
private static void ValidateSemanticRules(string contents, System.Collections.Generic.List<string> errors)
|
||||||
|
{
|
||||||
|
// Check for potential memory leaks
|
||||||
|
if (contents.Contains("new ") && contents.Contains("Update()"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Creating objects in Update() may cause memory issues");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for magic numbers
|
||||||
|
var magicNumberPattern = new Regex(@"\b\d+\.?\d*f?\b(?!\s*[;})\]])");
|
||||||
|
var matches = magicNumberPattern.Matches(contents);
|
||||||
|
if (matches.Count > 5)
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Consider using named constants instead of magic numbers");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for long methods (simple line count check)
|
||||||
|
var methodPattern = new Regex(@"(public|private|protected|internal)?\s*(static)?\s*\w+\s+\w+\s*\([^)]*\)\s*{");
|
||||||
|
var methodMatches = methodPattern.Matches(contents);
|
||||||
|
foreach (Match match in methodMatches)
|
||||||
|
{
|
||||||
|
int startIndex = match.Index;
|
||||||
|
int braceCount = 0;
|
||||||
|
int lineCount = 0;
|
||||||
|
bool inMethod = false;
|
||||||
|
|
||||||
|
for (int i = startIndex; i < contents.Length; i++)
|
||||||
|
{
|
||||||
|
if (contents[i] == '{')
|
||||||
|
{
|
||||||
|
braceCount++;
|
||||||
|
inMethod = true;
|
||||||
|
}
|
||||||
|
else if (contents[i] == '}')
|
||||||
|
{
|
||||||
|
braceCount--;
|
||||||
|
if (braceCount == 0 && inMethod)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (contents[i] == '\n' && inMethod)
|
||||||
|
{
|
||||||
|
lineCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineCount > 50)
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Method is very long, consider breaking it into smaller methods");
|
||||||
|
break; // Only report once
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for proper exception handling
|
||||||
|
if (contents.Contains("catch") && contents.Contains("catch()"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Empty catch blocks should be avoided");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for proper async/await usage
|
||||||
|
if (contents.Contains("async ") && !contents.Contains("await"))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Async method should contain await or return Task");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for hardcoded tags and layers
|
||||||
|
if (contents.Contains("\"Player\"") || contents.Contains("\"Enemy\""))
|
||||||
|
{
|
||||||
|
errors.Add("WARNING: Consider using constants for tags instead of hardcoded strings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: A easier way for users to update incorrect scripts (now duplicated with the updateScript method and need to also update server side, put aside for now)
|
||||||
|
/// <summary>
|
||||||
|
/// Public method to validate script syntax with configurable validation level
|
||||||
|
/// Returns detailed validation results including errors and warnings
|
||||||
|
/// </summary>
|
||||||
|
// public static object ValidateScript(JObject @params)
|
||||||
|
// {
|
||||||
|
// string contents = @params["contents"]?.ToString();
|
||||||
|
// string validationLevel = @params["validationLevel"]?.ToString() ?? "standard";
|
||||||
|
|
||||||
|
// if (string.IsNullOrEmpty(contents))
|
||||||
|
// {
|
||||||
|
// return Response.Error("Contents parameter is required for validation.");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Parse validation level
|
||||||
|
// ValidationLevel level = ValidationLevel.Standard;
|
||||||
|
// switch (validationLevel.ToLower())
|
||||||
|
// {
|
||||||
|
// case "basic": level = ValidationLevel.Basic; break;
|
||||||
|
// case "standard": level = ValidationLevel.Standard; break;
|
||||||
|
// case "comprehensive": level = ValidationLevel.Comprehensive; break;
|
||||||
|
// case "strict": level = ValidationLevel.Strict; break;
|
||||||
|
// default:
|
||||||
|
// return Response.Error($"Invalid validation level: '{validationLevel}'. Valid levels are: basic, standard, comprehensive, strict.");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Perform validation
|
||||||
|
// bool isValid = ValidateScriptSyntax(contents, level, out string[] validationErrors);
|
||||||
|
|
||||||
|
// var errors = validationErrors?.Where(e => e.StartsWith("ERROR:")).ToArray() ?? new string[0];
|
||||||
|
// var warnings = validationErrors?.Where(e => e.StartsWith("WARNING:")).ToArray() ?? new string[0];
|
||||||
|
|
||||||
|
// var result = new
|
||||||
|
// {
|
||||||
|
// isValid = isValid,
|
||||||
|
// validationLevel = validationLevel,
|
||||||
|
// errorCount = errors.Length,
|
||||||
|
// warningCount = warnings.Length,
|
||||||
|
// errors = errors,
|
||||||
|
// warnings = warnings,
|
||||||
|
// summary = isValid
|
||||||
|
// ? (warnings.Length > 0 ? $"Validation passed with {warnings.Length} warnings" : "Validation passed with no issues")
|
||||||
|
// : $"Validation failed with {errors.Length} errors and {warnings.Length} warnings"
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (isValid)
|
||||||
|
// {
|
||||||
|
// return Response.Success("Script validation completed successfully.", result);
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// return Response.Error("Script validation failed.", result);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,342 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityMcpBridge.Editor.Helpers;
|
||||||
|
|
||||||
|
namespace UnityMcpBridge.Editor.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles CRUD operations for shader files within the Unity project.
|
||||||
|
/// </summary>
|
||||||
|
public static class ManageShader
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Main handler for shader management actions.
|
||||||
|
/// </summary>
|
||||||
|
public static object HandleCommand(JObject @params)
|
||||||
|
{
|
||||||
|
// Extract parameters
|
||||||
|
string action = @params["action"]?.ToString().ToLower();
|
||||||
|
string name = @params["name"]?.ToString();
|
||||||
|
string path = @params["path"]?.ToString(); // Relative to Assets/
|
||||||
|
string contents = null;
|
||||||
|
|
||||||
|
// Check if we have base64 encoded contents
|
||||||
|
bool contentsEncoded = @params["contentsEncoded"]?.ToObject<bool>() ?? false;
|
||||||
|
if (contentsEncoded && @params["encodedContents"] != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
contents = DecodeBase64(@params["encodedContents"].ToString());
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Response.Error($"Failed to decode shader contents: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
contents = @params["contents"]?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required parameters
|
||||||
|
if (string.IsNullOrEmpty(action))
|
||||||
|
{
|
||||||
|
return Response.Error("Action parameter is required.");
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
return Response.Error("Name parameter is required.");
|
||||||
|
}
|
||||||
|
// Basic name validation (alphanumeric, underscores, cannot start with number)
|
||||||
|
if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
|
||||||
|
{
|
||||||
|
return Response.Error(
|
||||||
|
$"Invalid shader name: '{name}'. Use only letters, numbers, underscores, and don't start with a number."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
||||||
|
// Set default directory to "Shaders" if path is not provided
|
||||||
|
string relativeDir = path ?? "Shaders"; // Default to "Shaders" if path is null
|
||||||
|
if (!string.IsNullOrEmpty(relativeDir))
|
||||||
|
{
|
||||||
|
relativeDir = relativeDir.Replace('\\', '/').Trim('/');
|
||||||
|
if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle empty string case explicitly after processing
|
||||||
|
if (string.IsNullOrEmpty(relativeDir))
|
||||||
|
{
|
||||||
|
relativeDir = "Shaders"; // Ensure default if path was provided as "" or only "/" or "Assets/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct paths
|
||||||
|
string shaderFileName = $"{name}.shader";
|
||||||
|
string fullPathDir = Path.Combine(Application.dataPath, relativeDir);
|
||||||
|
string fullPath = Path.Combine(fullPathDir, shaderFileName);
|
||||||
|
string relativePath = Path.Combine("Assets", relativeDir, shaderFileName)
|
||||||
|
.Replace('\\', '/'); // Ensure "Assets/" prefix and forward slashes
|
||||||
|
|
||||||
|
// Ensure the target directory exists for create/update
|
||||||
|
if (action == "create" || action == "update")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(fullPathDir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(fullPathDir);
|
||||||
|
// Refresh AssetDatabase to recognize new folders
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Response.Error(
|
||||||
|
$"Could not create directory '{fullPathDir}': {e.Message}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route to specific action handlers
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case "create":
|
||||||
|
return CreateShader(fullPath, relativePath, name, contents);
|
||||||
|
case "read":
|
||||||
|
return ReadShader(fullPath, relativePath);
|
||||||
|
case "update":
|
||||||
|
return UpdateShader(fullPath, relativePath, name, contents);
|
||||||
|
case "delete":
|
||||||
|
return DeleteShader(fullPath, relativePath);
|
||||||
|
default:
|
||||||
|
return Response.Error(
|
||||||
|
$"Unknown action: '{action}'. Valid actions are: create, read, update, delete."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decode base64 string to normal text
|
||||||
|
/// </summary>
|
||||||
|
private static string DecodeBase64(string encoded)
|
||||||
|
{
|
||||||
|
byte[] data = Convert.FromBase64String(encoded);
|
||||||
|
return System.Text.Encoding.UTF8.GetString(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encode text to base64 string
|
||||||
|
/// </summary>
|
||||||
|
private static string EncodeBase64(string text)
|
||||||
|
{
|
||||||
|
byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
|
||||||
|
return Convert.ToBase64String(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object CreateShader(
|
||||||
|
string fullPath,
|
||||||
|
string relativePath,
|
||||||
|
string name,
|
||||||
|
string contents
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Check if shader already exists
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
return Response.Error(
|
||||||
|
$"Shader already exists at '{relativePath}'. Use 'update' action to modify."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add validation for shader name conflicts in Unity
|
||||||
|
if (Shader.Find(name) != null)
|
||||||
|
{
|
||||||
|
return Response.Error(
|
||||||
|
$"A shader with name '{name}' already exists in the project. Choose a different name."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate default content if none provided
|
||||||
|
if (string.IsNullOrEmpty(contents))
|
||||||
|
{
|
||||||
|
contents = GenerateDefaultShaderContent(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(fullPath, contents);
|
||||||
|
AssetDatabase.ImportAsset(relativePath);
|
||||||
|
AssetDatabase.Refresh(); // Ensure Unity recognizes the new shader
|
||||||
|
return Response.Success(
|
||||||
|
$"Shader '{name}.shader' created successfully at '{relativePath}'.",
|
||||||
|
new { path = relativePath }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Response.Error($"Failed to create shader '{relativePath}': {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ReadShader(string fullPath, string relativePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
return Response.Error($"Shader not found at '{relativePath}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string contents = File.ReadAllText(fullPath);
|
||||||
|
|
||||||
|
// Return both normal and encoded contents for larger files
|
||||||
|
//TODO: Consider a threshold for large files
|
||||||
|
bool isLarge = contents.Length > 10000; // If content is large, include encoded version
|
||||||
|
var responseData = new
|
||||||
|
{
|
||||||
|
path = relativePath,
|
||||||
|
contents = contents,
|
||||||
|
// For large files, also include base64-encoded version
|
||||||
|
encodedContents = isLarge ? EncodeBase64(contents) : null,
|
||||||
|
contentsEncoded = isLarge,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Response.Success(
|
||||||
|
$"Shader '{Path.GetFileName(relativePath)}' read successfully.",
|
||||||
|
responseData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Response.Error($"Failed to read shader '{relativePath}': {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object UpdateShader(
|
||||||
|
string fullPath,
|
||||||
|
string relativePath,
|
||||||
|
string name,
|
||||||
|
string contents
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
return Response.Error(
|
||||||
|
$"Shader not found at '{relativePath}'. Use 'create' action to add a new shader."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(contents))
|
||||||
|
{
|
||||||
|
return Response.Error("Content is required for the 'update' action.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(fullPath, contents);
|
||||||
|
AssetDatabase.ImportAsset(relativePath);
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
return Response.Success(
|
||||||
|
$"Shader '{Path.GetFileName(relativePath)}' updated successfully.",
|
||||||
|
new { path = relativePath }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Response.Error($"Failed to update shader '{relativePath}': {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object DeleteShader(string fullPath, string relativePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
return Response.Error($"Shader not found at '{relativePath}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Delete the asset through Unity's AssetDatabase first
|
||||||
|
bool success = AssetDatabase.DeleteAsset(relativePath);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
return Response.Error($"Failed to delete shader through Unity's AssetDatabase: '{relativePath}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the file still exists (rare case), try direct deletion
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
File.Delete(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.Success($"Shader '{Path.GetFileName(relativePath)}' deleted successfully.");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Response.Error($"Failed to delete shader '{relativePath}': {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//This is a CGProgram template
|
||||||
|
//TODO: making a HLSL template as well?
|
||||||
|
private static string GenerateDefaultShaderContent(string name)
|
||||||
|
{
|
||||||
|
return @"Shader """ + name + @"""
|
||||||
|
{
|
||||||
|
Properties
|
||||||
|
{
|
||||||
|
_MainTex (""Texture"", 2D) = ""white"" {}
|
||||||
|
}
|
||||||
|
SubShader
|
||||||
|
{
|
||||||
|
Tags { ""RenderType""=""Opaque"" }
|
||||||
|
LOD 100
|
||||||
|
|
||||||
|
Pass
|
||||||
|
{
|
||||||
|
CGPROGRAM
|
||||||
|
#pragma vertex vert
|
||||||
|
#pragma fragment frag
|
||||||
|
#include ""UnityCG.cginc""
|
||||||
|
|
||||||
|
struct appdata
|
||||||
|
{
|
||||||
|
float4 vertex : POSITION;
|
||||||
|
float2 uv : TEXCOORD0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct v2f
|
||||||
|
{
|
||||||
|
float2 uv : TEXCOORD0;
|
||||||
|
float4 vertex : SV_POSITION;
|
||||||
|
};
|
||||||
|
|
||||||
|
sampler2D _MainTex;
|
||||||
|
float4 _MainTex_ST;
|
||||||
|
|
||||||
|
v2f vert (appdata v)
|
||||||
|
{
|
||||||
|
v2f o;
|
||||||
|
o.vertex = UnityObjectToClipPos(v.vertex);
|
||||||
|
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed4 frag (v2f i) : SV_Target
|
||||||
|
{
|
||||||
|
fixed4 col = tex2D(_MainTex, i.uv);
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
ENDCG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bcf4f1f3110494344b2af9324cf5c571
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -25,9 +25,40 @@ namespace UnityMcpBridge.Editor
|
||||||
string,
|
string,
|
||||||
(string commandJson, TaskCompletionSource<string> tcs)
|
(string commandJson, TaskCompletionSource<string> tcs)
|
||||||
> commandQueue = new();
|
> commandQueue = new();
|
||||||
private static readonly int unityPort = 6400; // Hardcoded port
|
private static int currentUnityPort = 6400; // Dynamic port, starts with default
|
||||||
|
private static bool isAutoConnectMode = false;
|
||||||
|
|
||||||
public static bool IsRunning => isRunning;
|
public static bool IsRunning => isRunning;
|
||||||
|
public static int GetCurrentPort() => currentUnityPort;
|
||||||
|
public static bool IsAutoConnectMode() => isAutoConnectMode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start with Auto-Connect mode - discovers new port and saves it
|
||||||
|
/// </summary>
|
||||||
|
public static void StartAutoConnect()
|
||||||
|
{
|
||||||
|
Stop(); // Stop current connection
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Discover new port and save it
|
||||||
|
currentUnityPort = PortManager.DiscoverNewPort();
|
||||||
|
|
||||||
|
listener = new TcpListener(IPAddress.Loopback, currentUnityPort);
|
||||||
|
listener.Start();
|
||||||
|
isRunning = true;
|
||||||
|
isAutoConnectMode = true;
|
||||||
|
|
||||||
|
Debug.Log($"UnityMcpBridge auto-connected on port {currentUnityPort}");
|
||||||
|
Task.Run(ListenerLoop);
|
||||||
|
EditorApplication.update += ProcessCommands;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Auto-connect failed: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static bool FolderExists(string path)
|
public static bool FolderExists(string path)
|
||||||
{
|
{
|
||||||
|
|
@ -74,10 +105,14 @@ namespace UnityMcpBridge.Editor
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
listener = new TcpListener(IPAddress.Loopback, unityPort);
|
// Use PortManager to get available port with automatic fallback
|
||||||
|
currentUnityPort = PortManager.GetPortWithFallback();
|
||||||
|
|
||||||
|
listener = new TcpListener(IPAddress.Loopback, currentUnityPort);
|
||||||
listener.Start();
|
listener.Start();
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
Debug.Log($"UnityMcpBridge started on port {unityPort}.");
|
isAutoConnectMode = false; // Normal startup mode
|
||||||
|
Debug.Log($"UnityMcpBridge started on port {currentUnityPort}.");
|
||||||
// Assuming ListenerLoop and ProcessCommands are defined elsewhere
|
// Assuming ListenerLoop and ProcessCommands are defined elsewhere
|
||||||
Task.Run(ListenerLoop);
|
Task.Run(ListenerLoop);
|
||||||
EditorApplication.update += ProcessCommands;
|
EditorApplication.update += ProcessCommands;
|
||||||
|
|
@ -87,7 +122,7 @@ namespace UnityMcpBridge.Editor
|
||||||
if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
|
if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
|
||||||
{
|
{
|
||||||
Debug.LogError(
|
Debug.LogError(
|
||||||
$"Port {unityPort} is already in use. Ensure no other instances are running or change the port."
|
$"Port {currentUnityPort} is already in use. This should not happen with dynamic port allocation."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -379,6 +414,7 @@ namespace UnityMcpBridge.Editor
|
||||||
"manage_editor" => ManageEditor.HandleCommand(paramsObject),
|
"manage_editor" => ManageEditor.HandleCommand(paramsObject),
|
||||||
"manage_gameobject" => ManageGameObject.HandleCommand(paramsObject),
|
"manage_gameobject" => ManageGameObject.HandleCommand(paramsObject),
|
||||||
"manage_asset" => ManageAsset.HandleCommand(paramsObject),
|
"manage_asset" => ManageAsset.HandleCommand(paramsObject),
|
||||||
|
"manage_shader" => ManageShader.HandleCommand(paramsObject),
|
||||||
"read_console" => ReadConsole.HandleCommand(paramsObject),
|
"read_console" => ReadConsole.HandleCommand(paramsObject),
|
||||||
"execute_menu_item" => ExecuteMenuItem.HandleCommand(paramsObject),
|
"execute_menu_item" => ExecuteMenuItem.HandleCommand(paramsObject),
|
||||||
_ => throw new ArgumentException(
|
_ => throw new ArgumentException(
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
// Editor window to display manual configuration instructions
|
// Editor window to display manual configuration instructions
|
||||||
public class ManualConfigEditorWindow : EditorWindow
|
public class ManualConfigEditorWindow : EditorWindow
|
||||||
{
|
{
|
||||||
private string configPath;
|
protected string configPath;
|
||||||
private string configJson;
|
protected string configJson;
|
||||||
private Vector2 scrollPos;
|
protected Vector2 scrollPos;
|
||||||
private bool pathCopied = false;
|
protected bool pathCopied = false;
|
||||||
private bool jsonCopied = false;
|
protected bool jsonCopied = false;
|
||||||
private float copyFeedbackTimer = 0;
|
protected float copyFeedbackTimer = 0;
|
||||||
private McpClient mcpClient;
|
protected McpClient mcpClient;
|
||||||
|
|
||||||
public static void ShowWindow(string configPath, string configJson, McpClient mcpClient)
|
public static void ShowWindow(string configPath, string configJson, McpClient mcpClient)
|
||||||
{
|
{
|
||||||
|
|
@ -26,7 +26,7 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
window.Show();
|
window.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGUI()
|
protected virtual void OnGUI()
|
||||||
{
|
{
|
||||||
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
);
|
);
|
||||||
GUI.Label(
|
GUI.Label(
|
||||||
new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
|
new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
|
||||||
mcpClient.name + " Manual Configuration",
|
(mcpClient?.name ?? "Unknown") + " Manual Configuration",
|
||||||
EditorStyles.boldLabel
|
EditorStyles.boldLabel
|
||||||
);
|
);
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
|
|
@ -70,17 +70,17 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
};
|
};
|
||||||
|
|
||||||
EditorGUILayout.LabelField(
|
EditorGUILayout.LabelField(
|
||||||
"1. Open " + mcpClient.name + " config file by either:",
|
"1. Open " + (mcpClient?.name ?? "Unknown") + " config file by either:",
|
||||||
instructionStyle
|
instructionStyle
|
||||||
);
|
);
|
||||||
if (mcpClient.mcpType == McpTypes.ClaudeDesktop)
|
if (mcpClient?.mcpType == McpTypes.ClaudeDesktop)
|
||||||
{
|
{
|
||||||
EditorGUILayout.LabelField(
|
EditorGUILayout.LabelField(
|
||||||
" a) Going to Settings > Developer > Edit Config",
|
" a) Going to Settings > Developer > Edit Config",
|
||||||
instructionStyle
|
instructionStyle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else if (mcpClient.mcpType == McpTypes.Cursor)
|
else if (mcpClient?.mcpType == McpTypes.Cursor)
|
||||||
{
|
{
|
||||||
EditorGUILayout.LabelField(
|
EditorGUILayout.LabelField(
|
||||||
" a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server",
|
" a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server",
|
||||||
|
|
@ -96,16 +96,23 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
// Path section with improved styling
|
// Path section with improved styling
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
string displayPath;
|
string displayPath;
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (mcpClient != null)
|
||||||
{
|
{
|
||||||
displayPath = mcpClient.windowsConfigPath;
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
}
|
{
|
||||||
else if (
|
displayPath = mcpClient.windowsConfigPath;
|
||||||
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
}
|
||||||
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
else if (
|
||||||
)
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||||
{
|
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||||
displayPath = mcpClient.linuxConfigPath;
|
)
|
||||||
|
{
|
||||||
|
displayPath = mcpClient.linuxConfigPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
displayPath = configPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -224,7 +231,7 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
|
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
EditorGUILayout.LabelField(
|
EditorGUILayout.LabelField(
|
||||||
"3. Save the file and restart " + mcpClient.name,
|
"3. Save the file and restart " + (mcpClient?.name ?? "Unknown"),
|
||||||
instructionStyle
|
instructionStyle
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -245,7 +252,7 @@ namespace UnityMcpBridge.Editor.Windows
|
||||||
EditorGUILayout.EndScrollView();
|
EditorGUILayout.EndScrollView();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
protected virtual void Update()
|
||||||
{
|
{
|
||||||
// Handle the feedback message timer
|
// Handle the feedback message timer
|
||||||
if (copyFeedbackTimer > 0)
|
if (copyFeedbackTimer > 0)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,295 @@
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityMcpBridge.Editor.Models;
|
||||||
|
|
||||||
|
namespace UnityMcpBridge.Editor.Windows
|
||||||
|
{
|
||||||
|
public class VSCodeManualSetupWindow : ManualConfigEditorWindow
|
||||||
|
{
|
||||||
|
public static new void ShowWindow(string configPath, string configJson)
|
||||||
|
{
|
||||||
|
var window = GetWindow<VSCodeManualSetupWindow>("VSCode GitHub Copilot Setup");
|
||||||
|
window.configPath = configPath;
|
||||||
|
window.configJson = configJson;
|
||||||
|
window.minSize = new Vector2(550, 500);
|
||||||
|
|
||||||
|
// Create a McpClient for VSCode
|
||||||
|
window.mcpClient = new McpClient
|
||||||
|
{
|
||||||
|
name = "VSCode GitHub Copilot",
|
||||||
|
mcpType = McpTypes.VSCode
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnGUI()
|
||||||
|
{
|
||||||
|
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
||||||
|
|
||||||
|
// Header with improved styling
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
Rect titleRect = EditorGUILayout.GetControlRect(false, 30);
|
||||||
|
EditorGUI.DrawRect(
|
||||||
|
new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height),
|
||||||
|
new Color(0.2f, 0.2f, 0.2f, 0.1f)
|
||||||
|
);
|
||||||
|
GUI.Label(
|
||||||
|
new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
|
||||||
|
"VSCode GitHub Copilot MCP Setup",
|
||||||
|
EditorStyles.boldLabel
|
||||||
|
);
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
// Instructions with improved styling
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
||||||
|
Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
|
||||||
|
EditorGUI.DrawRect(
|
||||||
|
new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height),
|
||||||
|
new Color(0.1f, 0.1f, 0.1f, 0.2f)
|
||||||
|
);
|
||||||
|
GUI.Label(
|
||||||
|
new Rect(
|
||||||
|
headerRect.x + 8,
|
||||||
|
headerRect.y + 4,
|
||||||
|
headerRect.width - 16,
|
||||||
|
headerRect.height
|
||||||
|
),
|
||||||
|
"Setting up GitHub Copilot in VSCode with Unity MCP",
|
||||||
|
EditorStyles.boldLabel
|
||||||
|
);
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel)
|
||||||
|
{
|
||||||
|
margin = new RectOffset(10, 10, 5, 5),
|
||||||
|
};
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"1. Prerequisites",
|
||||||
|
EditorStyles.boldLabel
|
||||||
|
);
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"• Ensure you have VSCode installed",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"• Ensure you have GitHub Copilot extension installed in VSCode",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"• Ensure you have a valid GitHub Copilot subscription",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"2. Steps to Configure",
|
||||||
|
EditorStyles.boldLabel
|
||||||
|
);
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"a) Open VSCode Settings (File > Preferences > Settings)",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"b) Click on the 'Open Settings (JSON)' button in the top right",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"c) Add the MCP configuration shown below to your settings.json file",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"d) Save the file and restart VSCode",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
EditorGUILayout.Space(5);
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"3. VSCode settings.json location:",
|
||||||
|
EditorStyles.boldLabel
|
||||||
|
);
|
||||||
|
|
||||||
|
// Path section with improved styling
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
string displayPath;
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
displayPath = System.IO.Path.Combine(
|
||||||
|
System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData),
|
||||||
|
"Code",
|
||||||
|
"User",
|
||||||
|
"settings.json"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
displayPath = System.IO.Path.Combine(
|
||||||
|
System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"Code",
|
||||||
|
"User",
|
||||||
|
"settings.json"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the path in the base class config path
|
||||||
|
if (string.IsNullOrEmpty(configPath))
|
||||||
|
{
|
||||||
|
configPath = displayPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent text overflow by allowing the text field to wrap
|
||||||
|
GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true };
|
||||||
|
|
||||||
|
EditorGUILayout.TextField(
|
||||||
|
displayPath,
|
||||||
|
pathStyle,
|
||||||
|
GUILayout.Height(EditorGUIUtility.singleLineHeight)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy button with improved styling
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
GUIStyle copyButtonStyle = new(GUI.skin.button)
|
||||||
|
{
|
||||||
|
padding = new RectOffset(15, 15, 5, 5),
|
||||||
|
margin = new RectOffset(10, 10, 5, 5),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
GUILayout.Button(
|
||||||
|
"Copy Path",
|
||||||
|
copyButtonStyle,
|
||||||
|
GUILayout.Height(25),
|
||||||
|
GUILayout.Width(100)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
EditorGUIUtility.systemCopyBuffer = displayPath;
|
||||||
|
pathCopied = true;
|
||||||
|
copyFeedbackTimer = 2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
GUILayout.Button(
|
||||||
|
"Open File",
|
||||||
|
copyButtonStyle,
|
||||||
|
GUILayout.Height(25),
|
||||||
|
GUILayout.Width(100)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Open the file using the system's default application
|
||||||
|
System.Diagnostics.Process.Start(
|
||||||
|
new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = displayPath,
|
||||||
|
UseShellExecute = true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathCopied)
|
||||||
|
{
|
||||||
|
GUIStyle feedbackStyle = new(EditorStyles.label);
|
||||||
|
feedbackStyle.normal.textColor = Color.green;
|
||||||
|
EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"4. Add this configuration to your settings.json:",
|
||||||
|
EditorStyles.boldLabel
|
||||||
|
);
|
||||||
|
|
||||||
|
// JSON section with improved styling
|
||||||
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
||||||
|
// Improved text area for JSON with syntax highlighting colors
|
||||||
|
GUIStyle jsonStyle = new(EditorStyles.textArea)
|
||||||
|
{
|
||||||
|
font = EditorStyles.boldFont,
|
||||||
|
wordWrap = true,
|
||||||
|
};
|
||||||
|
jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue
|
||||||
|
|
||||||
|
// Draw the JSON in a text area with a taller height for better readability
|
||||||
|
EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200));
|
||||||
|
|
||||||
|
// Copy JSON button with improved styling
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
|
||||||
|
if (
|
||||||
|
GUILayout.Button(
|
||||||
|
"Copy JSON",
|
||||||
|
copyButtonStyle,
|
||||||
|
GUILayout.Height(25),
|
||||||
|
GUILayout.Width(100)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
EditorGUIUtility.systemCopyBuffer = configJson;
|
||||||
|
jsonCopied = true;
|
||||||
|
copyFeedbackTimer = 2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonCopied)
|
||||||
|
{
|
||||||
|
GUIStyle feedbackStyle = new(EditorStyles.label);
|
||||||
|
feedbackStyle.normal.textColor = Color.green;
|
||||||
|
EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"5. After configuration:",
|
||||||
|
EditorStyles.boldLabel
|
||||||
|
);
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"• Restart VSCode",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"• GitHub Copilot will now be able to interact with your Unity project through the MCP protocol",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
"• Remember to have the Unity MCP Bridge running in Unity Editor",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
|
||||||
|
EditorGUILayout.EndVertical();
|
||||||
|
|
||||||
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
|
// Close button at the bottom
|
||||||
|
EditorGUILayout.BeginHorizontal();
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100)))
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
GUILayout.FlexibleSpace();
|
||||||
|
EditorGUILayout.EndHorizontal();
|
||||||
|
|
||||||
|
EditorGUILayout.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
// Call the base implementation which handles the copy feedback timer
|
||||||
|
base.Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 377fe73d52cf0435fabead5f50a0d204
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# Install required system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install uv package manager
|
||||||
|
RUN pip install uv
|
||||||
|
|
||||||
|
# Copy required files
|
||||||
|
COPY config.py /app/
|
||||||
|
COPY server.py /app/
|
||||||
|
COPY unity_connection.py /app/
|
||||||
|
COPY pyproject.toml /app/
|
||||||
|
COPY __init__.py /app/
|
||||||
|
COPY tools/ /app/tools/
|
||||||
|
|
||||||
|
# Install dependencies using uv
|
||||||
|
RUN uv pip install --system -e .
|
||||||
|
|
||||||
|
|
||||||
|
# Command to run the server
|
||||||
|
CMD ["uv", "run", "server.py"]
|
||||||
|
|
@ -15,7 +15,7 @@ class ServerConfig:
|
||||||
mcp_port: int = 6500
|
mcp_port: int = 6500
|
||||||
|
|
||||||
# Connection settings
|
# Connection settings
|
||||||
connection_timeout: float = 86400.0 # 24 hours timeout
|
connection_timeout: float = 600.0 # 10 minutes timeout
|
||||||
buffer_size: int = 16 * 1024 * 1024 # 16MB buffer
|
buffer_size: int = 16 * 1024 * 1024 # 16MB buffer
|
||||||
|
|
||||||
# Logging settings
|
# Logging settings
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
"""
|
||||||
|
Port discovery utility for Unity MCP Server.
|
||||||
|
Reads port configuration saved by Unity Bridge.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger("unity-mcp-server")
|
||||||
|
|
||||||
|
class PortDiscovery:
|
||||||
|
"""Handles port discovery from Unity Bridge registry"""
|
||||||
|
|
||||||
|
REGISTRY_FILE = "unity-mcp-port.json"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_registry_path() -> Path:
|
||||||
|
"""Get the path to the port registry file"""
|
||||||
|
return Path.home() / ".unity-mcp" / PortDiscovery.REGISTRY_FILE
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def discover_unity_port() -> int:
|
||||||
|
"""
|
||||||
|
Discover Unity port from registry file with fallback to default
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Port number to connect to
|
||||||
|
"""
|
||||||
|
registry_file = PortDiscovery.get_registry_path()
|
||||||
|
|
||||||
|
if registry_file.exists():
|
||||||
|
try:
|
||||||
|
with open(registry_file, 'r') as f:
|
||||||
|
port_config = json.load(f)
|
||||||
|
|
||||||
|
unity_port = port_config.get('unity_port')
|
||||||
|
if unity_port and isinstance(unity_port, int):
|
||||||
|
logger.info(f"Discovered Unity port from registry: {unity_port}")
|
||||||
|
return unity_port
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not read port registry: {e}")
|
||||||
|
|
||||||
|
# Fallback to default port
|
||||||
|
logger.info("No port registry found, using default port 6400")
|
||||||
|
return 6400
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_port_config() -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Get the full port configuration from registry
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Port configuration dict or None if not found
|
||||||
|
"""
|
||||||
|
registry_file = PortDiscovery.get_registry_path()
|
||||||
|
|
||||||
|
if not registry_file.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(registry_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not read port configuration: {e}")
|
||||||
|
return None
|
||||||
|
|
@ -61,7 +61,8 @@ def asset_creation_strategy() -> str:
|
||||||
"- `manage_scene`: Manages scenes.\\n"
|
"- `manage_scene`: Manages scenes.\\n"
|
||||||
"- `manage_gameobject`: Manages GameObjects in the scene.\\n"
|
"- `manage_gameobject`: Manages GameObjects in the scene.\\n"
|
||||||
"- `manage_script`: Manages C# script files.\\n"
|
"- `manage_script`: Manages C# script files.\\n"
|
||||||
"- `manage_asset`: Manages prefabs and assets.\\n\\n"
|
"- `manage_asset`: Manages prefabs and assets.\\n"
|
||||||
|
"- `manage_shader`: Manages shaders.\\n\\n"
|
||||||
"Tips:\\n"
|
"Tips:\\n"
|
||||||
"- Create prefabs for reusable GameObjects.\\n"
|
"- Create prefabs for reusable GameObjects.\\n"
|
||||||
"- Always include a camera and main light in your scenes.\\n"
|
"- Always include a camera and main light in your scenes.\\n"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from .manage_scene import register_manage_scene_tools
|
||||||
from .manage_editor import register_manage_editor_tools
|
from .manage_editor import register_manage_editor_tools
|
||||||
from .manage_gameobject import register_manage_gameobject_tools
|
from .manage_gameobject import register_manage_gameobject_tools
|
||||||
from .manage_asset import register_manage_asset_tools
|
from .manage_asset import register_manage_asset_tools
|
||||||
|
from .manage_shader import register_manage_shader_tools
|
||||||
from .read_console import register_read_console_tools
|
from .read_console import register_read_console_tools
|
||||||
from .execute_menu_item import register_execute_menu_item_tools
|
from .execute_menu_item import register_execute_menu_item_tools
|
||||||
|
|
||||||
|
|
@ -14,6 +15,7 @@ def register_all_tools(mcp):
|
||||||
register_manage_editor_tools(mcp)
|
register_manage_editor_tools(mcp)
|
||||||
register_manage_gameobject_tools(mcp)
|
register_manage_gameobject_tools(mcp)
|
||||||
register_manage_asset_tools(mcp)
|
register_manage_asset_tools(mcp)
|
||||||
|
register_manage_shader_tools(mcp)
|
||||||
register_read_console_tools(mcp)
|
register_read_console_tools(mcp)
|
||||||
register_execute_menu_item_tools(mcp)
|
register_execute_menu_item_tools(mcp)
|
||||||
print("Unity MCP Server tool registration complete.")
|
print("Unity MCP Server tool registration complete.")
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ def register_manage_asset_tools(mcp: FastMCP):
|
||||||
path: Asset path (e.g., "Materials/MyMaterial.mat") or search scope.
|
path: Asset path (e.g., "Materials/MyMaterial.mat") or search scope.
|
||||||
asset_type: Asset type (e.g., 'Material', 'Folder') - required for 'create'.
|
asset_type: Asset type (e.g., 'Material', 'Folder') - required for 'create'.
|
||||||
properties: Dictionary of properties for 'create'/'modify'.
|
properties: Dictionary of properties for 'create'/'modify'.
|
||||||
|
example properties for Material: {"color": [1, 0, 0, 1], "shader": "Standard"}.
|
||||||
|
example properties for Texture: {"width": 1024, "height": 1024, "format": "RGBA32"}.
|
||||||
|
example properties for PhysicsMaterial: {"bounciness": 1.0, "staticFriction": 0.5, "dynamicFriction": 0.5}.
|
||||||
destination: Target path for 'duplicate'/'move'.
|
destination: Target path for 'duplicate'/'move'.
|
||||||
search_pattern: Search pattern (e.g., '*.prefab').
|
search_pattern: Search pattern (e.g., '*.prefab').
|
||||||
filter_*: Filters for search (type, date).
|
filter_*: Filters for search (type, date).
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ def register_manage_gameobject_tools(mcp: FastMCP):
|
||||||
"""Manages GameObjects: create, modify, delete, find, and component operations.
|
"""Manages GameObjects: create, modify, delete, find, and component operations.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property').
|
action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property', 'get_components').
|
||||||
target: GameObject identifier (name or path string) for modify/delete/component actions.
|
target: GameObject identifier (name or path string) for modify/delete/component actions.
|
||||||
search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups.
|
search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups.
|
||||||
name: GameObject name - used for both 'create' (initial name) and 'modify' (rename).
|
name: GameObject name - used for both 'create' (initial name) and 'modify' (rename).
|
||||||
|
|
@ -62,8 +62,16 @@ def register_manage_gameobject_tools(mcp: FastMCP):
|
||||||
search_term, find_all for 'find').
|
search_term, find_all for 'find').
|
||||||
includeNonPublicSerialized: If True, includes private fields marked [SerializeField] in component data.
|
includeNonPublicSerialized: If True, includes private fields marked [SerializeField] in component data.
|
||||||
|
|
||||||
|
Action-specific details:
|
||||||
|
- For 'get_components':
|
||||||
|
Required: target, search_method
|
||||||
|
Optional: includeNonPublicSerialized (defaults to True)
|
||||||
|
Returns all components on the target GameObject with their serialized data.
|
||||||
|
The search_method parameter determines how to find the target ('by_name', 'by_id', 'by_path').
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with operation results ('success', 'message', 'data').
|
Dictionary with operation results ('success', 'message', 'data').
|
||||||
|
For 'get_components', the 'data' field contains a dictionary of component names and their serialized properties.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# --- Early check for attempting to modify a prefab asset ---
|
# --- Early check for attempting to modify a prefab asset ---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
|
from typing import Dict, Any
|
||||||
|
from unity_connection import get_unity_connection
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
|
def register_manage_shader_tools(mcp: FastMCP):
|
||||||
|
"""Register all shader script management tools with the MCP server."""
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def manage_shader(
|
||||||
|
ctx: Context,
|
||||||
|
action: str,
|
||||||
|
name: str,
|
||||||
|
path: str,
|
||||||
|
contents: str,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Manages shader scripts in Unity (create, read, update, delete).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action: Operation ('create', 'read', 'update', 'delete').
|
||||||
|
name: Shader name (no .cs extension).
|
||||||
|
path: Asset path (default: "Assets/").
|
||||||
|
contents: Shader code for 'create'/'update'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with results ('success', 'message', 'data').
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Prepare parameters for Unity
|
||||||
|
params = {
|
||||||
|
"action": action,
|
||||||
|
"name": name,
|
||||||
|
"path": path,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Base64 encode the contents if they exist to avoid JSON escaping issues
|
||||||
|
if contents is not None:
|
||||||
|
if action in ['create', 'update']:
|
||||||
|
# Encode content for safer transmission
|
||||||
|
params["encodedContents"] = base64.b64encode(contents.encode('utf-8')).decode('utf-8')
|
||||||
|
params["contentsEncoded"] = True
|
||||||
|
else:
|
||||||
|
params["contents"] = contents
|
||||||
|
|
||||||
|
# Remove None values so they don't get sent as null
|
||||||
|
params = {k: v for k, v in params.items() if v is not None}
|
||||||
|
|
||||||
|
# Send command to Unity
|
||||||
|
response = get_unity_connection().send_command("manage_shader", params)
|
||||||
|
|
||||||
|
# Process response from Unity
|
||||||
|
if response.get("success"):
|
||||||
|
# If the response contains base64 encoded content, decode it
|
||||||
|
if response.get("data", {}).get("contentsEncoded"):
|
||||||
|
decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8')
|
||||||
|
response["data"]["contents"] = decoded_contents
|
||||||
|
del response["data"]["encodedContents"]
|
||||||
|
del response["data"]["contentsEncoded"]
|
||||||
|
|
||||||
|
return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")}
|
||||||
|
else:
|
||||||
|
return {"success": False, "message": response.get("error", "An unknown error occurred.")}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Handle Python-side errors (e.g., connection issues)
|
||||||
|
return {"success": False, "message": f"Python error managing shader: {str(e)}"}
|
||||||
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from config import config
|
from config import config
|
||||||
|
from port_discovery import PortDiscovery
|
||||||
|
|
||||||
# Configure logging using settings from config
|
# Configure logging using settings from config
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
|
@ -16,9 +17,14 @@ logger = logging.getLogger("unity-mcp-server")
|
||||||
class UnityConnection:
|
class UnityConnection:
|
||||||
"""Manages the socket connection to the Unity Editor."""
|
"""Manages the socket connection to the Unity Editor."""
|
||||||
host: str = config.unity_host
|
host: str = config.unity_host
|
||||||
port: int = config.unity_port
|
port: int = None # Will be set dynamically
|
||||||
sock: socket.socket = None # Socket for Unity communication
|
sock: socket.socket = None # Socket for Unity communication
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
"""Set port from discovery if not explicitly provided"""
|
||||||
|
if self.port is None:
|
||||||
|
self.port = PortDiscovery.discover_unity_port()
|
||||||
|
|
||||||
def connect(self) -> bool:
|
def connect(self) -> bool:
|
||||||
"""Establish a connection to the Unity Editor."""
|
"""Establish a connection to the Unity Editor."""
|
||||||
if self.sock:
|
if self.sock:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue