diff --git a/Assets/MCPForUnity.meta b/Assets/MCPForUnity.meta deleted file mode 100644 index 48917aa..0000000 --- a/Assets/MCPForUnity.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 45b75a96cdaa04af6b020f407d1a877c -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor.meta b/Assets/MCPForUnity/Editor.meta deleted file mode 100644 index 26495d4..0000000 --- a/Assets/MCPForUnity/Editor.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 31e7fac5858840340a75cc6df0ad3d9e -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/AssemblyInfo.cs b/Assets/MCPForUnity/Editor/AssemblyInfo.cs deleted file mode 100644 index bae75b6..0000000 --- a/Assets/MCPForUnity/Editor/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")] diff --git a/Assets/MCPForUnity/Editor/AssemblyInfo.cs.meta b/Assets/MCPForUnity/Editor/AssemblyInfo.cs.meta deleted file mode 100644 index 5a9deca..0000000 --- a/Assets/MCPForUnity/Editor/AssemblyInfo.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: be61633e00d934610ac1ff8192ffbe3d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/AssemblyInfo.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients.meta b/Assets/MCPForUnity/Editor/Clients.meta deleted file mode 100644 index b4105b3..0000000 --- a/Assets/MCPForUnity/Editor/Clients.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: c9d47f01d06964ee7843765d1bd71205 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators.meta b/Assets/MCPForUnity/Editor/Clients/Configurators.meta deleted file mode 100644 index a259c21..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 59ff83375c2c74c8385c4a22549778dd -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs deleted file mode 100644 index 9a83620..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Models; -using UnityEditor; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class AntigravityConfigurator : JsonFileMcpConfigurator - { - public AntigravityConfigurator() : base(new McpClient - { - name = "Antigravity", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"), - HttpUrlProperty = "serverUrl", - DefaultUnityFields = { { "disabled", false } }, - StripEnvWhenNotRequired = true - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Open Antigravity", - "Click the more_horiz menu in the Agent pane > MCP Servers", - "Select 'Install' for Unity MCP or use the Configure button above", - "Restart Antigravity if necessary" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs.meta deleted file mode 100644 index 4f08cd4..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 331b33961513042e3945d0a1d06615b5 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/CherryStudioConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/CherryStudioConfigurator.cs deleted file mode 100644 index bf14559..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/CherryStudioConfigurator.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Models; -using MCPForUnity.Editor.Services; -using UnityEditor; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class CherryStudioConfigurator : JsonFileMcpConfigurator - { - public const string ClientName = "Cherry Studio"; - - public CherryStudioConfigurator() : base(new McpClient - { - name = ClientName, - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Cherry Studio", "config"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Cherry Studio", "config"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Cherry Studio", "config"), - SupportsHttpTransport = false - }) - { } - - public override bool SupportsAutoConfigure => false; - - public override IList GetInstallationSteps() => new List - { - "Open Cherry Studio", - "Go to Settings (⚙️) → MCP Server", - "Click 'Add Server' button", - "For STDIO mode (recommended):", - " - Name: unity-mcp", - " - Type: STDIO", - " - Command: uvx", - " - Arguments: Copy from the Manual Configuration JSON below", - "Click Save and restart Cherry Studio", - "", - "Note: Cherry Studio uses UI-based configuration.", - "Use the manual snippet below as reference for the values to enter." - }; - - public override McpStatus CheckStatus(bool attemptAutoRewrite = true) - { - client.SetStatus(McpStatus.NotConfigured, "Cherry Studio requires manual UI configuration"); - return client.status; - } - - public override void Configure() - { - throw new InvalidOperationException( - "Cherry Studio uses UI-based configuration. " + - "Please use the Manual Configuration snippet and Installation Steps to configure manually." - ); - } - - public override string GetManualSnippet() - { - bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; - - if (useHttp) - { - return "# Cherry Studio does not support WebSocket transport.\n" + - "# Cherry Studio supports STDIO and SSE transports.\n" + - "# \n" + - "# To use Cherry Studio:\n" + - "# 1. Switch transport to 'Stdio' in Advanced Settings below\n" + - "# 2. Return to this configuration screen\n" + - "# 3. Copy the STDIO configuration snippet that will appear\n" + - "# \n" + - "# OPTION 2: SSE mode (future support)\n" + - "# Note: Unity MCP does not currently have an SSE endpoint.\n" + - "# This may be added in a future update."; - } - - return base.GetManualSnippet() + "\n\n" + - "# Cherry Studio Configuration Instructions:\n" + - "# Cherry Studio uses UI-based configuration, not a JSON file.\n" + - "# \n" + - "# To configure:\n" + - "# 1. Open Cherry Studio\n" + - "# 2. Go to Settings (⚙️) → MCP Server\n" + - "# 3. Click 'Add Server'\n" + - "# 4. Enter the following values from the JSON above:\n" + - "# - Name: unity-mcp\n" + - "# - Type: STDIO\n" + - "# - Command: (copy 'command' value from JSON)\n" + - "# - Arguments: (copy 'args' array values, space-separated or as individual entries)\n" + - "# - Active: true\n" + - "# 5. Click Save\n" + - "# 6. Restart Cherry Studio"; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/CherryStudioConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/CherryStudioConfigurator.cs.meta deleted file mode 100644 index c7d5d2e..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/CherryStudioConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 6de06c6bb0399154d840a1e4c84be869 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/CherryStudioConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs deleted file mode 100644 index c890d7c..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - /// - /// Claude Code configurator using the CLI-based registration (claude mcp add/remove). - /// This integrates with Claude Code's native MCP management. - /// - public class ClaudeCodeConfigurator : ClaudeCliMcpConfigurator - { - public ClaudeCodeConfigurator() : base(new McpClient - { - name = "Claude Code", - SupportsHttpTransport = true, - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Ensure Claude CLI is installed (comes with Claude Code)", - "Click Register to add UnityMCP via 'claude mcp add'", - "The server will be automatically available in Claude Code", - "Use Unregister to remove via 'claude mcp remove'" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs.meta deleted file mode 100644 index 85f9ee9..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: d0d22681fc594475db1c189f2d9abdf7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs deleted file mode 100644 index 7286134..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Models; -using MCPForUnity.Editor.Services; -using UnityEditor; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class ClaudeDesktopConfigurator : JsonFileMcpConfigurator - { - public const string ClientName = "Claude Desktop"; - - public ClaudeDesktopConfigurator() : base(new McpClient - { - name = ClientName, - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Claude", "claude_desktop_config.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude", "claude_desktop_config.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Claude", "claude_desktop_config.json"), - SupportsHttpTransport = false, - StripEnvWhenNotRequired = true - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Open Claude Desktop", - "Go to Settings > Developer > Edit Config\nOR open the config path", - "Paste the configuration JSON", - "Save and restart Claude Desktop" - }; - - public override void Configure() - { - bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; - if (useHttp) - { - throw new InvalidOperationException("Claude Desktop does not support HTTP transport. Switch to stdio in settings before configuring."); - } - - base.Configure(); - } - - public override string GetManualSnippet() - { - bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; - if (useHttp) - { - return "# Claude Desktop does not support HTTP transport.\n" + - "# Open Advanced Settings and disable HTTP transport to use stdio, then regenerate."; - } - - return base.GetManualSnippet(); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs.meta deleted file mode 100644 index 9df852c..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: d5e5d87c9db57495f842dc366f1ebd65 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/CodeBuddyCliConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/CodeBuddyCliConfigurator.cs deleted file mode 100644 index 4ef3624..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/CodeBuddyCliConfigurator.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - /// - /// Configures the CodeBuddy CLI (~/.codebuddy.json) MCP settings. - /// - public class CodeBuddyCliConfigurator : JsonFileMcpConfigurator - { - public CodeBuddyCliConfigurator() : base(new McpClient - { - name = "CodeBuddy CLI", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"), - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Install CodeBuddy CLI and ensure '~/.codebuddy.json' exists", - "Click Configure to add the UnityMCP entry (or manually edit the file above)", - "Restart your CLI session if needed" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/CodeBuddyCliConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/CodeBuddyCliConfigurator.cs.meta deleted file mode 100644 index 2d20ead..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/CodeBuddyCliConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 923728a98c8c74cfaa6e9203c408f34e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/CodeBuddyCliConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs deleted file mode 100644 index 9337d4c..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class CodexConfigurator : CodexMcpConfigurator - { - public CodexConfigurator() : base(new McpClient - { - name = "Codex", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml") - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Run 'codex config edit' in a terminal\nOR open the config file at the path above", - "Paste the configuration TOML", - "Save and restart Codex" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs.meta deleted file mode 100644 index 2d374ad..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: c7037ef8b168e49f79247cb31c3be75a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/CopilotCliConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/CopilotCliConfigurator.cs deleted file mode 100644 index 84120a4..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/CopilotCliConfigurator.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class CopilotCliConfigurator : JsonFileMcpConfigurator - { - public CopilotCliConfigurator() : base(new McpClient - { - name = "GitHub Copilot CLI", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json") - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Install GitHub Copilot CLI (https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)", - "Open or create mcp-config.json at the path above", - "Paste the configuration JSON (or use /mcp add in the CLI)", - "Restart your Copilot CLI session" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/CopilotCliConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/CopilotCliConfigurator.cs.meta deleted file mode 100644 index 7fa9987..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/CopilotCliConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 14a4b9a7f749248d496466c2a3a53e56 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/CopilotCliConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs deleted file mode 100644 index d63b226..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class CursorConfigurator : JsonFileMcpConfigurator - { - public CursorConfigurator() : base(new McpClient - { - name = "Cursor", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json") - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Open Cursor", - "Go to File > Preferences > Cursor Settings > MCP > Add new global MCP server\nOR open the config file at the path above", - "Paste the configuration JSON", - "Save and restart Cursor" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs.meta deleted file mode 100644 index 60e70b2..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b708eda314746481fb8f4a1fb0652b03 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/KiloCodeConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/KiloCodeConfigurator.cs deleted file mode 100644 index e62b645..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/KiloCodeConfigurator.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class KiloCodeConfigurator : JsonFileMcpConfigurator - { - public KiloCodeConfigurator() : base(new McpClient - { - name = "Kilo Code", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "globalStorage", "kilocode.kilo-code", "settings", "mcp_settings.json"), - IsVsCodeLayout = true - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Install Kilo Code extension in VS Code", - "Open Kilo Code settings (gear icon in sidebar)", - "Navigate to MCP Servers section and click 'Edit Global MCP Settings'\nOR open the config file at the path above", - "Paste the configuration JSON into the mcpServers object", - "Save and restart VS Code" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/KiloCodeConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/KiloCodeConfigurator.cs.meta deleted file mode 100644 index d8a11e9..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/KiloCodeConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 3286d62ffe5644f5ea60488fd7e6513d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/KiloCodeConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs deleted file mode 100644 index 445b6e5..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class KiroConfigurator : JsonFileMcpConfigurator - { - public KiroConfigurator() : base(new McpClient - { - name = "Kiro", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"), - EnsureEnvObject = true, - DefaultUnityFields = { { "disabled", false } } - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Open Kiro", - "Go to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config\nOR open the config file at the path above", - "Paste the configuration JSON", - "Save and restart Kiro" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs.meta deleted file mode 100644 index 96e9162..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: e9b73ff071a6043dda1f2ec7d682ef71 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs deleted file mode 100644 index 34ab535..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - /// - /// Configurator for OpenCode (opencode.ai) - a Go-based terminal AI coding assistant. - /// OpenCode uses ~/.config/opencode/opencode.json with a custom "mcp" format. - /// - public class OpenCodeConfigurator : McpClientConfiguratorBase - { - private const string ServerName = "unityMCP"; - private const string SchemaUrl = "https://opencode.ai/config.json"; - - public OpenCodeConfigurator() : base(new McpClient - { - name = "OpenCode", - windowsConfigPath = BuildConfigPath(), - macConfigPath = BuildConfigPath(), - linuxConfigPath = BuildConfigPath() - }) - { } - - private static string BuildConfigPath() - { - string xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); - string configBase = !string.IsNullOrEmpty(xdgConfigHome) - ? xdgConfigHome - : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); - return Path.Combine(configBase, "opencode", "opencode.json"); - } - - public override string GetConfigPath() => CurrentOsPath(); - - /// - /// Attempts to load and parse the config file. - /// Returns null if file doesn't exist or cannot be read. - /// Returns parsed JObject if valid JSON found. - /// Logs warning if file exists but contains malformed JSON. - /// - private JObject TryLoadConfig(string path) - { - if (!File.Exists(path)) - return null; - - string content; - try - { - content = File.ReadAllText(path); - } - catch (Exception ex) - { - UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Failed to read config file {path}: {ex.Message}"); - return null; - } - - try - { - return JsonConvert.DeserializeObject(content) ?? new JObject(); - } - catch (JsonException ex) - { - // Malformed JSON - log warning and return null. - // When Configure() receives null, it will do: TryLoadConfig(path) ?? new JObject() - // This creates a fresh empty JObject, which replaces the entire file with only the unityMCP section. - // Existing config sections are lost. To preserve sections, a different recovery strategy - // (e.g., line-by-line parsing, JSON repair, or manual user intervention) would be needed. - UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Malformed JSON in {path}: {ex.Message}"); - return null; - } - } - - public override McpStatus CheckStatus(bool attemptAutoRewrite = true) - { - try - { - string path = GetConfigPath(); - var config = TryLoadConfig(path); - - if (config == null) - { - client.SetStatus(McpStatus.NotConfigured); - return client.status; - } - - var unityMcp = config["mcp"]?[ServerName] as JObject; - - if (unityMcp == null) - { - client.SetStatus(McpStatus.NotConfigured); - return client.status; - } - - string configuredUrl = unityMcp["url"]?.ToString(); - string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl(); - - if (UrlsEqual(configuredUrl, expectedUrl)) - { - client.SetStatus(McpStatus.Configured); - } - else if (attemptAutoRewrite) - { - Configure(); - } - else - { - client.SetStatus(McpStatus.IncorrectPath); - } - } - catch (Exception ex) - { - client.SetStatus(McpStatus.Error, ex.Message); - } - - return client.status; - } - - public override void Configure() - { - try - { - string path = GetConfigPath(); - McpConfigurationHelper.EnsureConfigDirectoryExists(path); - - // Load existing config or start fresh, preserving all other properties and MCP servers - var config = TryLoadConfig(path) ?? new JObject(); - - // Only add $schema if creating a new file - if (!File.Exists(path)) - { - config["$schema"] = SchemaUrl; - } - - // Preserve existing mcp section and only update our server entry - var mcpSection = config["mcp"] as JObject ?? new JObject(); - config["mcp"] = mcpSection; - - mcpSection[ServerName] = BuildServerEntry(); - - McpConfigurationHelper.WriteAtomicFile(path, JsonConvert.SerializeObject(config, Formatting.Indented)); - client.SetStatus(McpStatus.Configured); - } - catch (Exception ex) - { - client.SetStatus(McpStatus.Error, ex.Message); - } - } - - public override string GetManualSnippet() - { - var snippet = new JObject - { - ["mcp"] = new JObject { [ServerName] = BuildServerEntry() } - }; - return JsonConvert.SerializeObject(snippet, Formatting.Indented); - } - - public override IList GetInstallationSteps() => new List - { - "Install OpenCode (https://opencode.ai)", - "Click Configure to add Unity MCP to ~/.config/opencode/opencode.json", - "Restart OpenCode", - "The Unity MCP server should be detected automatically" - }; - - private static JObject BuildServerEntry() => new JObject - { - ["type"] = "remote", - ["url"] = HttpEndpointUtility.GetMcpRpcUrl(), - ["enabled"] = true - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta deleted file mode 100644 index a48a112..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 489f99ffb7e6743e88e3203552c8b37b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs deleted file mode 100644 index 2558a42..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class RiderConfigurator : JsonFileMcpConfigurator - { - public RiderConfigurator() : base(new McpClient - { - name = "Rider GitHub Copilot", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "github-copilot", "intellij", "mcp.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "github-copilot", "intellij", "mcp.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "github-copilot", "intellij", "mcp.json"), - IsVsCodeLayout = true - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Install GitHub Copilot plugin in Rider", - "Open or create mcp.json at the path above", - "Paste the configuration JSON", - "Save and restart Rider" - }; - } -} - diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs.meta deleted file mode 100644 index e30b168..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 2511b0d05271d486bb61f8cc9fd11363 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs deleted file mode 100644 index f32b688..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class TraeConfigurator : JsonFileMcpConfigurator - { - public TraeConfigurator() : base(new McpClient - { - name = "Trae", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Trae", "mcp.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Trae", "mcp.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Trae", "mcp.json"), - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Open Trae and go to Settings > MCP", - "Select Add Server > Add Manually", - "Paste the JSON or point to the mcp.json file\n"+ - "Windows: %AppData%\\Trae\\mcp.json\n" + - "macOS: ~/Library/Application Support/Trae/mcp.json\n" + - "Linux: ~/.config/Trae/mcp.json\n", - "Save and restart Trae" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs.meta deleted file mode 100644 index 617d0d4..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b3ab39e22ae0948ab94beae307f9902e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs deleted file mode 100644 index 9057930..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class VSCodeConfigurator : JsonFileMcpConfigurator - { - public VSCodeConfigurator() : base(new McpClient - { - name = "VSCode GitHub Copilot", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "mcp.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "mcp.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "mcp.json"), - IsVsCodeLayout = true - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Install GitHub Copilot extension", - "Open or create mcp.json at the path above", - "Paste the configuration JSON", - "Save and restart VSCode" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs.meta deleted file mode 100644 index 4f45de6..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: bcc7ead475a4d4ea2978151c217757b8 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeInsidersConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeInsidersConfigurator.cs deleted file mode 100644 index 55adb28..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeInsidersConfigurator.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class VSCodeInsidersConfigurator : JsonFileMcpConfigurator - { - public VSCodeInsidersConfigurator() : base(new McpClient - { - name = "VSCode Insiders GitHub Copilot", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code - Insiders", "User", "mcp.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code - Insiders", "User", "mcp.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code - Insiders", "User", "mcp.json"), - IsVsCodeLayout = true - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Install GitHub Copilot extension in VS Code Insiders", - "Open or create mcp.json at the path above", - "Paste the configuration JSON", - "Save and restart VS Code Insiders" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeInsidersConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeInsidersConfigurator.cs.meta deleted file mode 100644 index a714695..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeInsidersConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 2c4a1b0d3b34489cbf0f8c40c49c4f3b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/VSCodeInsidersConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs deleted file mode 100644 index 4437170..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients.Configurators -{ - public class WindsurfConfigurator : JsonFileMcpConfigurator - { - public WindsurfConfigurator() : base(new McpClient - { - name = "Windsurf", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"), - HttpUrlProperty = "serverUrl", - DefaultUnityFields = { { "disabled", false } }, - StripEnvWhenNotRequired = true - }) - { } - - public override IList GetInstallationSteps() => new List - { - "Open Windsurf", - "Go to File > Preferences > Windsurf Settings > MCP > Manage MCPs > View raw config\nOR open the config file at the path above", - "Paste the configuration JSON", - "Save and restart Windsurf" - }; - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs.meta deleted file mode 100644 index f730d68..0000000 --- a/Assets/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b528971e189f141d38db577f155bd222 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs b/Assets/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs deleted file mode 100644 index 9fdea29..0000000 --- a/Assets/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs +++ /dev/null @@ -1,47 +0,0 @@ -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Clients -{ - /// - /// Contract for MCP client configurators. Each client is responsible for - /// status detection, auto-configure, and manual snippet/steps. - /// - public interface IMcpClientConfigurator - { - /// Stable identifier (e.g., "cursor"). - string Id { get; } - - /// Display name shown in the UI. - string DisplayName { get; } - - /// Current status cached by the configurator. - McpStatus Status { get; } - - /// - /// The transport type the client is currently configured for. - /// Returns Unknown if the client is not configured or the transport cannot be determined. - /// - ConfiguredTransport ConfiguredTransport { get; } - - /// True if this client supports auto-configure. - bool SupportsAutoConfigure { get; } - - /// Label to show on the configure button for the current state. - string GetConfigureActionLabel(); - - /// Returns the platform-specific config path (or message for CLI-managed clients). - string GetConfigPath(); - - /// Checks and updates status; returns current status. - McpStatus CheckStatus(bool attemptAutoRewrite = true); - - /// Runs auto-configuration (register/write file/CLI etc.). - void Configure(); - - /// Returns the manual configuration snippet (JSON/TOML/commands). - string GetManualSnippet(); - - /// Returns ordered human-readable installation steps. - System.Collections.Generic.IList GetInstallationSteps(); - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs.meta b/Assets/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs.meta deleted file mode 100644 index 865b091..0000000 --- a/Assets/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: f5a5078d9e6e14027a1abfebf4018634 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/Assets/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs deleted file mode 100644 index 0d69caf..0000000 --- a/Assets/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ /dev/null @@ -1,925 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.InteropServices; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Models; -using MCPForUnity.Editor.Services; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Clients -{ - /// Shared base class for MCP configurators. - public abstract class McpClientConfiguratorBase : IMcpClientConfigurator - { - protected readonly McpClient client; - - protected McpClientConfiguratorBase(McpClient client) - { - this.client = client; - } - - internal McpClient Client => client; - - public string Id => client.name.Replace(" ", "").ToLowerInvariant(); - public virtual string DisplayName => client.name; - public McpStatus Status => client.status; - public ConfiguredTransport ConfiguredTransport => client.configuredTransport; - public virtual bool SupportsAutoConfigure => true; - public virtual string GetConfigureActionLabel() => "Configure"; - - public abstract string GetConfigPath(); - public abstract McpStatus CheckStatus(bool attemptAutoRewrite = true); - public abstract void Configure(); - public abstract string GetManualSnippet(); - public abstract IList GetInstallationSteps(); - - protected string GetUvxPathOrError() - { - string uvx = MCPServiceLocator.Paths.GetUvxPath(); - if (string.IsNullOrEmpty(uvx)) - { - throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings."); - } - return uvx; - } - - protected string CurrentOsPath() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return client.windowsConfigPath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - return client.macConfigPath; - return client.linuxConfigPath; - } - - protected bool UrlsEqual(string a, string b) - { - if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) - { - return false; - } - - if (Uri.TryCreate(a.Trim(), UriKind.Absolute, out var uriA) && - Uri.TryCreate(b.Trim(), UriKind.Absolute, out var uriB)) - { - return Uri.Compare( - uriA, - uriB, - UriComponents.HttpRequestUrl, - UriFormat.SafeUnescaped, - StringComparison.OrdinalIgnoreCase) == 0; - } - - string Normalize(string value) => value.Trim().TrimEnd('/'); - return string.Equals(Normalize(a), Normalize(b), StringComparison.OrdinalIgnoreCase); - } - } - - /// JSON-file based configurator (Cursor, Windsurf, VS Code, etc.). - public abstract class JsonFileMcpConfigurator : McpClientConfiguratorBase - { - public JsonFileMcpConfigurator(McpClient client) : base(client) { } - - public override string GetConfigPath() => CurrentOsPath(); - - public override McpStatus CheckStatus(bool attemptAutoRewrite = true) - { - try - { - string path = GetConfigPath(); - if (!File.Exists(path)) - { - client.SetStatus(McpStatus.NotConfigured); - client.configuredTransport = Models.ConfiguredTransport.Unknown; - return client.status; - } - - string configJson = File.ReadAllText(path); - string[] args = null; - string configuredUrl = null; - bool configExists = false; - - if (client.IsVsCodeLayout) - { - var vsConfig = JsonConvert.DeserializeObject(configJson) as JObject; - if (vsConfig != null) - { - var unityToken = - vsConfig["servers"]?["unityMCP"] - ?? vsConfig["mcp"]?["servers"]?["unityMCP"]; - - if (unityToken is JObject unityObj) - { - configExists = true; - - var argsToken = unityObj["args"]; - if (argsToken is JArray) - { - args = argsToken.ToObject(); - } - - var urlToken = unityObj["url"] ?? unityObj["serverUrl"]; - if (urlToken != null && urlToken.Type != JTokenType.Null) - { - configuredUrl = urlToken.ToString(); - } - } - } - } - else - { - McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); - if (standardConfig?.mcpServers?.unityMCP != null) - { - args = standardConfig.mcpServers.unityMCP.args; - configuredUrl = standardConfig.mcpServers.unityMCP.url; - configExists = true; - } - } - - if (!configExists) - { - client.SetStatus(McpStatus.MissingConfig); - client.configuredTransport = Models.ConfiguredTransport.Unknown; - return client.status; - } - - // Determine and set the configured transport type - if (args != null && args.Length > 0) - { - client.configuredTransport = Models.ConfiguredTransport.Stdio; - } - else if (!string.IsNullOrEmpty(configuredUrl)) - { - // Distinguish HTTP Local from HTTP Remote by matching against both URLs - string localRpcUrl = HttpEndpointUtility.GetLocalMcpRpcUrl(); - string remoteRpcUrl = HttpEndpointUtility.GetRemoteMcpRpcUrl(); - if (!string.IsNullOrEmpty(remoteRpcUrl) && UrlsEqual(configuredUrl, remoteRpcUrl)) - { - client.configuredTransport = Models.ConfiguredTransport.HttpRemote; - } - else - { - client.configuredTransport = Models.ConfiguredTransport.Http; - } - } - else - { - client.configuredTransport = Models.ConfiguredTransport.Unknown; - } - - bool matches = false; - if (args != null && args.Length > 0) - { - string expectedUvxUrl = AssetPathUtility.GetMcpServerPackageSource(); - string configuredUvxUrl = McpConfigurationHelper.ExtractUvxUrl(args); - matches = !string.IsNullOrEmpty(configuredUvxUrl) && - McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl); - } - else if (!string.IsNullOrEmpty(configuredUrl)) - { - // Match against the active scope's URL - string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl(); - matches = UrlsEqual(configuredUrl, expectedUrl); - } - - if (matches) - { - client.SetStatus(McpStatus.Configured); - return client.status; - } - - if (attemptAutoRewrite) - { - var result = McpConfigurationHelper.WriteMcpConfiguration(path, client); - if (result == "Configured successfully") - { - client.SetStatus(McpStatus.Configured); - client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport(); - } - else - { - client.SetStatus(McpStatus.IncorrectPath); - } - } - else - { - client.SetStatus(McpStatus.IncorrectPath); - } - } - catch (Exception ex) - { - client.SetStatus(McpStatus.Error, ex.Message); - client.configuredTransport = Models.ConfiguredTransport.Unknown; - } - - return client.status; - } - - public override void Configure() - { - string path = GetConfigPath(); - McpConfigurationHelper.EnsureConfigDirectoryExists(path); - string result = McpConfigurationHelper.WriteMcpConfiguration(path, client); - if (result == "Configured successfully") - { - client.SetStatus(McpStatus.Configured); - client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport(); - } - else - { - throw new InvalidOperationException(result); - } - } - - public override string GetManualSnippet() - { - try - { - string uvx = GetUvxPathOrError(); - return ConfigJsonBuilder.BuildManualConfigJson(uvx, client); - } - catch (Exception ex) - { - var errorObj = new { error = ex.Message }; - return JsonConvert.SerializeObject(errorObj); - } - } - - public override IList GetInstallationSteps() => new List { "Configuration steps not available for this client." }; - } - - /// Codex (TOML) configurator. - public abstract class CodexMcpConfigurator : McpClientConfiguratorBase - { - public CodexMcpConfigurator(McpClient client) : base(client) { } - - public override string GetConfigPath() => CurrentOsPath(); - - public override McpStatus CheckStatus(bool attemptAutoRewrite = true) - { - try - { - string path = GetConfigPath(); - if (!File.Exists(path)) - { - client.SetStatus(McpStatus.NotConfigured); - client.configuredTransport = Models.ConfiguredTransport.Unknown; - return client.status; - } - - string toml = File.ReadAllText(path); - if (CodexConfigHelper.TryParseCodexServer(toml, out _, out var args, out var url)) - { - // Determine and set the configured transport type - if (!string.IsNullOrEmpty(url)) - { - // Distinguish HTTP Local from HTTP Remote - string remoteRpcUrl = HttpEndpointUtility.GetRemoteMcpRpcUrl(); - if (!string.IsNullOrEmpty(remoteRpcUrl) && UrlsEqual(url, remoteRpcUrl)) - { - client.configuredTransport = Models.ConfiguredTransport.HttpRemote; - } - else - { - client.configuredTransport = Models.ConfiguredTransport.Http; - } - } - else if (args != null && args.Length > 0) - { - client.configuredTransport = Models.ConfiguredTransport.Stdio; - } - else - { - client.configuredTransport = Models.ConfiguredTransport.Unknown; - } - - bool matches = false; - if (!string.IsNullOrEmpty(url)) - { - // Match against the active scope's URL - matches = UrlsEqual(url, HttpEndpointUtility.GetMcpRpcUrl()); - } - else if (args != null && args.Length > 0) - { - string expected = AssetPathUtility.GetMcpServerPackageSource(); - string configured = McpConfigurationHelper.ExtractUvxUrl(args); - matches = !string.IsNullOrEmpty(configured) && - McpConfigurationHelper.PathsEqual(configured, expected); - } - - if (matches) - { - client.SetStatus(McpStatus.Configured); - return client.status; - } - } - else - { - client.configuredTransport = Models.ConfiguredTransport.Unknown; - } - - if (attemptAutoRewrite) - { - string result = McpConfigurationHelper.ConfigureCodexClient(path, client); - if (result == "Configured successfully") - { - client.SetStatus(McpStatus.Configured); - client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport(); - } - else - { - client.SetStatus(McpStatus.IncorrectPath); - } - } - else - { - client.SetStatus(McpStatus.IncorrectPath); - } - } - catch (Exception ex) - { - client.SetStatus(McpStatus.Error, ex.Message); - client.configuredTransport = Models.ConfiguredTransport.Unknown; - } - - return client.status; - } - - public override void Configure() - { - string path = GetConfigPath(); - McpConfigurationHelper.EnsureConfigDirectoryExists(path); - string result = McpConfigurationHelper.ConfigureCodexClient(path, client); - if (result == "Configured successfully") - { - client.SetStatus(McpStatus.Configured); - client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport(); - } - else - { - throw new InvalidOperationException(result); - } - } - - public override string GetManualSnippet() - { - try - { - string uvx = GetUvxPathOrError(); - return CodexConfigHelper.BuildCodexServerBlock(uvx); - } - catch (Exception ex) - { - return $"# error: {ex.Message}"; - } - } - - public override IList GetInstallationSteps() => new List - { - "Run 'codex config edit' or open the config path", - "Paste the TOML", - "Save and restart Codex" - }; - } - - /// CLI-based configurator (Claude Code). - public abstract class ClaudeCliMcpConfigurator : McpClientConfiguratorBase - { - public ClaudeCliMcpConfigurator(McpClient client) : base(client) { } - - public override bool SupportsAutoConfigure => true; - public override string GetConfigureActionLabel() => client.status == McpStatus.Configured ? "Unregister" : "Register"; - - public override string GetConfigPath() => "Managed via Claude CLI"; - - /// - /// Checks the Claude CLI registration status. - /// MUST be called from the main Unity thread due to EditorPrefs and Application.dataPath access. - /// - public override McpStatus CheckStatus(bool attemptAutoRewrite = true) - { - // Capture main-thread-only values before delegating to thread-safe method - string projectDir = Path.GetDirectoryName(Application.dataPath); - bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport; - // Resolve claudePath on the main thread (EditorPrefs access) - string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath(); - return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, attemptAutoRewrite); - } - - /// - /// Internal thread-safe version of CheckStatus. - /// Can be called from background threads because all main-thread-only values are passed as parameters. - /// projectDir, useHttpTransport, and claudePath are REQUIRED (non-nullable) to enforce thread safety at compile time. - /// NOTE: attemptAutoRewrite is NOT fully thread-safe because Configure() requires the main thread. - /// When called from a background thread, pass attemptAutoRewrite=false and handle re-registration - /// on the main thread based on the returned status. - /// - internal McpStatus CheckStatusWithProjectDir(string projectDir, bool useHttpTransport, string claudePath, bool attemptAutoRewrite = false) - { - try - { - if (string.IsNullOrEmpty(claudePath)) - { - client.SetStatus(McpStatus.NotConfigured, "Claude CLI not found"); - client.configuredTransport = Models.ConfiguredTransport.Unknown; - return client.status; - } - - // projectDir is required - no fallback to Application.dataPath - if (string.IsNullOrEmpty(projectDir)) - { - throw new ArgumentNullException(nameof(projectDir), "Project directory must be provided for thread-safe execution"); - } - - string pathPrepend = null; - if (Application.platform == RuntimePlatform.OSXEditor) - { - pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; - } - else if (Application.platform == RuntimePlatform.LinuxEditor) - { - pathPrepend = "/usr/local/bin:/usr/bin:/bin"; - } - - try - { - string claudeDir = Path.GetDirectoryName(claudePath); - if (!string.IsNullOrEmpty(claudeDir)) - { - pathPrepend = string.IsNullOrEmpty(pathPrepend) - ? claudeDir - : $"{claudeDir}:{pathPrepend}"; - } - } - catch { } - - // Check if UnityMCP exists (handles both "UnityMCP" and legacy "unityMCP") - if (ExecPath.TryRun(claudePath, "mcp list", projectDir, out var listStdout, out var listStderr, 10000, pathPrepend)) - { - if (!string.IsNullOrEmpty(listStdout) && listStdout.IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0) - { - // UnityMCP is registered - now verify transport mode matches - // useHttpTransport parameter is required (non-nullable) to ensure thread safety - bool currentUseHttp = useHttpTransport; - - // Get detailed info about the registration to check transport type - // Try both "UnityMCP" and "unityMCP" (legacy naming) - string getStdout = null, getStderr = null; - bool gotInfo = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out getStdout, out getStderr, 7000, pathPrepend) - || ExecPath.TryRun(claudePath, "mcp get unityMCP", projectDir, out getStdout, out getStderr, 7000, pathPrepend); - if (gotInfo) - { - // Parse the output to determine registered transport mode - // The CLI output format contains "Type: http" or "Type: stdio" - bool registeredWithHttp = getStdout.Contains("Type: http", StringComparison.OrdinalIgnoreCase); - bool registeredWithStdio = getStdout.Contains("Type: stdio", StringComparison.OrdinalIgnoreCase); - - // Set the configured transport based on what we detected - // For HTTP, we can't distinguish local/remote from CLI output alone, - // so infer from the current scope setting when HTTP is detected. - if (registeredWithHttp) - { - client.configuredTransport = HttpEndpointUtility.IsRemoteScope() - ? Models.ConfiguredTransport.HttpRemote - : Models.ConfiguredTransport.Http; - } - else if (registeredWithStdio) - { - client.configuredTransport = Models.ConfiguredTransport.Stdio; - } - else - { - client.configuredTransport = Models.ConfiguredTransport.Unknown; - } - - // Check for transport mismatch (3-way: Stdio, Http, HttpRemote) - bool hasTransportMismatch = (currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp); - - // For stdio transport, also check package version - bool hasVersionMismatch = false; - string configuredPackageSource = null; - string expectedPackageSource = null; - if (registeredWithStdio) - { - expectedPackageSource = AssetPathUtility.GetMcpServerPackageSource(); - configuredPackageSource = ExtractPackageSourceFromCliOutput(getStdout); - hasVersionMismatch = !string.IsNullOrEmpty(configuredPackageSource) && - !string.Equals(configuredPackageSource, expectedPackageSource, StringComparison.OrdinalIgnoreCase); - } - - // If there's any mismatch and auto-rewrite is enabled, re-register - if (hasTransportMismatch || hasVersionMismatch) - { - // Configure() requires main thread (accesses EditorPrefs, Application.dataPath) - // Only attempt auto-rewrite if we're on the main thread - bool isMainThread = System.Threading.Thread.CurrentThread.ManagedThreadId == 1; - if (attemptAutoRewrite && isMainThread) - { - string reason = hasTransportMismatch - ? $"Transport mismatch (registered: {(registeredWithHttp ? "HTTP" : "stdio")}, expected: {(currentUseHttp ? "HTTP" : "stdio")})" - : $"Package version mismatch (registered: {configuredPackageSource}, expected: {expectedPackageSource})"; - McpLog.Info($"{reason}. Re-registering..."); - try - { - // Force re-register by ensuring status is not Configured (which would toggle to Unregister) - client.SetStatus(McpStatus.IncorrectPath); - Configure(); - return client.status; - } - catch (Exception ex) - { - McpLog.Warn($"Auto-reregister failed: {ex.Message}"); - client.SetStatus(McpStatus.IncorrectPath, $"Configuration mismatch. Click Configure to re-register."); - return client.status; - } - } - else - { - if (hasTransportMismatch) - { - string errorMsg = $"Transport mismatch: Claude Code is registered with {(registeredWithHttp ? "HTTP" : "stdio")} but current setting is {(currentUseHttp ? "HTTP" : "stdio")}. Click Configure to re-register."; - client.SetStatus(McpStatus.Error, errorMsg); - McpLog.Warn(errorMsg); - } - else - { - client.SetStatus(McpStatus.IncorrectPath, $"Package version mismatch: registered with '{configuredPackageSource}' but current version is '{expectedPackageSource}'."); - } - return client.status; - } - } - } - - client.SetStatus(McpStatus.Configured); - return client.status; - } - } - - client.SetStatus(McpStatus.NotConfigured); - client.configuredTransport = Models.ConfiguredTransport.Unknown; - } - catch (Exception ex) - { - client.SetStatus(McpStatus.Error, ex.Message); - client.configuredTransport = Models.ConfiguredTransport.Unknown; - } - - return client.status; - } - - public override void Configure() - { - if (client.status == McpStatus.Configured) - { - Unregister(); - } - else - { - Register(); - } - } - - /// - /// Thread-safe version of Configure that uses pre-captured main-thread values. - /// All parameters must be captured on the main thread before calling this method. - /// - public void ConfigureWithCapturedValues( - string projectDir, string claudePath, string pathPrepend, - bool useHttpTransport, string httpUrl, - string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh, - string apiKey, - Models.ConfiguredTransport serverTransport) - { - if (client.status == McpStatus.Configured) - { - UnregisterWithCapturedValues(projectDir, claudePath, pathPrepend); - } - else - { - RegisterWithCapturedValues(projectDir, claudePath, pathPrepend, - useHttpTransport, httpUrl, uvxPath, gitUrl, packageName, shouldForceRefresh, - apiKey, serverTransport); - } - } - - /// - /// Thread-safe registration using pre-captured values. - /// - private void RegisterWithCapturedValues( - string projectDir, string claudePath, string pathPrepend, - bool useHttpTransport, string httpUrl, - string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh, - string apiKey, - Models.ConfiguredTransport serverTransport) - { - if (string.IsNullOrEmpty(claudePath)) - { - throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); - } - - string args; - if (useHttpTransport) - { - // Only include API key header for remote-hosted mode - if (serverTransport == Models.ConfiguredTransport.HttpRemote && !string.IsNullOrEmpty(apiKey)) - { - string safeKey = SanitizeShellHeaderValue(apiKey); - args = $"mcp add --transport http UnityMCP {httpUrl} --header \"{AuthConstants.ApiKeyHeader}: {safeKey}\""; - } - else - { - args = $"mcp add --transport http UnityMCP {httpUrl}"; - } - } - else - { - // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead - string devFlags = shouldForceRefresh ? "--no-cache --refresh " : string.Empty; - args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}"; - } - - // Remove any existing registrations - handle both "UnityMCP" and "unityMCP" (legacy) - McpLog.Info("Removing any existing UnityMCP registrations before adding..."); - ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend); - ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend); - - // Now add the registration - if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend)) - { - throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}"); - } - - McpLog.Info($"Successfully registered with Claude Code using {(useHttpTransport ? "HTTP" : "stdio")} transport."); - client.SetStatus(McpStatus.Configured); - client.configuredTransport = serverTransport; - } - - /// - /// Thread-safe unregistration using pre-captured values. - /// - private void UnregisterWithCapturedValues(string projectDir, string claudePath, string pathPrepend) - { - if (string.IsNullOrEmpty(claudePath)) - { - throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); - } - - // Remove both "UnityMCP" and "unityMCP" (legacy naming) - McpLog.Info("Removing all UnityMCP registrations..."); - ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend); - ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend); - - McpLog.Info("MCP server successfully unregistered from Claude Code."); - client.SetStatus(McpStatus.NotConfigured); - client.configuredTransport = Models.ConfiguredTransport.Unknown; - } - - private void Register() - { - var pathService = MCPServiceLocator.Paths; - string claudePath = pathService.GetClaudeCliPath(); - if (string.IsNullOrEmpty(claudePath)) - { - throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); - } - - bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport; - - string args; - if (useHttpTransport) - { - string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); - // Only include API key header for remote-hosted mode - if (HttpEndpointUtility.IsRemoteScope()) - { - string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); - if (!string.IsNullOrEmpty(apiKey)) - { - string safeKey = SanitizeShellHeaderValue(apiKey); - args = $"mcp add --transport http UnityMCP {httpUrl} --header \"{AuthConstants.ApiKeyHeader}: {safeKey}\""; - } - else - { - args = $"mcp add --transport http UnityMCP {httpUrl}"; - } - } - else - { - args = $"mcp add --transport http UnityMCP {httpUrl}"; - } - } - else - { - var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts(); - // Use central helper that checks both DevModeForceServerRefresh AND local path detection. - // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead - string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty; - args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}"; - } - - string projectDir = Path.GetDirectoryName(Application.dataPath); - - string pathPrepend = null; - if (Application.platform == RuntimePlatform.OSXEditor) - { - pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; - } - else if (Application.platform == RuntimePlatform.LinuxEditor) - { - pathPrepend = "/usr/local/bin:/usr/bin:/bin"; - } - - try - { - string claudeDir = Path.GetDirectoryName(claudePath); - if (!string.IsNullOrEmpty(claudeDir)) - { - pathPrepend = string.IsNullOrEmpty(pathPrepend) - ? claudeDir - : $"{claudeDir}:{pathPrepend}"; - } - } - catch { } - - // Remove any existing registrations - handle both "UnityMCP" and "unityMCP" (legacy) - McpLog.Info("Removing any existing UnityMCP registrations before adding..."); - ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend); - ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend); - - // Now add the registration with the current transport mode - if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend)) - { - throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}"); - } - - McpLog.Info($"Successfully registered with Claude Code using {(useHttpTransport ? "HTTP" : "stdio")} transport."); - - // Set status to Configured immediately after successful registration - // The UI will trigger an async verification check separately to avoid blocking - client.SetStatus(McpStatus.Configured); - client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport(); - } - - private void Unregister() - { - var pathService = MCPServiceLocator.Paths; - string claudePath = pathService.GetClaudeCliPath(); - - if (string.IsNullOrEmpty(claudePath)) - { - throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); - } - - string projectDir = Path.GetDirectoryName(Application.dataPath); - string pathPrepend = null; - if (Application.platform == RuntimePlatform.OSXEditor) - { - pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; - } - else if (Application.platform == RuntimePlatform.LinuxEditor) - { - pathPrepend = "/usr/local/bin:/usr/bin:/bin"; - } - - // Remove both "UnityMCP" and "unityMCP" (legacy naming) - McpLog.Info("Removing all UnityMCP registrations..."); - ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend); - ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend); - - McpLog.Info("MCP server successfully unregistered from Claude Code."); - client.SetStatus(McpStatus.NotConfigured); - client.configuredTransport = Models.ConfiguredTransport.Unknown; - } - - public override string GetManualSnippet() - { - string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); - bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport; - - if (useHttpTransport) - { - string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); - // Only include API key header for remote-hosted mode - string headerArg = ""; - if (HttpEndpointUtility.IsRemoteScope()) - { - string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); - headerArg = !string.IsNullOrEmpty(apiKey) ? $" --header \"{AuthConstants.ApiKeyHeader}: {SanitizeShellHeaderValue(apiKey)}\"" : ""; - } - return "# Register the MCP server with Claude Code:\n" + - $"claude mcp add --transport http UnityMCP {httpUrl}{headerArg}\n\n" + - "# Unregister the MCP server:\n" + - "claude mcp remove UnityMCP\n\n" + - "# List registered servers:\n" + - "claude mcp list"; - } - - if (string.IsNullOrEmpty(uvxPath)) - { - return "# Error: Configuration not available - check paths in Advanced Settings"; - } - - string packageSource = AssetPathUtility.GetMcpServerPackageSource(); - // Use central helper that checks both DevModeForceServerRefresh AND local path detection. - // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead - string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty; - - return "# Register the MCP server with Claude Code:\n" + - $"claude mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{packageSource}\" mcp-for-unity\n\n" + - "# Unregister the MCP server:\n" + - "claude mcp remove UnityMCP\n\n" + - "# List registered servers:\n" + - "claude mcp list"; - } - - public override IList GetInstallationSteps() => new List - { - "Ensure Claude CLI is installed", - "Use Register to add UnityMCP (or run claude mcp add UnityMCP)", - "Restart Claude Code" - }; - - /// - /// Sanitizes a value for safe inclusion inside a double-quoted shell argument. - /// Escapes characters that are special within double quotes (", \, `, $, !) - /// to prevent shell injection or argument splitting. - /// - private static string SanitizeShellHeaderValue(string value) - { - if (string.IsNullOrEmpty(value)) - return value; - - var sb = new System.Text.StringBuilder(value.Length); - foreach (char c in value) - { - switch (c) - { - case '"': - case '\\': - case '`': - case '$': - case '!': - sb.Append('\\'); - sb.Append(c); - break; - default: - sb.Append(c); - break; - } - } - return sb.ToString(); - } - - /// - /// Extracts the package source (--from argument value) from claude mcp get output. - /// The output format includes args like: --from "mcpforunityserver==9.0.1" - /// - private static string ExtractPackageSourceFromCliOutput(string cliOutput) - { - if (string.IsNullOrEmpty(cliOutput)) - return null; - - // Look for --from followed by the package source - // The CLI output may have it quoted or unquoted - int fromIndex = cliOutput.IndexOf("--from", StringComparison.OrdinalIgnoreCase); - if (fromIndex < 0) - return null; - - // Move past "--from" and any whitespace - int startIndex = fromIndex + 6; - while (startIndex < cliOutput.Length && char.IsWhiteSpace(cliOutput[startIndex])) - startIndex++; - - if (startIndex >= cliOutput.Length) - return null; - - // Check if value is quoted - char quoteChar = cliOutput[startIndex]; - if (quoteChar == '"' || quoteChar == '\'') - { - startIndex++; - int endIndex = cliOutput.IndexOf(quoteChar, startIndex); - if (endIndex > startIndex) - return cliOutput.Substring(startIndex, endIndex - startIndex); - } - else - { - // Unquoted - read until whitespace or end of line - int endIndex = startIndex; - while (endIndex < cliOutput.Length && !char.IsWhiteSpace(cliOutput[endIndex])) - endIndex++; - - if (endIndex > startIndex) - return cliOutput.Substring(startIndex, endIndex - startIndex); - } - - return null; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs.meta b/Assets/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs.meta deleted file mode 100644 index da94d06..0000000 --- a/Assets/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 8d408fd7733cb4a1eb80f785307db2ff -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Clients/McpClientRegistry.cs b/Assets/MCPForUnity/Editor/Clients/McpClientRegistry.cs deleted file mode 100644 index 57e4dc1..0000000 --- a/Assets/MCPForUnity/Editor/Clients/McpClientRegistry.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MCPForUnity.Editor.Helpers; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Clients -{ - /// - /// Central registry that auto-discovers configurators via TypeCache. - /// - public static class McpClientRegistry - { - private static List cached; - - public static IReadOnlyList All - { - get - { - if (cached == null) - { - cached = BuildRegistry(); - } - return cached; - } - } - - private static List BuildRegistry() - { - var configurators = new List(); - - foreach (var type in TypeCache.GetTypesDerivedFrom()) - { - if (type.IsAbstract || !type.IsClass || !type.IsPublic) - continue; - - // Require a public parameterless constructor - if (type.GetConstructor(Type.EmptyTypes) == null) - continue; - - try - { - if (Activator.CreateInstance(type) is IMcpClientConfigurator instance) - { - configurators.Add(instance); - } - } - catch (Exception ex) - { - McpLog.Warn($"UnityMCP: Failed to instantiate configurator {type.Name}: {ex.Message}"); - } - } - - // Alphabetical order by display name - configurators = configurators.OrderBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase).ToList(); - return configurators; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Clients/McpClientRegistry.cs.meta b/Assets/MCPForUnity/Editor/Clients/McpClientRegistry.cs.meta deleted file mode 100644 index 24c57ea..0000000 --- a/Assets/MCPForUnity/Editor/Clients/McpClientRegistry.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4ce08555f995e4e848a826c63f18cb35 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Clients/McpClientRegistry.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Constants.meta b/Assets/MCPForUnity/Editor/Constants.meta deleted file mode 100644 index 7c23235..0000000 --- a/Assets/MCPForUnity/Editor/Constants.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: f7e009cbf3e74f6c987331c2b438ec59 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Constants/AuthConstants.cs b/Assets/MCPForUnity/Editor/Constants/AuthConstants.cs deleted file mode 100644 index 76579e6..0000000 --- a/Assets/MCPForUnity/Editor/Constants/AuthConstants.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MCPForUnity.Editor.Constants -{ - /// - /// Protocol-level constants for API key authentication. - /// - internal static class AuthConstants - { - internal const string ApiKeyHeader = "X-API-Key"; - } -} diff --git a/Assets/MCPForUnity/Editor/Constants/AuthConstants.cs.meta b/Assets/MCPForUnity/Editor/Constants/AuthConstants.cs.meta deleted file mode 100644 index 2c43b99..0000000 --- a/Assets/MCPForUnity/Editor/Constants/AuthConstants.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 96844bc39e9a94cf18b18f8127f3854f -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Constants/AuthConstants.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/Assets/MCPForUnity/Editor/Constants/EditorPrefKeys.cs deleted file mode 100644 index 488bf39..0000000 --- a/Assets/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ /dev/null @@ -1,66 +0,0 @@ -namespace MCPForUnity.Editor.Constants -{ - /// - /// Centralized list of EditorPrefs keys used by the MCP for Unity package. - /// Keeping them in one place avoids typos and simplifies migrations. - /// - internal static class EditorPrefKeys - { - internal const string UseHttpTransport = "MCPForUnity.UseHttpTransport"; - internal const string HttpTransportScope = "MCPForUnity.HttpTransportScope"; // "local" | "remote" - internal const string LastLocalHttpServerPid = "MCPForUnity.LocalHttpServer.LastPid"; - internal const string LastLocalHttpServerPort = "MCPForUnity.LocalHttpServer.LastPort"; - internal const string LastLocalHttpServerStartedUtc = "MCPForUnity.LocalHttpServer.LastStartedUtc"; - internal const string LastLocalHttpServerPidArgsHash = "MCPForUnity.LocalHttpServer.LastPidArgsHash"; - internal const string LastLocalHttpServerPidFilePath = "MCPForUnity.LocalHttpServer.LastPidFilePath"; - internal const string LastLocalHttpServerInstanceToken = "MCPForUnity.LocalHttpServer.LastInstanceToken"; - internal const string DebugLogs = "MCPForUnity.DebugLogs"; - internal const string ValidationLevel = "MCPForUnity.ValidationLevel"; - internal const string UnitySocketPort = "MCPForUnity.UnitySocketPort"; - internal const string ResumeHttpAfterReload = "MCPForUnity.ResumeHttpAfterReload"; - internal const string ResumeStdioAfterReload = "MCPForUnity.ResumeStdioAfterReload"; - - internal const string UvxPathOverride = "MCPForUnity.UvxPath"; - internal const string ClaudeCliPathOverride = "MCPForUnity.ClaudeCliPath"; - - internal const string HttpBaseUrl = "MCPForUnity.HttpUrl"; - internal const string HttpRemoteBaseUrl = "MCPForUnity.HttpRemoteUrl"; - internal const string SessionId = "MCPForUnity.SessionId"; - internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl"; - internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride"; - internal const string DevModeForceServerRefresh = "MCPForUnity.DevModeForceServerRefresh"; - internal const string UseBetaServer = "MCPForUnity.UseBetaServer"; - internal const string ProjectScopedToolsLocalHttp = "MCPForUnity.ProjectScopedTools.LocalHttp"; - - internal const string PackageDeploySourcePath = "MCPForUnity.PackageDeploy.SourcePath"; - internal const string PackageDeployLastBackupPath = "MCPForUnity.PackageDeploy.LastBackupPath"; - internal const string PackageDeployLastTargetPath = "MCPForUnity.PackageDeploy.LastTargetPath"; - internal const string PackageDeployLastSourcePath = "MCPForUnity.PackageDeploy.LastSourcePath"; - - internal const string ServerSrc = "MCPForUnity.ServerSrc"; - internal const string UseEmbeddedServer = "MCPForUnity.UseEmbeddedServer"; - internal const string LockCursorConfig = "MCPForUnity.LockCursorConfig"; - internal const string AutoRegisterEnabled = "MCPForUnity.AutoRegisterEnabled"; - internal const string ToolEnabledPrefix = "MCPForUnity.ToolEnabled."; - internal const string ToolFoldoutStatePrefix = "MCPForUnity.ToolFoldout."; - internal const string ResourceEnabledPrefix = "MCPForUnity.ResourceEnabled."; - internal const string ResourceFoldoutStatePrefix = "MCPForUnity.ResourceFoldout."; - internal const string EditorWindowActivePanel = "MCPForUnity.EditorWindow.ActivePanel"; - - internal const string SetupCompleted = "MCPForUnity.SetupCompleted"; - internal const string SetupDismissed = "MCPForUnity.SetupDismissed"; - - internal const string CustomToolRegistrationEnabled = "MCPForUnity.CustomToolRegistrationEnabled"; - - internal const string LastUpdateCheck = "MCPForUnity.LastUpdateCheck"; - internal const string LatestKnownVersion = "MCPForUnity.LatestKnownVersion"; - internal const string LastAssetStoreUpdateCheck = "MCPForUnity.LastAssetStoreUpdateCheck"; - internal const string LatestKnownAssetStoreVersion = "MCPForUnity.LatestKnownAssetStoreVersion"; - internal const string LastStdIoUpgradeVersion = "MCPForUnity.LastStdIoUpgradeVersion"; - - internal const string TelemetryDisabled = "MCPForUnity.TelemetryDisabled"; - internal const string CustomerUuid = "MCPForUnity.CustomerUUID"; - - internal const string ApiKey = "MCPForUnity.ApiKey"; - } -} diff --git a/Assets/MCPForUnity/Editor/Constants/EditorPrefKeys.cs.meta b/Assets/MCPForUnity/Editor/Constants/EditorPrefKeys.cs.meta deleted file mode 100644 index 3f69bac..0000000 --- a/Assets/MCPForUnity/Editor/Constants/EditorPrefKeys.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 7317786cfb9304b0db20ca73a774b9fa -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Constants/EditorPrefKeys.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Constants/HealthStatus.cs b/Assets/MCPForUnity/Editor/Constants/HealthStatus.cs deleted file mode 100644 index 7506caa..0000000 --- a/Assets/MCPForUnity/Editor/Constants/HealthStatus.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MCPForUnity.Editor.Constants -{ - /// - /// Constants for health check status values. - /// Used for coordinating health state between Connection and Advanced sections. - /// - public static class HealthStatus - { - public const string Unknown = "Unknown"; - public const string Healthy = "Healthy"; - public const string PingFailed = "Ping Failed"; - public const string Unhealthy = "Unhealthy"; - } -} diff --git a/Assets/MCPForUnity/Editor/Constants/HealthStatus.cs.meta b/Assets/MCPForUnity/Editor/Constants/HealthStatus.cs.meta deleted file mode 100644 index 7db5589..0000000 --- a/Assets/MCPForUnity/Editor/Constants/HealthStatus.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: c15ed2426f43860479f1b8a99a343d16 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Constants/HealthStatus.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Dependencies.meta b/Assets/MCPForUnity/Editor/Dependencies.meta deleted file mode 100644 index 77685d1..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 221a4d6e595be6897a5b17b77aedd4d0 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Dependencies/DependencyManager.cs b/Assets/MCPForUnity/Editor/Dependencies/DependencyManager.cs deleted file mode 100644 index c3802c4..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/DependencyManager.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using MCPForUnity.Editor.Dependencies.Models; -using MCPForUnity.Editor.Dependencies.PlatformDetectors; -using MCPForUnity.Editor.Helpers; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Dependencies -{ - /// - /// Main orchestrator for dependency validation and management - /// - public static class DependencyManager - { - private static readonly List _detectors = new List - { - new WindowsPlatformDetector(), - new MacOSPlatformDetector(), - new LinuxPlatformDetector() - }; - - private static IPlatformDetector _currentDetector; - - /// - /// Get the platform detector for the current operating system - /// - public static IPlatformDetector GetCurrentPlatformDetector() - { - if (_currentDetector == null) - { - _currentDetector = _detectors.FirstOrDefault(d => d.CanDetect); - if (_currentDetector == null) - { - throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}"); - } - } - return _currentDetector; - } - - /// - /// Perform a comprehensive dependency check - /// - public static DependencyCheckResult CheckAllDependencies() - { - var result = new DependencyCheckResult(); - - try - { - var detector = GetCurrentPlatformDetector(); - McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false); - - // Check Python - var pythonStatus = detector.DetectPython(); - result.Dependencies.Add(pythonStatus); - - // Check uv - var uvStatus = detector.DetectUv(); - result.Dependencies.Add(uvStatus); - - // Generate summary and recommendations - result.GenerateSummary(); - GenerateRecommendations(result, detector); - - McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false); - } - catch (Exception ex) - { - McpLog.Error($"Error during dependency check: {ex.Message}"); - result.Summary = $"Dependency check failed: {ex.Message}"; - result.IsSystemReady = false; - } - - return result; - } - - /// - /// Get installation recommendations for the current platform - /// - public static string GetInstallationRecommendations() - { - try - { - var detector = GetCurrentPlatformDetector(); - return detector.GetInstallationRecommendations(); - } - catch (Exception ex) - { - return $"Error getting installation recommendations: {ex.Message}"; - } - } - - /// - /// Get platform-specific installation URLs - /// - public static (string pythonUrl, string uvUrl) GetInstallationUrls() - { - try - { - var detector = GetCurrentPlatformDetector(); - return (detector.GetPythonInstallUrl(), detector.GetUvInstallUrl()); - } - catch - { - return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/"); - } - } - - private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector) - { - var missing = result.GetMissingDependencies(); - - if (missing.Count == 0) - { - result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity."); - return; - } - - foreach (var dep in missing) - { - if (dep.Name == "Python") - { - result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}"); - } - else if (dep.Name == "uv Package Manager") - { - result.RecommendedActions.Add($"Install uv package manager from: {detector.GetUvInstallUrl()}"); - } - else if (dep.Name == "MCP Server") - { - result.RecommendedActions.Add("MCP Server will be installed automatically when needed."); - } - } - - if (result.GetMissingRequired().Count > 0) - { - result.RecommendedActions.Add("Use the Setup Window (Window > MCP for Unity > Local Setup Window) for guided installation."); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Dependencies/DependencyManager.cs.meta b/Assets/MCPForUnity/Editor/Dependencies/DependencyManager.cs.meta deleted file mode 100644 index dc58085..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/DependencyManager.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4a6d2236d370b4f1db4d0e3d3ce0dcac -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Dependencies/DependencyManager.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Dependencies/Models.meta b/Assets/MCPForUnity/Editor/Dependencies/Models.meta deleted file mode 100644 index 1e05b9d..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/Models.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 4c0f2e87395b4c6c9df8c21b6d0fae13 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs b/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs deleted file mode 100644 index 5dd2eda..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace MCPForUnity.Editor.Dependencies.Models -{ - /// - /// Result of a comprehensive dependency check - /// - [Serializable] - public class DependencyCheckResult - { - /// - /// List of all dependency statuses checked - /// - public List Dependencies { get; set; } - - /// - /// Overall system readiness for MCP operations - /// - public bool IsSystemReady { get; set; } - - /// - /// Whether all required dependencies are available - /// - public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false; - - /// - /// Whether any optional dependencies are missing - /// - public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false; - - /// - /// Summary message about the dependency state - /// - public string Summary { get; set; } - - /// - /// Recommended next steps for the user - /// - public List RecommendedActions { get; set; } - - /// - /// Timestamp when this check was performed - /// - public DateTime CheckedAt { get; set; } - - public DependencyCheckResult() - { - Dependencies = new List(); - RecommendedActions = new List(); - CheckedAt = DateTime.UtcNow; - } - - /// - /// Get dependencies by availability status - /// - public List GetMissingDependencies() - { - return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List(); - } - - /// - /// Get missing required dependencies - /// - public List GetMissingRequired() - { - return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List(); - } - - /// - /// Generate a user-friendly summary of the dependency state - /// - public void GenerateSummary() - { - var missing = GetMissingDependencies(); - var missingRequired = GetMissingRequired(); - - if (missing.Count == 0) - { - Summary = "All dependencies are available and ready."; - IsSystemReady = true; - } - else if (missingRequired.Count == 0) - { - Summary = $"System is ready. {missing.Count} optional dependencies are missing."; - IsSystemReady = true; - } - else - { - Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing."; - IsSystemReady = false; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs.meta b/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs.meta deleted file mode 100644 index cd0f4df..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: f6df82faa423f4e9ebb13a3dcee8ba19 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Dependencies/Models/DependencyCheckResult.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs b/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs deleted file mode 100644 index e755eca..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; - -namespace MCPForUnity.Editor.Dependencies.Models -{ - /// - /// Represents the status of a dependency check - /// - [Serializable] - public class DependencyStatus - { - /// - /// Name of the dependency being checked - /// - public string Name { get; set; } - - /// - /// Whether the dependency is available and functional - /// - public bool IsAvailable { get; set; } - - /// - /// Version information if available - /// - public string Version { get; set; } - - /// - /// Path to the dependency executable/installation - /// - public string Path { get; set; } - - /// - /// Additional details about the dependency status - /// - public string Details { get; set; } - - /// - /// Error message if dependency check failed - /// - public string ErrorMessage { get; set; } - - /// - /// Whether this dependency is required for basic functionality - /// - public bool IsRequired { get; set; } - - /// - /// Suggested installation method or URL - /// - public string InstallationHint { get; set; } - - public DependencyStatus(string name, bool isRequired = true) - { - Name = name; - IsRequired = isRequired; - IsAvailable = false; - } - - public override string ToString() - { - var status = IsAvailable ? "✓" : "✗"; - var version = !string.IsNullOrEmpty(Version) ? $" ({Version})" : ""; - return $"{status} {Name}{version}"; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs.meta b/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs.meta deleted file mode 100644 index 0fba554..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: ddeeeca2f876f4083a84417404175199 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Dependencies/Models/DependencyStatus.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors.meta b/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors.meta deleted file mode 100644 index 8c86307..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: bdbaced669d14798a4ceeebfbff2b22c -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs b/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs deleted file mode 100644 index 3231105..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs +++ /dev/null @@ -1,45 +0,0 @@ -using MCPForUnity.Editor.Dependencies.Models; - -namespace MCPForUnity.Editor.Dependencies.PlatformDetectors -{ - /// - /// Interface for platform-specific dependency detection - /// - public interface IPlatformDetector - { - /// - /// Platform name this detector handles - /// - string PlatformName { get; } - - /// - /// Whether this detector can run on the current platform - /// - bool CanDetect { get; } - - /// - /// Detect Python installation on this platform - /// - DependencyStatus DetectPython(); - - /// - /// Detect uv package manager on this platform - /// - DependencyStatus DetectUv(); - - /// - /// Get platform-specific installation recommendations - /// - string GetInstallationRecommendations(); - - /// - /// Get platform-specific Python installation URL - /// - string GetPythonInstallUrl(); - - /// - /// Get platform-specific uv installation URL - /// - string GetUvInstallUrl(); - } -} diff --git a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta b/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta deleted file mode 100644 index 7585b1e..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 67d73d0e8caef4e60942f4419c6b76bf -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs b/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs deleted file mode 100644 index be9db17..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Dependencies.Models; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; - -namespace MCPForUnity.Editor.Dependencies.PlatformDetectors -{ - /// - /// Linux-specific dependency detection - /// - public class LinuxPlatformDetector : PlatformDetectorBase - { - public override string PlatformName => "Linux"; - - public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - - public override DependencyStatus DetectPython() - { - var status = new DependencyStatus("Python", isRequired: true) - { - InstallationHint = GetPythonInstallUrl() - }; - - try - { - // Try running python directly first - if (TryValidatePython("python3", out string version, out string fullPath) || - TryValidatePython("python", out version, out fullPath)) - { - status.IsAvailable = true; - status.Version = version; - status.Path = fullPath; - status.Details = $"Found Python {version} in PATH"; - return status; - } - - // Fallback: try 'which' command - if (TryFindInPath("python3", out string pathResult) || - TryFindInPath("python", out pathResult)) - { - if (TryValidatePython(pathResult, out version, out fullPath)) - { - status.IsAvailable = true; - status.Version = version; - status.Path = fullPath; - status.Details = $"Found Python {version} in PATH"; - return status; - } - } - - status.ErrorMessage = "Python not found in PATH"; - status.Details = "Install Python 3.10+ and ensure it's added to PATH."; - } - catch (Exception ex) - { - status.ErrorMessage = $"Error detecting Python: {ex.Message}"; - } - - return status; - } - - public override string GetPythonInstallUrl() - { - return "https://www.python.org/downloads/source/"; - } - - public override string GetUvInstallUrl() - { - return "https://docs.astral.sh/uv/getting-started/installation/#linux"; - } - - public override string GetInstallationRecommendations() - { - return @"Linux Installation Recommendations: - -1. Python: Install via package manager or pyenv - - Ubuntu/Debian: sudo apt install python3 python3-pip - - Fedora/RHEL: sudo dnf install python3 python3-pip - - Arch: sudo pacman -S python python-pip - - Or use pyenv: https://github.com/pyenv/pyenv - -2. uv Package Manager: Install via curl - - Run: curl -LsSf https://astral.sh/uv/install.sh | sh - - Or download from: https://github.com/astral-sh/uv/releases - -3. MCP Server: Will be installed automatically by MCP for Unity - -Note: Make sure ~/.local/bin is in your PATH for user-local installations."; - } - - public override DependencyStatus DetectUv() - { - // First, honor overrides and cross-platform resolution via the base implementation - var status = base.DetectUv(); - if (status.IsAvailable) - { - return status; - } - - // If the user configured an override path but fallback was not used, keep the base result - // (failure typically means the override path is invalid and no system fallback found) - if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback) - { - return status; - } - - try - { - string augmentedPath = BuildAugmentedPath(); - - // Try uv first, then uvx, using ExecPath.TryRun for proper timeout handling - if (TryValidateUvWithPath("uv", augmentedPath, out string version, out string fullPath) || - TryValidateUvWithPath("uvx", augmentedPath, out version, out fullPath)) - { - status.IsAvailable = true; - status.Version = version; - status.Path = fullPath; - status.Details = $"Found uv {version} in PATH"; - status.ErrorMessage = null; - return status; - } - - status.ErrorMessage = "uv not found in PATH"; - status.Details = "Install uv package manager and ensure it's added to PATH."; - } - catch (Exception ex) - { - status.ErrorMessage = $"Error detecting uv: {ex.Message}"; - } - - return status; - } - - private bool TryValidatePython(string pythonPath, out string version, out string fullPath) - { - version = null; - fullPath = null; - - try - { - string augmentedPath = BuildAugmentedPath(); - - // First, try to resolve the absolute path for better UI/logging display - string commandToRun = pythonPath; - if (TryFindInPath(pythonPath, out string resolvedPath)) - { - commandToRun = resolvedPath; - } - - if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr, - 5000, augmentedPath)) - return false; - - // Check stdout first, then stderr (some Python distributions output to stderr) - string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim(); - if (output.StartsWith("Python ")) - { - version = output.Substring(7); - fullPath = commandToRun; - - if (TryParseVersion(version, out var major, out var minor)) - { - return major > 3 || (major == 3 && minor >= 10); - } - } - } - catch - { - // Ignore validation errors - } - - return false; - } - - protected string BuildAugmentedPath() - { - var additions = GetPathAdditions(); - if (additions.Length == 0) return null; - - // Only return the additions - ExecPath.TryRun will prepend to existing PATH - return string.Join(Path.PathSeparator, additions); - } - - private string[] GetPathAdditions() - { - var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - return new[] - { - "/usr/local/bin", - "/usr/bin", - "/bin", - "/snap/bin", - Path.Combine(homeDir, ".local", "bin") - }; - } - - protected override bool TryFindInPath(string executable, out string fullPath) - { - fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath()); - return !string.IsNullOrEmpty(fullPath); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta b/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta deleted file mode 100644 index 53e3215..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b682b492eb80d4ed6834b76f72c9f0f3 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs b/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs deleted file mode 100644 index b162f11..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Dependencies.Models; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; - -namespace MCPForUnity.Editor.Dependencies.PlatformDetectors -{ - /// - /// macOS-specific dependency detection - /// - public class MacOSPlatformDetector : PlatformDetectorBase - { - public override string PlatformName => "macOS"; - - public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); - - public override DependencyStatus DetectPython() - { - var status = new DependencyStatus("Python", isRequired: true) - { - InstallationHint = GetPythonInstallUrl() - }; - - try - { - // 1. Try 'which' command with augmented PATH (prioritizing Homebrew) - if (TryFindInPath("python3", out string pathResult) || - TryFindInPath("python", out pathResult)) - { - if (TryValidatePython(pathResult, out string version, out string fullPath)) - { - status.IsAvailable = true; - status.Version = version; - status.Path = fullPath; - status.Details = $"Found Python {version} at {fullPath}"; - return status; - } - } - - // 2. Fallback: Try running python directly from PATH - if (TryValidatePython("python3", out string v, out string p) || - TryValidatePython("python", out v, out p)) - { - status.IsAvailable = true; - status.Version = v; - status.Path = p; - status.Details = $"Found Python {v} in PATH"; - return status; - } - - status.ErrorMessage = "Python not found in PATH or standard locations"; - status.Details = "Install Python 3.10+ via Homebrew ('brew install python3') and ensure it's in your PATH."; - } - catch (Exception ex) - { - status.ErrorMessage = $"Error detecting Python: {ex.Message}"; - } - - return status; - } - - public override string GetPythonInstallUrl() - { - return "https://www.python.org/downloads/macos/"; - } - - public override string GetUvInstallUrl() - { - return "https://docs.astral.sh/uv/getting-started/installation/#macos"; - } - - public override string GetInstallationRecommendations() - { - return @"macOS Installation Recommendations: - -1. Python: Install via Homebrew (recommended) or python.org - - Homebrew: brew install python3 - - Direct download: https://python.org/downloads/macos/ - -2. uv Package Manager: Install via curl or Homebrew - - Curl: curl -LsSf https://astral.sh/uv/install.sh | sh - - Homebrew: brew install uv - -3. MCP Server: Will be installed automatically by MCP for Unity Bridge - -Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH."; - } - - public override DependencyStatus DetectUv() - { - // First, honor overrides and cross-platform resolution via the base implementation - var status = base.DetectUv(); - if (status.IsAvailable) - { - return status; - } - - // If the user configured an override path but fallback was not used, keep the base result - // (failure typically means the override path is invalid and no system fallback found) - if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback) - { - return status; - } - - try - { - string augmentedPath = BuildAugmentedPath(); - - // Try uv first, then uvx, using ExecPath.TryRun for proper timeout handling - if (TryValidateUvWithPath("uv", augmentedPath, out string version, out string fullPath) || - TryValidateUvWithPath("uvx", augmentedPath, out version, out fullPath)) - { - status.IsAvailable = true; - status.Version = version; - status.Path = fullPath; - status.Details = $"Found uv {version} in PATH"; - status.ErrorMessage = null; - return status; - } - - status.ErrorMessage = "uv not found in PATH"; - status.Details = "Install uv package manager and ensure it's added to PATH."; - } - catch (Exception ex) - { - status.ErrorMessage = $"Error detecting uv: {ex.Message}"; - } - - return status; - } - - private bool TryValidatePython(string pythonPath, out string version, out string fullPath) - { - version = null; - fullPath = null; - - try - { - string augmentedPath = BuildAugmentedPath(); - - // First, try to resolve the absolute path for better UI/logging display - string commandToRun = pythonPath; - if (TryFindInPath(pythonPath, out string resolvedPath)) - { - commandToRun = resolvedPath; - } - - if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr, - 5000, augmentedPath)) - return false; - - // Check stdout first, then stderr (some Python distributions output to stderr) - string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim(); - if (output.StartsWith("Python ")) - { - version = output.Substring(7); - fullPath = commandToRun; - - if (TryParseVersion(version, out var major, out var minor)) - { - return major > 3 || (major == 3 && minor >= 10); - } - } - } - catch - { - // Ignore validation errors - } - - return false; - } - - protected string BuildAugmentedPath() - { - var additions = GetPathAdditions(); - if (additions.Length == 0) return null; - - // Only return the additions - ExecPath.TryRun will prepend to existing PATH - return string.Join(Path.PathSeparator, additions); - } - - private string[] GetPathAdditions() - { - var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - return new[] - { - "/opt/homebrew/bin", - "/usr/local/bin", - "/usr/bin", - "/bin", - Path.Combine(homeDir, ".local", "bin") - }; - } - - protected override bool TryFindInPath(string executable, out string fullPath) - { - fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath()); - return !string.IsNullOrEmpty(fullPath); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta b/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta deleted file mode 100644 index c5065c7..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: c6f602b0a8ca848859197f9a949a7a5d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs b/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs deleted file mode 100644 index c955381..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using MCPForUnity.Editor.Dependencies.Models; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; - -namespace MCPForUnity.Editor.Dependencies.PlatformDetectors -{ - /// - /// Base class for platform-specific dependency detection - /// - public abstract class PlatformDetectorBase : IPlatformDetector - { - public abstract string PlatformName { get; } - public abstract bool CanDetect { get; } - - public abstract DependencyStatus DetectPython(); - public abstract string GetPythonInstallUrl(); - public abstract string GetUvInstallUrl(); - public abstract string GetInstallationRecommendations(); - - public virtual DependencyStatus DetectUv() - { - var status = new DependencyStatus("uv Package Manager", isRequired: true) - { - InstallationHint = GetUvInstallUrl() - }; - - try - { - // Get uv path from PathResolverService (respects override) - string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); - - // Verify uv executable and get version - if (MCPServiceLocator.Paths.TryValidateUvxExecutable(uvxPath, out string version)) - { - status.IsAvailable = true; - status.Version = version; - status.Path = uvxPath; - - // Check if we used fallback from override to system path - if (MCPServiceLocator.Paths.HasUvxPathFallback) - { - status.Details = $"Found uv {version} (fallback to system path)"; - status.ErrorMessage = "Override path not found, using system path"; - } - else - { - status.Details = MCPServiceLocator.Paths.HasUvxPathOverride - ? $"Found uv {version} (override path)" - : $"Found uv {version} in system path"; - } - return status; - } - - status.ErrorMessage = "uvx not found"; - status.Details = "Install uv package manager or configure path override in Advanced Settings."; - } - catch (Exception ex) - { - status.ErrorMessage = $"Error detecting uvx: {ex.Message}"; - } - - return status; - } - - - protected bool TryParseVersion(string version, out int major, out int minor) - { - major = 0; - minor = 0; - - try - { - var parts = version.Split('.'); - if (parts.Length >= 2) - { - return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor); - } - } - catch - { - // Ignore parsing errors - } - - return false; - } - // In PlatformDetectorBase.cs - protected bool TryValidateUvWithPath(string command, string augmentedPath, out string version, out string fullPath) - { - version = null; - fullPath = null; - - try - { - string commandToRun = command; - if (TryFindInPath(command, out string resolvedPath)) - { - commandToRun = resolvedPath; - } - - if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr, - 5000, augmentedPath)) - return false; - - string output = string.IsNullOrWhiteSpace(stdout) ? stderr.Trim() : stdout.Trim(); - - if (output.StartsWith("uvx ") || output.StartsWith("uv ")) - { - int spaceIndex = output.IndexOf(' '); - if (spaceIndex >= 0) - { - var remainder = output.Substring(spaceIndex + 1).Trim(); - int nextSpace = remainder.IndexOf(' '); - int parenIndex = remainder.IndexOf('('); - int endIndex = Math.Min( - nextSpace >= 0 ? nextSpace : int.MaxValue, - parenIndex >= 0 ? parenIndex : int.MaxValue - ); - version = endIndex < int.MaxValue ? remainder.Substring(0, endIndex).Trim() : remainder; - fullPath = commandToRun; - return true; - } - } - } - catch - { - // Ignore validation errors - } - - return false; - } - - - // Add abstract method for subclasses to implement - protected abstract bool TryFindInPath(string executable, out string fullPath); - } -} diff --git a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta b/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta deleted file mode 100644 index e5d6a8c..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 44d715aedea2b8b41bf914433bbb2c49 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs b/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs deleted file mode 100644 index 706e503..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs +++ /dev/null @@ -1,297 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Dependencies.Models; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; - -namespace MCPForUnity.Editor.Dependencies.PlatformDetectors -{ - /// - /// Windows-specific dependency detection - /// - public class WindowsPlatformDetector : PlatformDetectorBase - { - public override string PlatformName => "Windows"; - - public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - - public override DependencyStatus DetectPython() - { - var status = new DependencyStatus("Python", isRequired: true) - { - InstallationHint = GetPythonInstallUrl() - }; - - try - { - // Try running python directly first (works with Windows App Execution Aliases) - if (TryValidatePython("python3.exe", out string version, out string fullPath) || - TryValidatePython("python.exe", out version, out fullPath)) - { - status.IsAvailable = true; - status.Version = version; - status.Path = fullPath; - status.Details = $"Found Python {version} in PATH"; - return status; - } - - // Fallback: try 'where' command - if (TryFindInPath("python3.exe", out string pathResult) || - TryFindInPath("python.exe", out pathResult)) - { - if (TryValidatePython(pathResult, out version, out fullPath)) - { - status.IsAvailable = true; - status.Version = version; - status.Path = fullPath; - status.Details = $"Found Python {version} in PATH"; - return status; - } - } - - // Fallback: try to find python via uv - if (TryFindPythonViaUv(out version, out fullPath)) - { - status.IsAvailable = true; - status.Version = version; - status.Path = fullPath; - status.Details = $"Found Python {version} via uv"; - return status; - } - - status.ErrorMessage = "Python not found in PATH"; - status.Details = "Install Python 3.10+ and ensure it's added to PATH."; - } - catch (Exception ex) - { - status.ErrorMessage = $"Error detecting Python: {ex.Message}"; - } - - return status; - } - - public override string GetPythonInstallUrl() - { - return "https://apps.microsoft.com/store/detail/python-313/9NCVDN91XZQP"; - } - - public override string GetUvInstallUrl() - { - return "https://docs.astral.sh/uv/getting-started/installation/#windows"; - } - - public override string GetInstallationRecommendations() - { - return @"Windows Installation Recommendations: - -1. Python: Install from Microsoft Store or python.org - - Microsoft Store: Search for 'Python 3.10' or higher - - Direct download: https://python.org/downloads/windows/ - -2. uv Package Manager: Install via PowerShell - - Run: powershell -ExecutionPolicy ByPass -c ""irm https://astral.sh/uv/install.ps1 | iex"" - - Or download from: https://github.com/astral-sh/uv/releases - -3. MCP Server: Will be installed automatically by MCP for Unity Bridge"; - } - - public override DependencyStatus DetectUv() - { - // First, honor overrides and cross-platform resolution via the base implementation - var status = base.DetectUv(); - if (status.IsAvailable) - { - return status; - } - - // If the user configured an override path but fallback was not used, keep the base result - // (failure typically means the override path is invalid and no system fallback found) - if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback) - { - return status; - } - - try - { - string augmentedPath = BuildAugmentedPath(); - - // try to find uv - if (TryValidateUvWithPath("uv.exe", augmentedPath, out string uvVersion, out string uvPath)) - { - status.IsAvailable = true; - status.Version = uvVersion; - status.Path = uvPath; - status.Details = $"Found uv {uvVersion} at {uvPath}"; - return status; - } - - // try to find uvx - if (TryValidateUvWithPath("uvx.exe", augmentedPath, out string uvxVersion, out string uvxPath)) - { - status.IsAvailable = true; - status.Version = uvxVersion; - status.Path = uvxPath; - status.Details = $"Found uvx {uvxVersion} at {uvxPath} (fallback)"; - return status; - } - - status.ErrorMessage = "uv not found in PATH"; - status.Details = "Install uv package manager and ensure it's added to PATH."; - } - catch (Exception ex) - { - status.ErrorMessage = $"Error detecting uv: {ex.Message}"; - } - - return status; - } - - - private bool TryFindPythonViaUv(out string version, out string fullPath) - { - version = null; - fullPath = null; - - try - { - string augmentedPath = BuildAugmentedPath(); - // Try to list installed python versions via uvx - if (!ExecPath.TryRun("uv", "python list", null, out string stdout, out string stderr, 5000, augmentedPath)) - return false; - - var lines = stdout.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - if (line.Contains("")) continue; - - var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length >= 2) - { - string potentialPath = parts[parts.Length - 1]; - if (File.Exists(potentialPath) && - (potentialPath.EndsWith("python.exe") || potentialPath.EndsWith("python3.exe"))) - { - if (TryValidatePython(potentialPath, out version, out fullPath)) - { - return true; - } - } - } - } - } - catch - { - // Ignore errors if uv is not installed or fails - } - - return false; - } - - private bool TryValidatePython(string pythonPath, out string version, out string fullPath) - { - version = null; - fullPath = null; - - try - { - string augmentedPath = BuildAugmentedPath(); - - // First, try to resolve the absolute path for better UI/logging display - string commandToRun = pythonPath; - if (TryFindInPath(pythonPath, out string resolvedPath)) - { - commandToRun = resolvedPath; - } - - // Run 'python --version' to get the version - if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr, 5000, augmentedPath)) - return false; - - // Check stdout first, then stderr (some Python distributions output to stderr) - string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim(); - if (output.StartsWith("Python ")) - { - version = output.Substring(7); - fullPath = commandToRun; - - if (TryParseVersion(version, out var major, out var minor)) - { - return major > 3 || (major == 3 && minor >= 10); - } - } - } - catch - { - // Ignore validation errors - } - - return false; - } - - protected override bool TryFindInPath(string executable, out string fullPath) - { - fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath()); - return !string.IsNullOrEmpty(fullPath); - } - - protected string BuildAugmentedPath() - { - var additions = GetPathAdditions(); - if (additions.Length == 0) return null; - - // Only return the additions - ExecPath.TryRun will prepend to existing PATH - return string.Join(Path.PathSeparator, additions); - } - - private string[] GetPathAdditions() - { - var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - - var additions = new List(); - - // uv common installation paths - if (!string.IsNullOrEmpty(localAppData)) - additions.Add(Path.Combine(localAppData, "Programs", "uv")); - if (!string.IsNullOrEmpty(programFiles)) - additions.Add(Path.Combine(programFiles, "uv")); - - // npm global paths - if (!string.IsNullOrEmpty(appData)) - additions.Add(Path.Combine(appData, "npm")); - if (!string.IsNullOrEmpty(localAppData)) - additions.Add(Path.Combine(localAppData, "npm")); - - // Python common paths - if (!string.IsNullOrEmpty(localAppData)) - additions.Add(Path.Combine(localAppData, "Programs", "Python")); - // Instead of hardcoded versions, enumerate existing directories - if (!string.IsNullOrEmpty(programFiles)) - { - try - { - var pythonDirs = Directory.GetDirectories(programFiles, "Python3*") - .OrderByDescending(d => d); // Newest first - foreach (var dir in pythonDirs) - { - additions.Add(dir); - } - } - catch { /* Ignore if directory doesn't exist */ } - } - - // User scripts - if (!string.IsNullOrEmpty(homeDir)) - additions.Add(Path.Combine(homeDir, ".local", "bin")); - - return additions.ToArray(); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta b/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta deleted file mode 100644 index 860924e..0000000 --- a/Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 1aedc29caa5704c07b487d20a27e9334 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/External.meta b/Assets/MCPForUnity/Editor/External.meta deleted file mode 100644 index ce757b1..0000000 --- a/Assets/MCPForUnity/Editor/External.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: c11944bcfb9ec4576bab52874b7df584 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/External/Tommy.cs b/Assets/MCPForUnity/Editor/External/Tommy.cs deleted file mode 100644 index 22e83b8..0000000 --- a/Assets/MCPForUnity/Editor/External/Tommy.cs +++ /dev/null @@ -1,2138 +0,0 @@ -#region LICENSE - -/* - * MIT License - * - * Copyright (c) 2020 Denis Zhidkikh - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -#endregion - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace MCPForUnity.External.Tommy -{ - #region TOML Nodes - - public abstract class TomlNode : IEnumerable - { - public virtual bool HasValue { get; } = false; - public virtual bool IsArray { get; } = false; - public virtual bool IsTable { get; } = false; - public virtual bool IsString { get; } = false; - public virtual bool IsInteger { get; } = false; - public virtual bool IsFloat { get; } = false; - public bool IsDateTime => IsDateTimeLocal || IsDateTimeOffset; - public virtual bool IsDateTimeLocal { get; } = false; - public virtual bool IsDateTimeOffset { get; } = false; - public virtual bool IsBoolean { get; } = false; - public virtual string Comment { get; set; } - public virtual int CollapseLevel { get; set; } - - public virtual TomlTable AsTable => this as TomlTable; - public virtual TomlString AsString => this as TomlString; - public virtual TomlInteger AsInteger => this as TomlInteger; - public virtual TomlFloat AsFloat => this as TomlFloat; - public virtual TomlBoolean AsBoolean => this as TomlBoolean; - public virtual TomlDateTimeLocal AsDateTimeLocal => this as TomlDateTimeLocal; - public virtual TomlDateTimeOffset AsDateTimeOffset => this as TomlDateTimeOffset; - public virtual TomlDateTime AsDateTime => this as TomlDateTime; - public virtual TomlArray AsArray => this as TomlArray; - - public virtual int ChildrenCount => 0; - - public virtual TomlNode this[string key] - { - get => null; - set { } - } - - public virtual TomlNode this[int index] - { - get => null; - set { } - } - - public virtual IEnumerable Children - { - get { yield break; } - } - - public virtual IEnumerable Keys - { - get { yield break; } - } - - public IEnumerator GetEnumerator() => Children.GetEnumerator(); - - public virtual bool TryGetNode(string key, out TomlNode node) - { - node = null; - return false; - } - - public virtual bool HasKey(string key) => false; - - public virtual bool HasItemAt(int index) => false; - - public virtual void Add(string key, TomlNode node) { } - - public virtual void Add(TomlNode node) { } - - public virtual void Delete(TomlNode node) { } - - public virtual void Delete(string key) { } - - public virtual void Delete(int index) { } - - public virtual void AddRange(IEnumerable nodes) - { - foreach (var tomlNode in nodes) Add(tomlNode); - } - - public virtual void WriteTo(TextWriter tw, string name = null) => tw.WriteLine(ToInlineToml()); - - public virtual string ToInlineToml() => ToString(); - - #region Native type to TOML cast - - public static implicit operator TomlNode(string value) => new TomlString { Value = value }; - - public static implicit operator TomlNode(bool value) => new TomlBoolean { Value = value }; - - public static implicit operator TomlNode(long value) => new TomlInteger { Value = value }; - - public static implicit operator TomlNode(float value) => new TomlFloat { Value = value }; - - public static implicit operator TomlNode(double value) => new TomlFloat { Value = value }; - - public static implicit operator TomlNode(DateTime value) => new TomlDateTimeLocal { Value = value }; - - public static implicit operator TomlNode(DateTimeOffset value) => new TomlDateTimeOffset { Value = value }; - - public static implicit operator TomlNode(TomlNode[] nodes) - { - var result = new TomlArray(); - result.AddRange(nodes); - return result; - } - - #endregion - - #region TOML to native type cast - - public static implicit operator string(TomlNode value) => value.ToString(); - - public static implicit operator int(TomlNode value) => (int)value.AsInteger.Value; - - public static implicit operator long(TomlNode value) => value.AsInteger.Value; - - public static implicit operator float(TomlNode value) => (float)value.AsFloat.Value; - - public static implicit operator double(TomlNode value) => value.AsFloat.Value; - - public static implicit operator bool(TomlNode value) => value.AsBoolean.Value; - - public static implicit operator DateTime(TomlNode value) => value.AsDateTimeLocal.Value; - - public static implicit operator DateTimeOffset(TomlNode value) => value.AsDateTimeOffset.Value; - - #endregion - } - - public class TomlString : TomlNode - { - public override bool HasValue { get; } = true; - public override bool IsString { get; } = true; - public bool IsMultiline { get; set; } - public bool MultilineTrimFirstLine { get; set; } - public bool PreferLiteral { get; set; } - - public string Value { get; set; } - - public override string ToString() => Value; - - public override string ToInlineToml() - { - // Automatically convert literal to non-literal if there are too many literal string symbols - if (Value.IndexOf(new string(TomlSyntax.LITERAL_STRING_SYMBOL, IsMultiline ? 3 : 1), StringComparison.Ordinal) != -1 && PreferLiteral) PreferLiteral = false; - var quotes = new string(PreferLiteral ? TomlSyntax.LITERAL_STRING_SYMBOL : TomlSyntax.BASIC_STRING_SYMBOL, - IsMultiline ? 3 : 1); - var result = PreferLiteral ? Value : Value.Escape(!IsMultiline); - if (IsMultiline) - result = result.Replace("\r\n", "\n").Replace("\n", Environment.NewLine); - if (IsMultiline && (MultilineTrimFirstLine || !MultilineTrimFirstLine && result.StartsWith(Environment.NewLine))) - result = $"{Environment.NewLine}{result}"; - return $"{quotes}{result}{quotes}"; - } - } - - public class TomlInteger : TomlNode - { - public enum Base - { - Binary = 2, - Octal = 8, - Decimal = 10, - Hexadecimal = 16 - } - - public override bool IsInteger { get; } = true; - public override bool HasValue { get; } = true; - public Base IntegerBase { get; set; } = Base.Decimal; - - public long Value { get; set; } - - public override string ToString() => Value.ToString(); - - public override string ToInlineToml() => - IntegerBase != Base.Decimal - ? $"0{TomlSyntax.BaseIdentifiers[(int)IntegerBase]}{Convert.ToString(Value, (int)IntegerBase)}" - : Value.ToString(CultureInfo.InvariantCulture); - } - - public class TomlFloat : TomlNode, IFormattable - { - public override bool IsFloat { get; } = true; - public override bool HasValue { get; } = true; - - public double Value { get; set; } - - public override string ToString() => Value.ToString(CultureInfo.InvariantCulture); - - public string ToString(string format, IFormatProvider formatProvider) => Value.ToString(format, formatProvider); - - public string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider); - - public override string ToInlineToml() => - Value switch - { - var v when double.IsNaN(v) => TomlSyntax.NAN_VALUE, - var v when double.IsPositiveInfinity(v) => TomlSyntax.INF_VALUE, - var v when double.IsNegativeInfinity(v) => TomlSyntax.NEG_INF_VALUE, - var v => v.ToString("G", CultureInfo.InvariantCulture).ToLowerInvariant() - }; - } - - public class TomlBoolean : TomlNode - { - public override bool IsBoolean { get; } = true; - public override bool HasValue { get; } = true; - - public bool Value { get; set; } - - public override string ToString() => Value.ToString(); - - public override string ToInlineToml() => Value ? TomlSyntax.TRUE_VALUE : TomlSyntax.FALSE_VALUE; - } - - public class TomlDateTime : TomlNode, IFormattable - { - public int SecondsPrecision { get; set; } - public override bool HasValue { get; } = true; - public virtual string ToString(string format, IFormatProvider formatProvider) => string.Empty; - public virtual string ToString(IFormatProvider formatProvider) => string.Empty; - protected virtual string ToInlineTomlInternal() => string.Empty; - - public override string ToInlineToml() => ToInlineTomlInternal() - .Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator) - .Replace(TomlSyntax.ISO861ZeroZone, TomlSyntax.RFC3339ZeroZone); - } - - public class TomlDateTimeOffset : TomlDateTime - { - public override bool IsDateTimeOffset { get; } = true; - public DateTimeOffset Value { get; set; } - - public override string ToString() => Value.ToString(CultureInfo.CurrentCulture); - public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider); - - public override string ToString(string format, IFormatProvider formatProvider) => - Value.ToString(format, formatProvider); - - protected override string ToInlineTomlInternal() => Value.ToString(TomlSyntax.RFC3339Formats[SecondsPrecision]); - } - - public class TomlDateTimeLocal : TomlDateTime - { - public enum DateTimeStyle - { - Date, - Time, - DateTime - } - - public override bool IsDateTimeLocal { get; } = true; - public DateTimeStyle Style { get; set; } = DateTimeStyle.DateTime; - public DateTime Value { get; set; } - - public override string ToString() => Value.ToString(CultureInfo.CurrentCulture); - - public override string ToString(IFormatProvider formatProvider) => Value.ToString(formatProvider); - - public override string ToString(string format, IFormatProvider formatProvider) => - Value.ToString(format, formatProvider); - - public override string ToInlineToml() => - Style switch - { - DateTimeStyle.Date => Value.ToString(TomlSyntax.LocalDateFormat), - DateTimeStyle.Time => Value.ToString(TomlSyntax.RFC3339LocalTimeFormats[SecondsPrecision]), - var _ => Value.ToString(TomlSyntax.RFC3339LocalDateTimeFormats[SecondsPrecision]) - }; - } - - public class TomlArray : TomlNode - { - private List values; - - public override bool HasValue { get; } = true; - public override bool IsArray { get; } = true; - public bool IsMultiline { get; set; } - public bool IsTableArray { get; set; } - public List RawArray => values ??= new List(); - - public override TomlNode this[int index] - { - get - { - if (index < RawArray.Count) return RawArray[index]; - var lazy = new TomlLazy(this); - this[index] = lazy; - return lazy; - } - set - { - if (index == RawArray.Count) - RawArray.Add(value); - else - RawArray[index] = value; - } - } - - public override int ChildrenCount => RawArray.Count; - - public override IEnumerable Children => RawArray.AsEnumerable(); - - public override void Add(TomlNode node) => RawArray.Add(node); - - public override void AddRange(IEnumerable nodes) => RawArray.AddRange(nodes); - - public override void Delete(TomlNode node) => RawArray.Remove(node); - - public override void Delete(int index) => RawArray.RemoveAt(index); - - public override string ToString() => ToString(false); - - public string ToString(bool multiline) - { - var sb = new StringBuilder(); - sb.Append(TomlSyntax.ARRAY_START_SYMBOL); - if (ChildrenCount != 0) - { - var arrayStart = multiline ? $"{Environment.NewLine} " : " "; - var arraySeparator = multiline ? $"{TomlSyntax.ITEM_SEPARATOR}{Environment.NewLine} " : $"{TomlSyntax.ITEM_SEPARATOR} "; - var arrayEnd = multiline ? Environment.NewLine : " "; - sb.Append(arrayStart) - .Append(arraySeparator.Join(RawArray.Select(n => n.ToInlineToml()))) - .Append(arrayEnd); - } - sb.Append(TomlSyntax.ARRAY_END_SYMBOL); - return sb.ToString(); - } - - public override void WriteTo(TextWriter tw, string name = null) - { - // If it's a normal array, write it as usual - if (!IsTableArray) - { - tw.WriteLine(ToString(IsMultiline)); - return; - } - - if (!(Comment is null)) - { - tw.WriteLine(); - Comment.AsComment(tw); - } - tw.Write(TomlSyntax.ARRAY_START_SYMBOL); - tw.Write(TomlSyntax.ARRAY_START_SYMBOL); - tw.Write(name); - tw.Write(TomlSyntax.ARRAY_END_SYMBOL); - tw.Write(TomlSyntax.ARRAY_END_SYMBOL); - tw.WriteLine(); - - var first = true; - - foreach (var tomlNode in RawArray) - { - if (!(tomlNode is TomlTable tbl)) - throw new TomlFormatException("The array is marked as array table but contains non-table nodes!"); - - // Ensure it's parsed as a section - tbl.IsInline = false; - - if (!first) - { - tw.WriteLine(); - - Comment?.AsComment(tw); - tw.Write(TomlSyntax.ARRAY_START_SYMBOL); - tw.Write(TomlSyntax.ARRAY_START_SYMBOL); - tw.Write(name); - tw.Write(TomlSyntax.ARRAY_END_SYMBOL); - tw.Write(TomlSyntax.ARRAY_END_SYMBOL); - tw.WriteLine(); - } - - first = false; - - // Don't write section since it's already written here - tbl.WriteTo(tw, name, false); - } - } - } - - public class TomlTable : TomlNode - { - private Dictionary children; - internal bool isImplicit; - - public override bool HasValue { get; } = false; - public override bool IsTable { get; } = true; - public bool IsInline { get; set; } - public Dictionary RawTable => children ??= new Dictionary(); - - public override TomlNode this[string key] - { - get - { - if (RawTable.TryGetValue(key, out var result)) return result; - var lazy = new TomlLazy(this); - RawTable[key] = lazy; - return lazy; - } - set => RawTable[key] = value; - } - - public override int ChildrenCount => RawTable.Count; - public override IEnumerable Children => RawTable.Select(kv => kv.Value); - public override IEnumerable Keys => RawTable.Select(kv => kv.Key); - public override bool HasKey(string key) => RawTable.ContainsKey(key); - public override void Add(string key, TomlNode node) => RawTable.Add(key, node); - public override bool TryGetNode(string key, out TomlNode node) => RawTable.TryGetValue(key, out node); - public override void Delete(TomlNode node) => RawTable.Remove(RawTable.First(kv => kv.Value == node).Key); - public override void Delete(string key) => RawTable.Remove(key); - - public override string ToString() - { - var sb = new StringBuilder(); - sb.Append(TomlSyntax.INLINE_TABLE_START_SYMBOL); - - if (ChildrenCount != 0) - { - var collapsed = CollectCollapsedItems(normalizeOrder: false); - - if (collapsed.Count != 0) - sb.Append(' ') - .Append($"{TomlSyntax.ITEM_SEPARATOR} ".Join(collapsed.Select(n => - $"{n.Key} {TomlSyntax.KEY_VALUE_SEPARATOR} {n.Value.ToInlineToml()}"))); - sb.Append(' '); - } - - sb.Append(TomlSyntax.INLINE_TABLE_END_SYMBOL); - return sb.ToString(); - } - - private LinkedList> CollectCollapsedItems(string prefix = "", int level = 0, bool normalizeOrder = true) - { - var nodes = new LinkedList>(); - var postNodes = normalizeOrder ? new LinkedList>() : nodes; - - foreach (var keyValuePair in RawTable) - { - var node = keyValuePair.Value; - var key = keyValuePair.Key.AsKey(); - - if (node is TomlTable tbl) - { - var subnodes = tbl.CollectCollapsedItems($"{prefix}{key}.", level + 1, normalizeOrder); - // Write main table first before writing collapsed items - if (subnodes.Count == 0 && node.CollapseLevel == level) - { - postNodes.AddLast(new KeyValuePair($"{prefix}{key}", node)); - } - foreach (var kv in subnodes) - postNodes.AddLast(kv); - } - else if (node.CollapseLevel == level) - nodes.AddLast(new KeyValuePair($"{prefix}{key}", node)); - } - - if (normalizeOrder) - foreach (var kv in postNodes) - nodes.AddLast(kv); - - return nodes; - } - - public override void WriteTo(TextWriter tw, string name = null) => WriteTo(tw, name, true); - - internal void WriteTo(TextWriter tw, string name, bool writeSectionName) - { - // The table is inline table - if (IsInline && name != null) - { - tw.WriteLine(ToInlineToml()); - return; - } - - var collapsedItems = CollectCollapsedItems(); - - if (collapsedItems.Count == 0) - return; - - var hasRealValues = !collapsedItems.All(n => n.Value is TomlTable { IsInline: false } or TomlArray { IsTableArray: true }); - - Comment?.AsComment(tw); - - if (name != null && (hasRealValues || Comment != null) && writeSectionName) - { - tw.Write(TomlSyntax.ARRAY_START_SYMBOL); - tw.Write(name); - tw.Write(TomlSyntax.ARRAY_END_SYMBOL); - tw.WriteLine(); - } - else if (Comment != null) // Add some spacing between the first node and the comment - { - tw.WriteLine(); - } - - var namePrefix = name == null ? "" : $"{name}."; - var first = true; - - foreach (var collapsedItem in collapsedItems) - { - var key = collapsedItem.Key; - if (collapsedItem.Value is TomlArray { IsTableArray: true } or TomlTable { IsInline: false }) - { - if (!first) tw.WriteLine(); - first = false; - collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}"); - continue; - } - first = false; - - collapsedItem.Value.Comment?.AsComment(tw); - tw.Write(key); - tw.Write(' '); - tw.Write(TomlSyntax.KEY_VALUE_SEPARATOR); - tw.Write(' '); - - collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}"); - } - } - } - - internal class TomlLazy : TomlNode - { - private readonly TomlNode parent; - private TomlNode replacement; - - public TomlLazy(TomlNode parent) => this.parent = parent; - - public override TomlNode this[int index] - { - get => Set()[index]; - set => Set()[index] = value; - } - - public override TomlNode this[string key] - { - get => Set()[key]; - set => Set()[key] = value; - } - - public override void Add(TomlNode node) => Set().Add(node); - - public override void Add(string key, TomlNode node) => Set().Add(key, node); - - public override void AddRange(IEnumerable nodes) => Set().AddRange(nodes); - - private TomlNode Set() where T : TomlNode, new() - { - if (replacement != null) return replacement; - - var newNode = new T - { - Comment = Comment - }; - - if (parent.IsTable) - { - var key = parent.Keys.FirstOrDefault(s => parent.TryGetNode(s, out var node) && node.Equals(this)); - if (key == null) return default(T); - - parent[key] = newNode; - } - else if (parent.IsArray) - { - var index = parent.Children.TakeWhile(child => child != this).Count(); - if (index == parent.ChildrenCount) return default(T); - parent[index] = newNode; - } - else - { - return default(T); - } - - replacement = newNode; - return newNode; - } - } - - #endregion - - #region Parser - - public class TOMLParser : IDisposable - { - public enum ParseState - { - None, - KeyValuePair, - SkipToNextLine, - Table - } - - private readonly TextReader reader; - private ParseState currentState; - private int line, col; - private List syntaxErrors; - - public TOMLParser(TextReader reader) - { - this.reader = reader; - line = col = 0; - } - - public bool ForceASCII { get; set; } - - public void Dispose() => reader?.Dispose(); - - public TomlTable Parse() - { - syntaxErrors = new List(); - line = col = 1; - var rootNode = new TomlTable(); - var currentNode = rootNode; - currentState = ParseState.None; - var keyParts = new List(); - var arrayTable = false; - StringBuilder latestComment = null; - var firstComment = true; - - int currentChar; - while ((currentChar = reader.Peek()) >= 0) - { - var c = (char)currentChar; - - if (currentState == ParseState.None) - { - // Skip white space - if (TomlSyntax.IsWhiteSpace(c)) goto consume_character; - - if (TomlSyntax.IsNewLine(c)) - { - // Check if there are any comments and so far no items being declared - if (latestComment != null && firstComment) - { - rootNode.Comment = latestComment.ToString().TrimEnd(); - latestComment = null; - firstComment = false; - } - - if (TomlSyntax.IsLineBreak(c)) - AdvanceLine(); - - goto consume_character; - } - - // Start of a comment; ignore until newline - if (c == TomlSyntax.COMMENT_SYMBOL) - { - latestComment ??= new StringBuilder(); - latestComment.AppendLine(ParseComment()); - AdvanceLine(1); - continue; - } - - // Encountered a non-comment value. The comment must belong to it (ignore possible newlines)! - firstComment = false; - - if (c == TomlSyntax.TABLE_START_SYMBOL) - { - currentState = ParseState.Table; - goto consume_character; - } - - if (TomlSyntax.IsBareKey(c) || TomlSyntax.IsQuoted(c)) - { - currentState = ParseState.KeyValuePair; - } - else - { - AddError($"Unexpected character \"{c}\""); - continue; - } - } - - if (currentState == ParseState.KeyValuePair) - { - var keyValuePair = ReadKeyValuePair(keyParts); - - if (keyValuePair == null) - { - latestComment = null; - keyParts.Clear(); - - if (currentState != ParseState.None) - AddError("Failed to parse key-value pair!"); - continue; - } - - keyValuePair.Comment = latestComment?.ToString()?.TrimEnd(); - var inserted = InsertNode(keyValuePair, currentNode, keyParts); - latestComment = null; - keyParts.Clear(); - if (inserted) - currentState = ParseState.SkipToNextLine; - continue; - } - - if (currentState == ParseState.Table) - { - if (keyParts.Count == 0) - { - // We have array table - if (c == TomlSyntax.TABLE_START_SYMBOL) - { - // Consume the character - ConsumeChar(); - arrayTable = true; - } - - if (!ReadKeyName(ref keyParts, TomlSyntax.TABLE_END_SYMBOL)) - { - keyParts.Clear(); - continue; - } - - if (keyParts.Count == 0) - { - AddError("Table name is emtpy."); - arrayTable = false; - latestComment = null; - keyParts.Clear(); - } - - continue; - } - - if (c == TomlSyntax.TABLE_END_SYMBOL) - { - if (arrayTable) - { - // Consume the ending bracket so we can peek the next character - ConsumeChar(); - var nextChar = reader.Peek(); - if (nextChar < 0 || (char)nextChar != TomlSyntax.TABLE_END_SYMBOL) - { - AddError($"Array table {".".Join(keyParts)} has only one closing bracket."); - keyParts.Clear(); - arrayTable = false; - latestComment = null; - continue; - } - } - - currentNode = CreateTable(rootNode, keyParts, arrayTable); - if (currentNode != null) - { - currentNode.IsInline = false; - currentNode.Comment = latestComment?.ToString()?.TrimEnd(); - } - - keyParts.Clear(); - arrayTable = false; - latestComment = null; - - if (currentNode == null) - { - if (currentState != ParseState.None) - AddError("Error creating table array!"); - // Reset a node to root in order to try and continue parsing - currentNode = rootNode; - continue; - } - - currentState = ParseState.SkipToNextLine; - goto consume_character; - } - - if (keyParts.Count != 0) - { - AddError($"Unexpected character \"{c}\""); - keyParts.Clear(); - arrayTable = false; - latestComment = null; - } - } - - if (currentState == ParseState.SkipToNextLine) - { - if (TomlSyntax.IsWhiteSpace(c) || c == TomlSyntax.NEWLINE_CARRIAGE_RETURN_CHARACTER) - goto consume_character; - - if (c is TomlSyntax.COMMENT_SYMBOL or TomlSyntax.NEWLINE_CHARACTER) - { - currentState = ParseState.None; - AdvanceLine(); - - if (c == TomlSyntax.COMMENT_SYMBOL) - { - col++; - ParseComment(); - continue; - } - - goto consume_character; - } - - AddError($"Unexpected character \"{c}\" at the end of the line."); - } - - consume_character: - reader.Read(); - col++; - } - - if (currentState != ParseState.None && currentState != ParseState.SkipToNextLine) - AddError("Unexpected end of file!"); - - if (syntaxErrors.Count > 0) - throw new TomlParseException(rootNode, syntaxErrors); - - return rootNode; - } - - private bool AddError(string message, bool skipLine = true) - { - syntaxErrors.Add(new TomlSyntaxException(message, currentState, line, col)); - // Skip the whole line in hope that it was only a single faulty value (and non-multiline one at that) - if (skipLine) - { - reader.ReadLine(); - AdvanceLine(1); - } - currentState = ParseState.None; - return false; - } - - private void AdvanceLine(int startCol = 0) - { - line++; - col = startCol; - } - - private int ConsumeChar() - { - col++; - return reader.Read(); - } - - #region Key-Value pair parsing - - /** - * Reads a single key-value pair. - * Assumes the cursor is at the first character that belong to the pair (including possible whitespace). - * Consumes all characters that belong to the key and the value (ignoring possible trailing whitespace at the end). - * - * Example: - * foo = "bar" ==> foo = "bar" - * ^ ^ - */ - private TomlNode ReadKeyValuePair(List keyParts) - { - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - - if (TomlSyntax.IsQuoted(c) || TomlSyntax.IsBareKey(c)) - { - if (keyParts.Count != 0) - { - AddError("Encountered extra characters in key definition!"); - return null; - } - - if (!ReadKeyName(ref keyParts, TomlSyntax.KEY_VALUE_SEPARATOR)) - return null; - - continue; - } - - if (TomlSyntax.IsWhiteSpace(c)) - { - ConsumeChar(); - continue; - } - - if (c == TomlSyntax.KEY_VALUE_SEPARATOR) - { - ConsumeChar(); - return ReadValue(); - } - - AddError($"Unexpected character \"{c}\" in key name."); - return null; - } - - return null; - } - - /** - * Reads a single value. - * Assumes the cursor is at the first character that belongs to the value (including possible starting whitespace). - * Consumes all characters belonging to the value (ignoring possible trailing whitespace at the end). - * - * Example: - * "test" ==> "test" - * ^ ^ - */ - private TomlNode ReadValue(bool skipNewlines = false) - { - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - - if (TomlSyntax.IsWhiteSpace(c)) - { - ConsumeChar(); - continue; - } - - if (c == TomlSyntax.COMMENT_SYMBOL) - { - AddError("No value found!"); - return null; - } - - if (TomlSyntax.IsNewLine(c)) - { - if (skipNewlines) - { - reader.Read(); - AdvanceLine(1); - continue; - } - - AddError("Encountered a newline when expecting a value!"); - return null; - } - - if (TomlSyntax.IsQuoted(c)) - { - var isMultiline = IsTripleQuote(c, out var excess); - - // Error occurred in triple quote parsing - if (currentState == ParseState.None) - return null; - - var value = isMultiline - ? ReadQuotedValueMultiLine(c) - : ReadQuotedValueSingleLine(c, excess); - - if (value is null) - return null; - - return new TomlString - { - Value = value, - IsMultiline = isMultiline, - PreferLiteral = c == TomlSyntax.LITERAL_STRING_SYMBOL - }; - } - - return c switch - { - TomlSyntax.INLINE_TABLE_START_SYMBOL => ReadInlineTable(), - TomlSyntax.ARRAY_START_SYMBOL => ReadArray(), - var _ => ReadTomlValue() - }; - } - - return null; - } - - /** - * Reads a single key name. - * Assumes the cursor is at the first character belonging to the key (with possible trailing whitespace if `skipWhitespace = true`). - * Consumes all the characters until the `until` character is met (but does not consume the character itself). - * - * Example 1: - * foo.bar ==> foo.bar (`skipWhitespace = false`, `until = ' '`) - * ^ ^ - * - * Example 2: - * [ foo . bar ] ==> [ foo . bar ] (`skipWhitespace = true`, `until = ']'`) - * ^ ^ - */ - private bool ReadKeyName(ref List parts, char until) - { - var buffer = new StringBuilder(); - var quoted = false; - var prevWasSpace = false; - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - - // Reached the final character - if (c == until) break; - - if (TomlSyntax.IsWhiteSpace(c)) - { - prevWasSpace = true; - goto consume_character; - } - - if (buffer.Length == 0) prevWasSpace = false; - - if (c == TomlSyntax.SUBKEY_SEPARATOR) - { - if (buffer.Length == 0 && !quoted) - return AddError($"Found an extra subkey separator in {".".Join(parts)}..."); - - parts.Add(buffer.ToString()); - buffer.Length = 0; - quoted = false; - prevWasSpace = false; - goto consume_character; - } - - if (prevWasSpace) - return AddError("Invalid spacing in key name"); - - if (TomlSyntax.IsQuoted(c)) - { - if (quoted) - - return AddError("Expected a subkey separator but got extra data instead!"); - - if (buffer.Length != 0) - return AddError("Encountered a quote in the middle of subkey name!"); - - // Consume the quote character and read the key name - col++; - buffer.Append(ReadQuotedValueSingleLine((char)reader.Read())); - quoted = true; - continue; - } - - if (TomlSyntax.IsBareKey(c)) - { - buffer.Append(c); - goto consume_character; - } - - // If we see an invalid symbol, let the next parser handle it - break; - - consume_character: - reader.Read(); - col++; - } - - if (buffer.Length == 0 && !quoted) - return AddError($"Found an extra subkey separator in {".".Join(parts)}..."); - - parts.Add(buffer.ToString()); - - return true; - } - - #endregion - - #region Non-string value parsing - - /** - * Reads the whole raw value until the first non-value character is encountered. - * Assumes the cursor start position at the first value character and consumes all characters that may be related to the value. - * Example: - * - * 1_0_0_0 ==> 1_0_0_0 - * ^ ^ - */ - private string ReadRawValue() - { - var result = new StringBuilder(); - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - if (c == TomlSyntax.COMMENT_SYMBOL || TomlSyntax.IsNewLine(c) || TomlSyntax.IsValueSeparator(c)) break; - result.Append(c); - ConsumeChar(); - } - - // Replace trim with manual space counting? - return result.ToString().Trim(); - } - - /** - * Reads and parses a non-string, non-composite TOML value. - * Assumes the cursor at the first character that is related to the value (with possible spaces). - * Consumes all the characters that are related to the value. - * - * Example - * 1_0_0_0 # This is a comment - * - * ==> 1_0_0_0 # This is a comment - * ^ ^ - */ - private TomlNode ReadTomlValue() - { - var value = ReadRawValue(); - TomlNode node = value switch - { - var v when TomlSyntax.IsBoolean(v) => bool.Parse(v), - var v when TomlSyntax.IsNaN(v) => double.NaN, - var v when TomlSyntax.IsPosInf(v) => double.PositiveInfinity, - var v when TomlSyntax.IsNegInf(v) => double.NegativeInfinity, - var v when TomlSyntax.IsInteger(v) => long.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), - CultureInfo.InvariantCulture), - var v when TomlSyntax.IsFloat(v) => double.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), - CultureInfo.InvariantCulture), - var v when TomlSyntax.IsIntegerWithBase(v, out var numberBase) => new TomlInteger - { - Value = Convert.ToInt64(value.Substring(2).RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), numberBase), - IntegerBase = (TomlInteger.Base)numberBase - }, - var _ => null - }; - if (node != null) return node; - - // Normalize by removing space separator - value = value.Replace(TomlSyntax.RFC3339EmptySeparator, TomlSyntax.ISO861Separator); - if (StringUtils.TryParseDateTime(value, - TomlSyntax.RFC3339LocalDateTimeFormats, - DateTimeStyles.AssumeLocal, - DateTime.TryParseExact, - out var dateTimeResult, - out var precision)) - return new TomlDateTimeLocal - { - Value = dateTimeResult, - SecondsPrecision = precision - }; - - if (DateTime.TryParseExact(value, - TomlSyntax.LocalDateFormat, - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeLocal, - out dateTimeResult)) - return new TomlDateTimeLocal - { - Value = dateTimeResult, - Style = TomlDateTimeLocal.DateTimeStyle.Date - }; - - if (StringUtils.TryParseDateTime(value, - TomlSyntax.RFC3339LocalTimeFormats, - DateTimeStyles.AssumeLocal, - DateTime.TryParseExact, - out dateTimeResult, - out precision)) - return new TomlDateTimeLocal - { - Value = dateTimeResult, - Style = TomlDateTimeLocal.DateTimeStyle.Time, - SecondsPrecision = precision - }; - - if (StringUtils.TryParseDateTime(value, - TomlSyntax.RFC3339Formats, - DateTimeStyles.None, - DateTimeOffset.TryParseExact, - out var dateTimeOffsetResult, - out precision)) - return new TomlDateTimeOffset - { - Value = dateTimeOffsetResult, - SecondsPrecision = precision - }; - - AddError($"Value \"{value}\" is not a valid TOML value!"); - return null; - } - - /** - * Reads an array value. - * Assumes the cursor is at the start of the array definition. Reads all character until the array closing bracket. - * - * Example: - * [1, 2, 3] ==> [1, 2, 3] - * ^ ^ - */ - private TomlArray ReadArray() - { - // Consume the start of array character - ConsumeChar(); - var result = new TomlArray(); - TomlNode currentValue = null; - var expectValue = true; - - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - - if (c == TomlSyntax.ARRAY_END_SYMBOL) - { - ConsumeChar(); - break; - } - - if (c == TomlSyntax.COMMENT_SYMBOL) - { - reader.ReadLine(); - AdvanceLine(1); - continue; - } - - if (TomlSyntax.IsWhiteSpace(c) || TomlSyntax.IsNewLine(c)) - { - if (TomlSyntax.IsLineBreak(c)) - AdvanceLine(); - goto consume_character; - } - - if (c == TomlSyntax.ITEM_SEPARATOR) - { - if (currentValue == null) - { - AddError("Encountered multiple value separators"); - return null; - } - - result.Add(currentValue); - currentValue = null; - expectValue = true; - goto consume_character; - } - - if (!expectValue) - { - AddError("Missing separator between values"); - return null; - } - currentValue = ReadValue(true); - if (currentValue == null) - { - if (currentState != ParseState.None) - AddError("Failed to determine and parse a value!"); - return null; - } - expectValue = false; - - continue; - consume_character: - ConsumeChar(); - } - - if (currentValue != null) result.Add(currentValue); - return result; - } - - /** - * Reads an inline table. - * Assumes the cursor is at the start of the table definition. Reads all character until the table closing bracket. - * - * Example: - * { test = "foo", value = 1 } ==> { test = "foo", value = 1 } - * ^ ^ - */ - private TomlNode ReadInlineTable() - { - ConsumeChar(); - var result = new TomlTable { IsInline = true }; - TomlNode currentValue = null; - var separator = false; - var keyParts = new List(); - int cur; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - - if (c == TomlSyntax.INLINE_TABLE_END_SYMBOL) - { - ConsumeChar(); - break; - } - - if (c == TomlSyntax.COMMENT_SYMBOL) - { - AddError("Incomplete inline table definition!"); - return null; - } - - if (TomlSyntax.IsNewLine(c)) - { - AddError("Inline tables are only allowed to be on single line"); - return null; - } - - if (TomlSyntax.IsWhiteSpace(c)) - goto consume_character; - - if (c == TomlSyntax.ITEM_SEPARATOR) - { - if (currentValue == null) - { - AddError("Encountered multiple value separators in inline table!"); - return null; - } - - if (!InsertNode(currentValue, result, keyParts)) - return null; - keyParts.Clear(); - currentValue = null; - separator = true; - goto consume_character; - } - - separator = false; - currentValue = ReadKeyValuePair(keyParts); - continue; - - consume_character: - ConsumeChar(); - } - - if (separator) - { - AddError("Trailing commas are not allowed in inline tables."); - return null; - } - - if (currentValue != null && !InsertNode(currentValue, result, keyParts)) - return null; - - return result; - } - - #endregion - - #region String parsing - - /** - * Checks if the string value a multiline string (i.e. a triple quoted string). - * Assumes the cursor is at the first quote character. Consumes the least amount of characters needed to determine if the string is multiline. - * - * If the result is false, returns the consumed character through the `excess` variable. - * - * Example 1: - * """test""" ==> """test""" - * ^ ^ - * - * Example 2: - * "test" ==> "test" (doesn't return the first quote) - * ^ ^ - * - * Example 3: - * "" ==> "" (returns the extra `"` through the `excess` variable) - * ^ ^ - */ - private bool IsTripleQuote(char quote, out char excess) - { - // Copypasta, but it's faster... - - int cur; - // Consume the first quote - ConsumeChar(); - if ((cur = reader.Peek()) < 0) - { - excess = '\0'; - return AddError("Unexpected end of file!"); - } - - if ((char)cur != quote) - { - excess = '\0'; - return false; - } - - // Consume the second quote - excess = (char)ConsumeChar(); - if ((cur = reader.Peek()) < 0 || (char)cur != quote) return false; - - // Consume the final quote - ConsumeChar(); - excess = '\0'; - return true; - } - - /** - * A convenience method to process a single character within a quote. - */ - private bool ProcessQuotedValueCharacter(char quote, - bool isNonLiteral, - char c, - StringBuilder sb, - ref bool escaped) - { - if (TomlSyntax.MustBeEscaped(c)) - return AddError($"The character U+{(int)c:X8} must be escaped in a string!"); - - if (escaped) - { - sb.Append(c); - escaped = false; - return false; - } - - if (c == quote) - { - if (!isNonLiteral && reader.Peek() == quote) - { - reader.Read(); - col++; - sb.Append(quote); - return false; - } - - return true; - } - if (isNonLiteral && c == TomlSyntax.ESCAPE_SYMBOL) - escaped = true; - if (c == TomlSyntax.NEWLINE_CHARACTER) - return AddError("Encountered newline in single line string!"); - - sb.Append(c); - return false; - } - - /** - * Reads a single-line string. - * Assumes the cursor is at the first character that belongs to the string. - * Consumes all characters that belong to the string (including the closing quote). - * - * Example: - * "test" ==> "test" - * ^ ^ - */ - private string ReadQuotedValueSingleLine(char quote, char initialData = '\0') - { - var isNonLiteral = quote == TomlSyntax.BASIC_STRING_SYMBOL; - var sb = new StringBuilder(); - var escaped = false; - - if (initialData != '\0') - { - var shouldReturn = - ProcessQuotedValueCharacter(quote, isNonLiteral, initialData, sb, ref escaped); - if (currentState == ParseState.None) return null; - if (shouldReturn) - if (isNonLiteral) - { - if (sb.ToString().TryUnescape(out var res, out var ex)) return res; - AddError(ex.Message); - return null; - } - else - return sb.ToString(); - } - - int cur; - var readDone = false; - while ((cur = reader.Read()) >= 0) - { - // Consume the character - col++; - var c = (char)cur; - readDone = ProcessQuotedValueCharacter(quote, isNonLiteral, c, sb, ref escaped); - if (readDone) - { - if (currentState == ParseState.None) return null; - break; - } - } - - if (!readDone) - { - AddError("Unclosed string."); - return null; - } - - if (!isNonLiteral) return sb.ToString(); - if (sb.ToString().TryUnescape(out var unescaped, out var unescapedEx)) return unescaped; - AddError(unescapedEx.Message); - return null; - } - - /** - * Reads a multiline string. - * Assumes the cursor is at the first character that belongs to the string. - * Consumes all characters that belong to the string and the three closing quotes. - * - * Example: - * """test""" ==> """test""" - * ^ ^ - */ - private string ReadQuotedValueMultiLine(char quote) - { - var isBasic = quote == TomlSyntax.BASIC_STRING_SYMBOL; - var sb = new StringBuilder(); - var escaped = false; - var skipWhitespace = false; - var skipWhitespaceLineSkipped = false; - var quotesEncountered = 0; - var first = true; - int cur; - while ((cur = ConsumeChar()) >= 0) - { - var c = (char)cur; - if (TomlSyntax.MustBeEscaped(c, true)) - { - AddError($"The character U+{(int)c:X8} must be escaped!"); - return null; - } - // Trim the first newline - if (first && TomlSyntax.IsNewLine(c)) - { - if (TomlSyntax.IsLineBreak(c)) - first = false; - else - AdvanceLine(); - continue; - } - - first = false; - //TODO: Reuse ProcessQuotedValueCharacter - // Skip the current character if it is going to be escaped later - if (escaped) - { - sb.Append(c); - escaped = false; - continue; - } - - // If we are currently skipping empty spaces, skip - if (skipWhitespace) - { - if (TomlSyntax.IsEmptySpace(c)) - { - if (TomlSyntax.IsLineBreak(c)) - { - skipWhitespaceLineSkipped = true; - AdvanceLine(); - } - continue; - } - - if (!skipWhitespaceLineSkipped) - { - AddError("Non-whitespace character after trim marker."); - return null; - } - - skipWhitespaceLineSkipped = false; - skipWhitespace = false; - } - - // If we encounter an escape sequence... - if (isBasic && c == TomlSyntax.ESCAPE_SYMBOL) - { - var next = reader.Peek(); - var nc = (char)next; - if (next >= 0) - { - // ...and the next char is empty space, we must skip all whitespaces - if (TomlSyntax.IsEmptySpace(nc)) - { - skipWhitespace = true; - continue; - } - - // ...and we have \" or \, skip the character - if (nc == quote || nc == TomlSyntax.ESCAPE_SYMBOL) escaped = true; - } - } - - // Count the consecutive quotes - if (c == quote) - quotesEncountered++; - else - quotesEncountered = 0; - - // If the are three quotes, count them as closing quotes - if (quotesEncountered == 3) break; - - sb.Append(c); - } - - // TOML actually allows to have five ending quotes like - // """"" => "" belong to the string + """ is the actual ending - quotesEncountered = 0; - while ((cur = reader.Peek()) >= 0) - { - var c = (char)cur; - if (c == quote && ++quotesEncountered < 3) - { - sb.Append(c); - ConsumeChar(); - } - else break; - } - - // Remove last two quotes (third one wasn't included by default) - sb.Length -= 2; - if (!isBasic) return sb.ToString(); - if (sb.ToString().TryUnescape(out var res, out var ex)) return res; - AddError(ex.Message); - return null; - } - - #endregion - - #region Node creation - - private bool InsertNode(TomlNode node, TomlNode root, IList path) - { - var latestNode = root; - if (path.Count > 1) - for (var index = 0; index < path.Count - 1; index++) - { - var subkey = path[index]; - if (latestNode.TryGetNode(subkey, out var currentNode)) - { - if (currentNode.HasValue) - return AddError($"The key {".".Join(path)} already has a value assigned to it!"); - } - else - { - currentNode = new TomlTable(); - latestNode[subkey] = currentNode; - } - - latestNode = currentNode; - if (latestNode is TomlTable { IsInline: true }) - return AddError($"Cannot assign {".".Join(path)} because it will edit an immutable table."); - } - - if (latestNode.HasKey(path[path.Count - 1])) - return AddError($"The key {".".Join(path)} is already defined!"); - latestNode[path[path.Count - 1]] = node; - node.CollapseLevel = path.Count - 1; - return true; - } - - private TomlTable CreateTable(TomlNode root, IList path, bool arrayTable) - { - if (path.Count == 0) return null; - var latestNode = root; - for (var index = 0; index < path.Count; index++) - { - var subkey = path[index]; - - if (latestNode.TryGetNode(subkey, out var node)) - { - if (node.IsArray && arrayTable) - { - var arr = (TomlArray)node; - - if (!arr.IsTableArray) - { - AddError($"The array {".".Join(path)} cannot be redefined as an array table!"); - return null; - } - - if (index == path.Count - 1) - { - latestNode = new TomlTable(); - arr.Add(latestNode); - break; - } - - latestNode = arr[arr.ChildrenCount - 1]; - continue; - } - - if (node is TomlTable { IsInline: true }) - { - AddError($"Cannot create table {".".Join(path)} because it will edit an immutable table."); - return null; - } - - if (node.HasValue) - { - if (!(node is TomlArray { IsTableArray: true } array)) - { - AddError($"The key {".".Join(path)} has a value assigned to it!"); - return null; - } - - latestNode = array[array.ChildrenCount - 1]; - continue; - } - - if (index == path.Count - 1) - { - if (arrayTable && !node.IsArray) - { - AddError($"The table {".".Join(path)} cannot be redefined as an array table!"); - return null; - } - - if (node is TomlTable { isImplicit: false }) - { - AddError($"The table {".".Join(path)} is defined multiple times!"); - return null; - } - } - } - else - { - if (index == path.Count - 1 && arrayTable) - { - var table = new TomlTable(); - var arr = new TomlArray - { - IsTableArray = true - }; - arr.Add(table); - latestNode[subkey] = arr; - latestNode = table; - break; - } - - node = new TomlTable { isImplicit = true }; - latestNode[subkey] = node; - } - - latestNode = node; - } - - var result = (TomlTable)latestNode; - result.isImplicit = false; - return result; - } - - #endregion - - #region Misc parsing - - private string ParseComment() - { - ConsumeChar(); - var commentLine = reader.ReadLine()?.Trim() ?? ""; - if (commentLine.Any(ch => TomlSyntax.MustBeEscaped(ch))) - AddError("Comment must not contain control characters other than tab.", false); - return commentLine; - } - #endregion - } - - #endregion - - public static class TOML - { - public static bool ForceASCII { get; set; } = false; - - public static TomlTable Parse(TextReader reader) - { - using var parser = new TOMLParser(reader) { ForceASCII = ForceASCII }; - return parser.Parse(); - } - } - - #region Exception Types - - public class TomlFormatException : Exception - { - public TomlFormatException(string message) : base(message) { } - } - - public class TomlParseException : Exception - { - public TomlParseException(TomlTable parsed, IEnumerable exceptions) : - base("TOML file contains format errors") - { - ParsedTable = parsed; - SyntaxErrors = exceptions; - } - - public TomlTable ParsedTable { get; } - - public IEnumerable SyntaxErrors { get; } - } - - public class TomlSyntaxException : Exception - { - public TomlSyntaxException(string message, TOMLParser.ParseState state, int line, int col) : base(message) - { - ParseState = state; - Line = line; - Column = col; - } - - public TOMLParser.ParseState ParseState { get; } - - public int Line { get; } - - public int Column { get; } - } - - #endregion - - #region Parse utilities - - internal static class TomlSyntax - { - #region Type Patterns - - public const string TRUE_VALUE = "true"; - public const string FALSE_VALUE = "false"; - public const string NAN_VALUE = "nan"; - public const string POS_NAN_VALUE = "+nan"; - public const string NEG_NAN_VALUE = "-nan"; - public const string INF_VALUE = "inf"; - public const string POS_INF_VALUE = "+inf"; - public const string NEG_INF_VALUE = "-inf"; - - public static bool IsBoolean(string s) => s is TRUE_VALUE or FALSE_VALUE; - - public static bool IsPosInf(string s) => s is INF_VALUE or POS_INF_VALUE; - - public static bool IsNegInf(string s) => s == NEG_INF_VALUE; - - public static bool IsNaN(string s) => s is NAN_VALUE or POS_NAN_VALUE or NEG_NAN_VALUE; - - public static bool IsInteger(string s) => IntegerPattern.IsMatch(s); - - public static bool IsFloat(string s) => FloatPattern.IsMatch(s); - - public static bool IsIntegerWithBase(string s, out int numberBase) - { - numberBase = 10; - var match = BasedIntegerPattern.Match(s); - if (!match.Success) return false; - IntegerBases.TryGetValue(match.Groups["base"].Value, out numberBase); - return true; - } - - /** - * A pattern to verify the integer value according to the TOML specification. - */ - public static readonly Regex IntegerPattern = - new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)*)$", RegexOptions.Compiled); - - /** - * A pattern to verify a special 0x, 0o and 0b forms of an integer according to the TOML specification. - */ - public static readonly Regex BasedIntegerPattern = - new(@"^0(?x|b|o)(?!_)(_?[0-9A-F])*$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - /** - * A pattern to verify the float value according to the TOML specification. - */ - public static readonly Regex FloatPattern = - new(@"^(\+|-)?(?!_)(0|(?!0)(_?\d)+)(((e(\+|-)?(?!_)(_?\d)+)?)|(\.(?!_)(_?\d)+(e(\+|-)?(?!_)(_?\d)+)?))$", - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - /** - * A helper dictionary to map TOML base codes into the radii. - */ - public static readonly Dictionary IntegerBases = new() - { - ["x"] = 16, - ["o"] = 8, - ["b"] = 2 - }; - - /** - * A helper dictionary to map non-decimal bases to their TOML identifiers - */ - public static readonly Dictionary BaseIdentifiers = new() - { - [2] = "b", - [8] = "o", - [16] = "x" - }; - - public const string RFC3339EmptySeparator = " "; - public const string ISO861Separator = "T"; - public const string ISO861ZeroZone = "+00:00"; - public const string RFC3339ZeroZone = "Z"; - - /** - * Valid date formats with timezone as per RFC3339. - */ - public static readonly string[] RFC3339Formats = - { - "yyyy'-'MM-ddTHH':'mm':'ssK", "yyyy'-'MM-ddTHH':'mm':'ss'.'fK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffK", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffK", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffK", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffffK", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffffK" - }; - - /** - * Valid date formats without timezone (assumes local) as per RFC3339. - */ - public static readonly string[] RFC3339LocalDateTimeFormats = - { - "yyyy'-'MM-ddTHH':'mm':'ss", "yyyy'-'MM-ddTHH':'mm':'ss'.'f", "yyyy'-'MM-ddTHH':'mm':'ss'.'ff", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffff", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fffff", "yyyy'-'MM-ddTHH':'mm':'ss'.'ffffff", - "yyyy'-'MM-ddTHH':'mm':'ss'.'fffffff" - }; - - /** - * Valid full date format as per TOML spec. - */ - public static readonly string LocalDateFormat = "yyyy'-'MM'-'dd"; - - /** - * Valid time formats as per TOML spec. - */ - public static readonly string[] RFC3339LocalTimeFormats = - { - "HH':'mm':'ss", "HH':'mm':'ss'.'f", "HH':'mm':'ss'.'ff", "HH':'mm':'ss'.'fff", "HH':'mm':'ss'.'ffff", - "HH':'mm':'ss'.'fffff", "HH':'mm':'ss'.'ffffff", "HH':'mm':'ss'.'fffffff" - }; - - #endregion - - #region Character definitions - - public const char ARRAY_END_SYMBOL = ']'; - public const char ITEM_SEPARATOR = ','; - public const char ARRAY_START_SYMBOL = '['; - public const char BASIC_STRING_SYMBOL = '\"'; - public const char COMMENT_SYMBOL = '#'; - public const char ESCAPE_SYMBOL = '\\'; - public const char KEY_VALUE_SEPARATOR = '='; - public const char NEWLINE_CARRIAGE_RETURN_CHARACTER = '\r'; - public const char NEWLINE_CHARACTER = '\n'; - public const char SUBKEY_SEPARATOR = '.'; - public const char TABLE_END_SYMBOL = ']'; - public const char TABLE_START_SYMBOL = '['; - public const char INLINE_TABLE_START_SYMBOL = '{'; - public const char INLINE_TABLE_END_SYMBOL = '}'; - public const char LITERAL_STRING_SYMBOL = '\''; - public const char INT_NUMBER_SEPARATOR = '_'; - - public static readonly char[] NewLineCharacters = { NEWLINE_CHARACTER, NEWLINE_CARRIAGE_RETURN_CHARACTER }; - - public static bool IsQuoted(char c) => c is BASIC_STRING_SYMBOL or LITERAL_STRING_SYMBOL; - - public static bool IsWhiteSpace(char c) => c is ' ' or '\t'; - - public static bool IsNewLine(char c) => c is NEWLINE_CHARACTER or NEWLINE_CARRIAGE_RETURN_CHARACTER; - - public static bool IsLineBreak(char c) => c == NEWLINE_CHARACTER; - - public static bool IsEmptySpace(char c) => IsWhiteSpace(c) || IsNewLine(c); - - public static bool IsBareKey(char c) => - c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '_' or '-'; - - public static bool MustBeEscaped(char c, bool allowNewLines = false) - { - var result = c is (>= '\u0000' and <= '\u0008') or '\u000b' or '\u000c' or (>= '\u000e' and <= '\u001f') or '\u007f'; - if (!allowNewLines) - result |= c is >= '\u000a' and <= '\u000e'; - return result; - } - - public static bool IsValueSeparator(char c) => - c is ITEM_SEPARATOR or ARRAY_END_SYMBOL or INLINE_TABLE_END_SYMBOL; - - #endregion - } - - internal static class StringUtils - { - public static string AsKey(this string key) - { - var quote = key == string.Empty || key.Any(c => !TomlSyntax.IsBareKey(c)); - return !quote ? key : $"{TomlSyntax.BASIC_STRING_SYMBOL}{key.Escape()}{TomlSyntax.BASIC_STRING_SYMBOL}"; - } - - public static string Join(this string self, IEnumerable subItems) - { - var sb = new StringBuilder(); - var first = true; - - foreach (var subItem in subItems) - { - if (!first) sb.Append(self); - first = false; - sb.Append(subItem); - } - - return sb.ToString(); - } - - public delegate bool TryDateParseDelegate(string s, string format, IFormatProvider ci, DateTimeStyles dts, out T dt); - - public static bool TryParseDateTime(string s, - string[] formats, - DateTimeStyles styles, - TryDateParseDelegate parser, - out T dateTime, - out int parsedFormat) - { - parsedFormat = 0; - dateTime = default; - for (var i = 0; i < formats.Length; i++) - { - var format = formats[i]; - if (!parser(s, format, CultureInfo.InvariantCulture, styles, out dateTime)) continue; - parsedFormat = i; - return true; - } - - return false; - } - - public static void AsComment(this string self, TextWriter tw) - { - foreach (var line in self.Split(TomlSyntax.NEWLINE_CHARACTER)) - tw.WriteLine($"{TomlSyntax.COMMENT_SYMBOL} {line.Trim()}"); - } - - public static string RemoveAll(this string txt, char toRemove) - { - var sb = new StringBuilder(txt.Length); - foreach (var c in txt.Where(c => c != toRemove)) - sb.Append(c); - return sb.ToString(); - } - - public static string Escape(this string txt, bool escapeNewlines = true) - { - var stringBuilder = new StringBuilder(txt.Length + 2); - for (var i = 0; i < txt.Length; i++) - { - var c = txt[i]; - - static string CodePoint(string txt, ref int i, char c) => char.IsSurrogatePair(txt, i) - ? $"\\U{char.ConvertToUtf32(txt, i++):X8}" - : $"\\u{(ushort)c:X4}"; - - stringBuilder.Append(c switch - { - '\b' => @"\b", - '\t' => @"\t", - '\n' when escapeNewlines => @"\n", - '\f' => @"\f", - '\r' when escapeNewlines => @"\r", - '\\' => @"\\", - '\"' => @"\""", - var _ when TomlSyntax.MustBeEscaped(c, !escapeNewlines) || TOML.ForceASCII && c > sbyte.MaxValue => - CodePoint(txt, ref i, c), - var _ => c - }); - } - - return stringBuilder.ToString(); - } - - public static bool TryUnescape(this string txt, out string unescaped, out Exception exception) - { - try - { - exception = null; - unescaped = txt.Unescape(); - return true; - } - catch (Exception e) - { - exception = e; - unescaped = null; - return false; - } - } - - public static string Unescape(this string txt) - { - if (string.IsNullOrEmpty(txt)) return txt; - var stringBuilder = new StringBuilder(txt.Length); - for (var i = 0; i < txt.Length;) - { - var num = txt.IndexOf('\\', i); - var next = num + 1; - if (num < 0 || num == txt.Length - 1) num = txt.Length; - stringBuilder.Append(txt, i, num - i); - if (num >= txt.Length) break; - var c = txt[next]; - - static string CodePoint(int next, string txt, ref int num, int size) - { - if (next + size >= txt.Length) throw new Exception("Undefined escape sequence!"); - num += size; - return char.ConvertFromUtf32(Convert.ToInt32(txt.Substring(next + 1, size), 16)); - } - - stringBuilder.Append(c switch - { - 'b' => "\b", - 't' => "\t", - 'n' => "\n", - 'f' => "\f", - 'r' => "\r", - '\'' => "\'", - '\"' => "\"", - '\\' => "\\", - 'u' => CodePoint(next, txt, ref num, 4), - 'U' => CodePoint(next, txt, ref num, 8), - var _ => throw new Exception("Undefined escape sequence!") - }); - i = num + 2; - } - - return stringBuilder.ToString(); - } - } - - #endregion -} diff --git a/Assets/MCPForUnity/Editor/External/Tommy.cs.meta b/Assets/MCPForUnity/Editor/External/Tommy.cs.meta deleted file mode 100644 index 59e9ece..0000000 --- a/Assets/MCPForUnity/Editor/External/Tommy.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: ea652131dcdaa44ca8cb35cd1191be3f -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/External/Tommy.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers.meta b/Assets/MCPForUnity/Editor/Helpers.meta deleted file mode 100644 index c57a342..0000000 --- a/Assets/MCPForUnity/Editor/Helpers.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 94cb070dc5e15024da86150b27699ca0 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Helpers/AssetPathUtility.cs b/Assets/MCPForUnity/Editor/Helpers/AssetPathUtility.cs deleted file mode 100644 index 69c017e..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/AssetPathUtility.cs +++ /dev/null @@ -1,430 +0,0 @@ -using System; -using System.IO; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Services; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; -using PackageInfo = UnityEditor.PackageManager.PackageInfo; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Provides common utility methods for working with Unity asset paths. - /// - public static class AssetPathUtility - { - /// - /// Normalizes path separators to forward slashes without modifying the path structure. - /// Use this for non-asset paths (e.g., file system paths, relative directories). - /// - public static string NormalizeSeparators(string path) - { - if (string.IsNullOrEmpty(path)) - return path; - return path.Replace('\\', '/'); - } - - /// - /// Normalizes a Unity asset path by ensuring forward slashes are used and that it is rooted under "Assets/". - /// Also protects against path traversal attacks using "../" sequences. - /// - public static string SanitizeAssetPath(string path) - { - if (string.IsNullOrEmpty(path)) - { - return path; - } - - path = NormalizeSeparators(path); - - // Check for path traversal sequences - if (path.Contains("..")) - { - McpLog.Warn($"[AssetPathUtility] Path contains potential traversal sequence: '{path}'"); - return null; - } - - // Ensure path starts with Assets/ - if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) - { - return "Assets/" + path.TrimStart('/'); - } - - return path; - } - - /// - /// Checks if a given asset path is valid and safe (no traversal, within Assets folder). - /// - /// True if the path is valid, false otherwise. - public static bool IsValidAssetPath(string path) - { - if (string.IsNullOrEmpty(path)) - { - return false; - } - - // Normalize for comparison - string normalized = NormalizeSeparators(path); - - // Must start with Assets/ - if (!normalized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - // Must not contain traversal sequences - if (normalized.Contains("..")) - { - return false; - } - - // Must not contain invalid path characters - char[] invalidChars = { ':', '*', '?', '"', '<', '>', '|' }; - foreach (char c in invalidChars) - { - if (normalized.IndexOf(c) >= 0) - { - return false; - } - } - - return true; - } - - /// - /// Gets the MCP for Unity package root path. - /// Works for registry Package Manager, local Package Manager, and Asset Store installations. - /// - /// The package root path (virtual for PM, absolute for Asset Store), or null if not found - public static string GetMcpPackageRootPath() - { - try - { - // Try Package Manager first (registry and local installs) - var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly); - if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.assetPath)) - { - return packageInfo.assetPath; - } - - // Fallback to AssetDatabase for Asset Store installs (Assets/MCPForUnity) - string[] guids = AssetDatabase.FindAssets($"t:Script {nameof(AssetPathUtility)}"); - - if (guids.Length == 0) - { - McpLog.Warn("Could not find AssetPathUtility script in AssetDatabase"); - return null; - } - - string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]); - - // Script is at: {packageRoot}/Editor/Helpers/AssetPathUtility.cs - // Extract {packageRoot} - int editorIndex = scriptPath.IndexOf("/Editor/", StringComparison.Ordinal); - - if (editorIndex >= 0) - { - return scriptPath.Substring(0, editorIndex); - } - - McpLog.Warn($"Could not determine package root from script path: {scriptPath}"); - return null; - } - catch (Exception ex) - { - McpLog.Error($"Failed to get package root path: {ex.Message}"); - return null; - } - } - - /// - /// Reads and parses the package.json file for MCP for Unity. - /// Handles both Package Manager (registry/local) and Asset Store installations. - /// - /// JObject containing package.json data, or null if not found or parse failed - public static JObject GetPackageJson() - { - try - { - string packageRoot = GetMcpPackageRootPath(); - if (string.IsNullOrEmpty(packageRoot)) - { - return null; - } - - string packageJsonPath = Path.Combine(packageRoot, "package.json"); - - // Convert virtual asset path to file system path - if (packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase)) - { - // Package Manager install - must use PackageInfo.resolvedPath - // Virtual paths like "Packages/..." don't work with File.Exists() - // Registry packages live in Library/PackageCache/package@version/ - var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly); - if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.resolvedPath)) - { - packageJsonPath = Path.Combine(packageInfo.resolvedPath, "package.json"); - } - else - { - McpLog.Warn("Could not resolve Package Manager path for package.json"); - return null; - } - } - else if (packageRoot.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) - { - // Asset Store install - convert to absolute file system path - // Application.dataPath is the absolute path to the Assets folder - string relativePath = packageRoot.Substring("Assets/".Length); - packageJsonPath = Path.Combine(Application.dataPath, relativePath, "package.json"); - } - - if (!File.Exists(packageJsonPath)) - { - McpLog.Warn($"package.json not found at: {packageJsonPath}"); - return null; - } - - string json = File.ReadAllText(packageJsonPath); - return JObject.Parse(json); - } - catch (Exception ex) - { - McpLog.Warn($"Failed to read or parse package.json: {ex.Message}"); - return null; - } - } - - /// - /// Gets the package source for the MCP server (used with uvx --from). - /// Checks for EditorPrefs override first (supports git URLs, file:// paths, etc.), - /// then falls back to PyPI package reference. - /// - /// Package source string for uvx --from argument - public static string GetMcpServerPackageSource() - { - // Check for override first (supports git URLs, file:// paths, local paths) - string sourceOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, ""); - if (!string.IsNullOrEmpty(sourceOverride)) - { - return sourceOverride; - } - - // Default to PyPI package (avoids Windows long path issues with git clone) - string version = GetPackageVersion(); - if (version == "unknown") - { - // Fall back to latest PyPI version so configs remain valid in test scenarios - return "mcpforunityserver"; - } - - return $"mcpforunityserver=={version}"; - } - - /// - /// Deprecated: Use GetMcpServerPackageSource() instead. - /// Kept for backwards compatibility. - /// - [System.Obsolete("Use GetMcpServerPackageSource() instead")] - public static string GetMcpServerGitUrl() => GetMcpServerPackageSource(); - - /// - /// Gets structured uvx command parts for different client configurations - /// - /// Tuple containing (uvxPath, fromUrl, packageName) - public static (string uvxPath, string fromUrl, string packageName) GetUvxCommandParts() - { - string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); - string fromUrl = GetMcpServerPackageSource(); - string packageName = "mcp-for-unity"; - - return (uvxPath, fromUrl, packageName); - } - - /// - /// Builds the uvx package source arguments for the MCP server. - /// Handles beta server mode (prerelease from PyPI) vs standard mode (pinned version or override). - /// Centralizes the prerelease logic to avoid duplication between HTTP and stdio transports. - /// Priority: explicit fromUrl override > beta server mode > default package. - /// - /// Whether to quote the --from path (needed for command-line strings, not for arg lists) - /// The package source arguments (e.g., "--prerelease explicit --from mcpforunityserver>=0.0.0a0") - public static string GetBetaServerFromArgs(bool quoteFromPath = false) - { - // Explicit override (local path, git URL, etc.) always wins - string fromUrl = GetMcpServerPackageSource(); - string overrideUrl = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, ""); - if (!string.IsNullOrEmpty(overrideUrl)) - { - return $"--from {fromUrl}"; - } - - // Beta server mode: use prerelease from PyPI - bool useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true); - if (useBetaServer) - { - // Use --prerelease explicit with version specifier to only get prereleases of our package, - // not of dependencies (which can be broken on PyPI). - string fromValue = quoteFromPath ? "\"mcpforunityserver>=0.0.0a0\"" : "mcpforunityserver>=0.0.0a0"; - return $"--prerelease explicit --from {fromValue}"; - } - - // Standard mode: use pinned version from package.json - if (!string.IsNullOrEmpty(fromUrl)) - { - return $"--from {fromUrl}"; - } - - return string.Empty; - } - - /// - /// Builds the uvx package source arguments as a list (for JSON config builders). - /// Priority: explicit fromUrl override > beta server mode > default package. - /// - /// List of arguments to add to uvx command - public static System.Collections.Generic.IList GetBetaServerFromArgsList() - { - var args = new System.Collections.Generic.List(); - - // Explicit override (local path, git URL, etc.) always wins - string fromUrl = GetMcpServerPackageSource(); - string overrideUrl = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, ""); - if (!string.IsNullOrEmpty(overrideUrl)) - { - args.Add("--from"); - args.Add(fromUrl); - return args; - } - - // Beta server mode: use prerelease from PyPI - bool useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true); - if (useBetaServer) - { - args.Add("--prerelease"); - args.Add("explicit"); - args.Add("--from"); - args.Add("mcpforunityserver>=0.0.0a0"); - return args; - } - - // Standard mode: use pinned version from package.json - if (!string.IsNullOrEmpty(fromUrl)) - { - args.Add("--from"); - args.Add(fromUrl); - } - - return args; - } - - /// - /// Determines whether uvx should use --no-cache --refresh flags. - /// Returns true if DevModeForceServerRefresh is enabled OR if the server URL is a local path. - /// Local paths (file:// or absolute) always need fresh builds to avoid stale uvx cache. - /// Note: --reinstall is not supported by uvx and will cause a warning. - /// - public static bool ShouldForceUvxRefresh() - { - bool devForceRefresh = false; - try { devForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); } catch { } - - if (devForceRefresh) - return true; - - // Auto-enable force refresh when using a local path override. - return IsLocalServerPath(); - } - - /// - /// Returns true if the server URL is a local path (file:// or absolute path). - /// - public static bool IsLocalServerPath() - { - string fromUrl = GetMcpServerPackageSource(); - if (string.IsNullOrEmpty(fromUrl)) - return false; - - // Check for file:// protocol or absolute local path - return fromUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase) || - System.IO.Path.IsPathRooted(fromUrl); - } - - /// - /// Gets the local server path if GitUrlOverride points to a local directory. - /// Returns null if not using a local path. - /// - public static string GetLocalServerPath() - { - if (!IsLocalServerPath()) - return null; - - string fromUrl = GetMcpServerPackageSource(); - if (fromUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) - { - // Strip file:// prefix - fromUrl = fromUrl.Substring(7); - } - - return fromUrl; - } - - /// - /// Cleans stale Python build artifacts from the local server path. - /// This is necessary because Python's build system doesn't remove deleted files from build/, - /// and the auto-discovery mechanism will pick up old .py files causing ghost resources/tools. - /// - /// True if cleaning was performed, false if not applicable or failed. - public static bool CleanLocalServerBuildArtifacts() - { - string localPath = GetLocalServerPath(); - if (string.IsNullOrEmpty(localPath)) - return false; - - // Clean the build/ directory which can contain stale .py files - string buildPath = System.IO.Path.Combine(localPath, "build"); - if (System.IO.Directory.Exists(buildPath)) - { - try - { - System.IO.Directory.Delete(buildPath, recursive: true); - McpLog.Info($"Cleaned stale build artifacts from: {buildPath}"); - return true; - } - catch (Exception ex) - { - McpLog.Warn($"Failed to clean build artifacts: {ex.Message}"); - return false; - } - } - - return false; - } - - /// - /// Gets the package version from package.json - /// - /// Version string, or "unknown" if not found - public static string GetPackageVersion() - { - try - { - var packageJson = GetPackageJson(); - if (packageJson == null) - { - return "unknown"; - } - - string version = packageJson["version"]?.ToString(); - return string.IsNullOrEmpty(version) ? "unknown" : version; - } - catch (Exception ex) - { - McpLog.Warn($"Failed to get package version: {ex.Message}"); - return "unknown"; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/AssetPathUtility.cs.meta b/Assets/MCPForUnity/Editor/Helpers/AssetPathUtility.cs.meta deleted file mode 100644 index bce8a7f..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/AssetPathUtility.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 1d42f5b5ea5d4d43ad1a771e14bda2a0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/AssetPathUtility.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs b/Assets/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs deleted file mode 100644 index a68d47e..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs +++ /dev/null @@ -1,319 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Services; -using MCPForUnity.External.Tommy; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Codex CLI specific configuration helpers. Handles TOML snippet - /// generation and lightweight parsing so Codex can join the auto-setup - /// flow alongside JSON-based clients. - /// - public static class CodexConfigHelper - { - private static void AddDevModeArgs(TomlArray args) - { - if (args == null) return; - // Use central helper that checks both DevModeForceServerRefresh AND local path detection. - // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead - if (!AssetPathUtility.ShouldForceUvxRefresh()) return; - args.Add(new TomlString { Value = "--no-cache" }); - args.Add(new TomlString { Value = "--refresh" }); - } - - public static string BuildCodexServerBlock(string uvPath) - { - var table = new TomlTable(); - var mcpServers = new TomlTable(); - var unityMCP = new TomlTable(); - - // Check transport preference - bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true); - - if (useHttpTransport) - { - // HTTP mode: Use url field - string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); - unityMCP["url"] = new TomlString { Value = httpUrl }; - - // Enable Codex's Rust MCP client for HTTP/SSE transport - EnsureRmcpClientFeature(table); - } - else - { - // Stdio mode: Use command and args - var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts(); - - unityMCP["command"] = uvxPath; - - var args = new TomlArray(); - AddDevModeArgs(args); - // Use centralized helper for beta server / prerelease args - foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList()) - { - args.Add(new TomlString { Value = arg }); - } - args.Add(new TomlString { Value = packageName }); - args.Add(new TomlString { Value = "--transport" }); - args.Add(new TomlString { Value = "stdio" }); - - unityMCP["args"] = args; - - // Add Windows-specific environment configuration for stdio mode - var platformService = MCPServiceLocator.Platform; - if (platformService.IsWindows()) - { - var envTable = new TomlTable { IsInline = true }; - envTable["SystemRoot"] = new TomlString { Value = platformService.GetSystemRoot() }; - unityMCP["env"] = envTable; - } - - // Allow extra time for uvx to download packages on first run - unityMCP["startup_timeout_sec"] = new TomlInteger { Value = 60 }; - } - - mcpServers["unityMCP"] = unityMCP; - table["mcp_servers"] = mcpServers; - - using var writer = new StringWriter(); - table.WriteTo(writer); - return writer.ToString(); - } - - public static string UpsertCodexServerBlock(string existingToml, string uvPath) - { - // Parse existing TOML or create new root table - var root = TryParseToml(existingToml) ?? new TomlTable(); - - bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true); - - // Ensure mcp_servers table exists - if (!root.TryGetNode("mcp_servers", out var mcpServersNode) || !(mcpServersNode is TomlTable)) - { - root["mcp_servers"] = new TomlTable(); - } - var mcpServers = root["mcp_servers"] as TomlTable; - - // Create or update unityMCP table - mcpServers["unityMCP"] = CreateUnityMcpTable(uvPath); - - if (useHttpTransport) - { - EnsureRmcpClientFeature(root); - } - - // Serialize back to TOML - using var writer = new StringWriter(); - root.WriteTo(writer); - return writer.ToString(); - } - - public static bool TryParseCodexServer(string toml, out string command, out string[] args) - { - return TryParseCodexServer(toml, out command, out args, out _); - } - - public static bool TryParseCodexServer(string toml, out string command, out string[] args, out string url) - { - command = null; - args = null; - url = null; - - var root = TryParseToml(toml); - if (root == null) return false; - - if (!TryGetTable(root, "mcp_servers", out var servers) - && !TryGetTable(root, "mcpServers", out servers)) - { - return false; - } - - if (!TryGetTable(servers, "unityMCP", out var unity)) - { - return false; - } - - // Check for HTTP mode (url field) - url = GetTomlString(unity, "url"); - if (!string.IsNullOrEmpty(url)) - { - // HTTP mode detected - return true with url - return true; - } - - // Check for stdio mode (command + args) - command = GetTomlString(unity, "command"); - args = GetTomlStringArray(unity, "args"); - - return !string.IsNullOrEmpty(command) && args != null; - } - - /// - /// Safely parses TOML string, returning null on failure - /// - private static TomlTable TryParseToml(string toml) - { - if (string.IsNullOrWhiteSpace(toml)) return null; - - try - { - using var reader = new StringReader(toml); - return TOML.Parse(reader); - } - catch (TomlParseException) - { - return null; - } - catch (TomlSyntaxException) - { - return null; - } - catch (FormatException) - { - return null; - } - } - - /// - /// Creates a TomlTable for the unityMCP server configuration - /// - /// Path to uv executable (used as fallback if uvx is not available) - private static TomlTable CreateUnityMcpTable(string uvPath) - { - var unityMCP = new TomlTable(); - - // Check transport preference - bool useHttpTransport = EditorPrefs.GetBool(MCPForUnity.Editor.Constants.EditorPrefKeys.UseHttpTransport, true); - - if (useHttpTransport) - { - // HTTP mode: Use url field - string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); - unityMCP["url"] = new TomlString { Value = httpUrl }; - } - else - { - // Stdio mode: Use command and args - var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts(); - - unityMCP["command"] = new TomlString { Value = uvxPath }; - - var argsArray = new TomlArray(); - AddDevModeArgs(argsArray); - // Use centralized helper for beta server / prerelease args - foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList()) - { - argsArray.Add(new TomlString { Value = arg }); - } - argsArray.Add(new TomlString { Value = packageName }); - argsArray.Add(new TomlString { Value = "--transport" }); - argsArray.Add(new TomlString { Value = "stdio" }); - unityMCP["args"] = argsArray; - - // Add Windows-specific environment configuration for stdio mode - var platformService = MCPServiceLocator.Platform; - if (platformService.IsWindows()) - { - var envTable = new TomlTable { IsInline = true }; - envTable["SystemRoot"] = new TomlString { Value = platformService.GetSystemRoot() }; - unityMCP["env"] = envTable; - } - - // Allow extra time for uvx to download packages on first run - unityMCP["startup_timeout_sec"] = new TomlInteger { Value = 60 }; - } - - return unityMCP; - } - - /// - /// Ensures the features table contains the rmcp_client flag for HTTP/SSE transport. - /// - private static void EnsureRmcpClientFeature(TomlTable root) - { - if (root == null) return; - - if (!root.TryGetNode("features", out var featuresNode) || featuresNode is not TomlTable features) - { - features = new TomlTable(); - root["features"] = features; - } - - features["rmcp_client"] = new TomlBoolean { Value = true }; - } - - private static bool TryGetTable(TomlTable parent, string key, out TomlTable table) - { - table = null; - if (parent == null) return false; - - if (parent.TryGetNode(key, out var node)) - { - if (node is TomlTable tbl) - { - table = tbl; - return true; - } - - if (node is TomlArray array) - { - var firstTable = array.Children.OfType().FirstOrDefault(); - if (firstTable != null) - { - table = firstTable; - return true; - } - } - } - - return false; - } - - private static string GetTomlString(TomlTable table, string key) - { - if (table != null && table.TryGetNode(key, out var node)) - { - if (node is TomlString str) return str.Value; - if (node.HasValue) return node.ToString(); - } - return null; - } - - private static string[] GetTomlStringArray(TomlTable table, string key) - { - if (table == null) return null; - if (!table.TryGetNode(key, out var node)) return null; - - if (node is TomlArray array) - { - List values = new List(); - foreach (TomlNode element in array.Children) - { - if (element is TomlString str) - { - values.Add(str.Value); - } - else if (element.HasValue) - { - values.Add(element.ToString()); - } - } - - return values.Count > 0 ? values.ToArray() : Array.Empty(); - } - - if (node is TomlString single) - { - return new[] { single.Value }; - } - - return null; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs.meta b/Assets/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs.meta deleted file mode 100644 index 5409554..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b3e68082ffc0b4cd39d3747673a4cc22 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/ComponentOps.cs b/Assets/MCPForUnity/Editor/Helpers/ComponentOps.cs deleted file mode 100644 index 90f1e7a..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ComponentOps.cs +++ /dev/null @@ -1,349 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Low-level component operations extracted from ManageGameObject and ManageComponents. - /// Provides pure C# operations without JSON parsing or response formatting. - /// - public static class ComponentOps - { - /// - /// Adds a component to a GameObject with Undo support. - /// - /// The target GameObject - /// The type of component to add - /// Error message if operation fails - /// The added component, or null if failed - public static Component AddComponent(GameObject target, Type componentType, out string error) - { - error = null; - - if (target == null) - { - error = "Target GameObject is null."; - return null; - } - - if (componentType == null || !typeof(Component).IsAssignableFrom(componentType)) - { - error = $"Type '{componentType?.Name ?? "null"}' is not a valid Component type."; - return null; - } - - // Prevent adding duplicate Transform - if (componentType == typeof(Transform)) - { - error = "Cannot add another Transform component."; - return null; - } - - // Check for 2D/3D physics conflicts - string conflictError = CheckPhysicsConflict(target, componentType); - if (conflictError != null) - { - error = conflictError; - return null; - } - - try - { - Component newComponent = Undo.AddComponent(target, componentType); - if (newComponent == null) - { - error = $"Failed to add component '{componentType.Name}' to '{target.name}'. It might be disallowed."; - return null; - } - - // Apply default values for specific component types - ApplyDefaultValues(newComponent); - - return newComponent; - } - catch (Exception ex) - { - error = $"Error adding component '{componentType.Name}': {ex.Message}"; - return null; - } - } - - /// - /// Removes a component from a GameObject with Undo support. - /// - /// The target GameObject - /// The type of component to remove - /// Error message if operation fails - /// True if component was removed successfully - public static bool RemoveComponent(GameObject target, Type componentType, out string error) - { - error = null; - - if (target == null) - { - error = "Target GameObject is null."; - return false; - } - - if (componentType == null) - { - error = "Component type is null."; - return false; - } - - // Prevent removing Transform - if (componentType == typeof(Transform)) - { - error = "Cannot remove Transform component."; - return false; - } - - Component component = target.GetComponent(componentType); - if (component == null) - { - error = $"Component '{componentType.Name}' not found on '{target.name}'."; - return false; - } - - try - { - Undo.DestroyObjectImmediate(component); - return true; - } - catch (Exception ex) - { - error = $"Error removing component '{componentType.Name}': {ex.Message}"; - return false; - } - } - - /// - /// Sets a property value on a component using reflection. - /// - /// The target component - /// The property or field name - /// The value to set (JToken) - /// Error message if operation fails - /// True if property was set successfully - public static bool SetProperty(Component component, string propertyName, JToken value, out string error) - { - error = null; - - if (component == null) - { - error = "Component is null."; - return false; - } - - if (string.IsNullOrEmpty(propertyName)) - { - error = "Property name is null or empty."; - return false; - } - - Type type = component.GetType(); - BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; - string normalizedName = ParamCoercion.NormalizePropertyName(propertyName); - - // Try property first - check both original and normalized names for backwards compatibility - PropertyInfo propInfo = type.GetProperty(propertyName, flags) - ?? type.GetProperty(normalizedName, flags); - if (propInfo != null && propInfo.CanWrite) - { - try - { - object convertedValue = PropertyConversion.ConvertToType(value, propInfo.PropertyType); - // Detect conversion failure: null result when input wasn't null - if (convertedValue == null && value.Type != JTokenType.Null) - { - error = $"Failed to convert value for property '{propertyName}' to type '{propInfo.PropertyType.Name}'."; - return false; - } - propInfo.SetValue(component, convertedValue); - return true; - } - catch (Exception ex) - { - error = $"Failed to set property '{propertyName}': {ex.Message}"; - return false; - } - } - - // Try field - check both original and normalized names for backwards compatibility - FieldInfo fieldInfo = type.GetField(propertyName, flags) - ?? type.GetField(normalizedName, flags); - if (fieldInfo != null && !fieldInfo.IsInitOnly) - { - try - { - object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType); - // Detect conversion failure: null result when input wasn't null - if (convertedValue == null && value.Type != JTokenType.Null) - { - error = $"Failed to convert value for field '{propertyName}' to type '{fieldInfo.FieldType.Name}'."; - return false; - } - fieldInfo.SetValue(component, convertedValue); - return true; - } - catch (Exception ex) - { - error = $"Failed to set field '{propertyName}': {ex.Message}"; - return false; - } - } - - // Try non-public serialized fields - traverse inheritance hierarchy - // Type.GetField() with NonPublic only finds fields declared directly on that type, - // so we need to walk up the inheritance chain manually - fieldInfo = FindSerializedFieldInHierarchy(type, propertyName) - ?? FindSerializedFieldInHierarchy(type, normalizedName); - if (fieldInfo != null) - { - try - { - object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType); - // Detect conversion failure: null result when input wasn't null - if (convertedValue == null && value.Type != JTokenType.Null) - { - error = $"Failed to convert value for serialized field '{propertyName}' to type '{fieldInfo.FieldType.Name}'."; - return false; - } - fieldInfo.SetValue(component, convertedValue); - return true; - } - catch (Exception ex) - { - error = $"Failed to set serialized field '{propertyName}': {ex.Message}"; - return false; - } - } - - error = $"Property or field '{propertyName}' not found on component '{type.Name}'."; - return false; - } - - /// - /// Gets all public properties and fields from a component type. - /// - public static List GetAccessibleMembers(Type componentType) - { - var members = new List(); - if (componentType == null) return members; - - BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; - - foreach (var prop in componentType.GetProperties(flags)) - { - if (prop.CanWrite && prop.GetSetMethod() != null) - { - members.Add(prop.Name); - } - } - - foreach (var field in componentType.GetFields(flags)) - { - if (!field.IsInitOnly) - { - members.Add(field.Name); - } - } - - // Include private [SerializeField] fields - traverse inheritance hierarchy - // Type.GetFields with NonPublic only returns fields declared directly on that type, - // so we need to walk up the chain to find inherited private serialized fields - var seenFieldNames = new HashSet(members); // Avoid duplicates with public fields - Type currentType = componentType; - while (currentType != null && currentType != typeof(object)) - { - foreach (var field in currentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)) - { - if (field.GetCustomAttribute() != null && !seenFieldNames.Contains(field.Name)) - { - members.Add(field.Name); - seenFieldNames.Add(field.Name); - } - } - currentType = currentType.BaseType; - } - - members.Sort(); - return members; - } - - // --- Private Helpers --- - - /// - /// Searches for a non-public [SerializeField] field through the entire inheritance hierarchy. - /// Type.GetField() with NonPublic only returns fields declared directly on that type, - /// so this method walks up the chain to find inherited private serialized fields. - /// - private static FieldInfo FindSerializedFieldInHierarchy(Type type, string fieldName) - { - if (type == null || string.IsNullOrEmpty(fieldName)) - return null; - - BindingFlags privateFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; - Type currentType = type; - - // Walk up the inheritance chain - while (currentType != null && currentType != typeof(object)) - { - // Search for the field on this specific type (case-insensitive) - foreach (var field in currentType.GetFields(privateFlags)) - { - if (string.Equals(field.Name, fieldName, StringComparison.OrdinalIgnoreCase) && - field.GetCustomAttribute() != null) - { - return field; - } - } - currentType = currentType.BaseType; - } - - return null; - } - - private static string CheckPhysicsConflict(GameObject target, Type componentType) - { - bool isAdding2DPhysics = - typeof(Rigidbody2D).IsAssignableFrom(componentType) || - typeof(Collider2D).IsAssignableFrom(componentType); - - bool isAdding3DPhysics = - typeof(Rigidbody).IsAssignableFrom(componentType) || - typeof(Collider).IsAssignableFrom(componentType); - - if (isAdding2DPhysics) - { - if (target.GetComponent() != null || target.GetComponent() != null) - { - return $"Cannot add 2D physics component '{componentType.Name}' because the GameObject '{target.name}' already has a 3D Rigidbody or Collider."; - } - } - else if (isAdding3DPhysics) - { - if (target.GetComponent() != null || target.GetComponent() != null) - { - return $"Cannot add 3D physics component '{componentType.Name}' because the GameObject '{target.name}' already has a 2D Rigidbody or Collider."; - } - } - - return null; - } - - private static void ApplyDefaultValues(Component component) - { - // Default newly added Lights to Directional - if (component is Light light) - { - light.type = LightType.Directional; - } - } - } -} - diff --git a/Assets/MCPForUnity/Editor/Helpers/ComponentOps.cs.meta b/Assets/MCPForUnity/Editor/Helpers/ComponentOps.cs.meta deleted file mode 100644 index 1cff988..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ComponentOps.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 13dead161bc4540eeb771961df437779 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/ComponentOps.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs b/Assets/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs deleted file mode 100644 index 938d33c..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Services; -using MCPForUnity.Editor.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - public static class ConfigJsonBuilder - { - public static string BuildManualConfigJson(string uvPath, McpClient client) - { - var root = new JObject(); - bool isVSCode = client?.IsVsCodeLayout == true; - JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers"); - - var unity = new JObject(); - PopulateUnityNode(unity, uvPath, client, isVSCode); - - container["unityMCP"] = unity; - - return root.ToString(Formatting.Indented); - } - - public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, McpClient client) - { - if (root == null) root = new JObject(); - bool isVSCode = client?.IsVsCodeLayout == true; - JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers"); - JObject unity = container["unityMCP"] as JObject ?? new JObject(); - PopulateUnityNode(unity, uvPath, client, isVSCode); - - container["unityMCP"] = unity; - return root; - } - - /// - /// Centralized builder that applies all caveats consistently. - /// - Sets command/args with uvx and package version - /// - Ensures env exists - /// - Adds transport configuration (HTTP or stdio) - /// - Adds disabled:false for Windsurf/Kiro only when missing - /// - private static void PopulateUnityNode(JObject unity, string uvPath, McpClient client, bool isVSCode) - { - // Get transport preference (default to HTTP) - bool prefValue = EditorConfigurationCache.Instance.UseHttpTransport; - bool clientSupportsHttp = client?.SupportsHttpTransport != false; - bool useHttpTransport = clientSupportsHttp && prefValue; - string httpProperty = string.IsNullOrEmpty(client?.HttpUrlProperty) ? "url" : client.HttpUrlProperty; - var urlPropsToRemove = new HashSet(StringComparer.OrdinalIgnoreCase) { "url", "serverUrl" }; - urlPropsToRemove.Remove(httpProperty); - - if (useHttpTransport) - { - // HTTP mode: Use URL, no command - string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); - unity[httpProperty] = httpUrl; - - foreach (var prop in urlPropsToRemove) - { - if (unity[prop] != null) unity.Remove(prop); - } - - // Remove command/args if they exist from previous config - if (unity["command"] != null) unity.Remove("command"); - if (unity["args"] != null) unity.Remove("args"); - - // Only include API key header for remote-hosted mode - if (HttpEndpointUtility.IsRemoteScope()) - { - string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); - if (!string.IsNullOrEmpty(apiKey)) - { - var headers = new JObject { [AuthConstants.ApiKeyHeader] = apiKey }; - unity["headers"] = headers; - } - else - { - if (unity["headers"] != null) unity.Remove("headers"); - } - } - else - { - // Local HTTP doesn't use API keys; remove any stale headers - if (unity["headers"] != null) unity.Remove("headers"); - } - - if (isVSCode) - { - unity["type"] = "http"; - } - // Also add type for Claude Code (uses mcpServers layout but needs type field) - else if (client?.name == "Claude Code") - { - unity["type"] = "http"; - } - } - else - { - // Stdio mode: Use uvx command - var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts(); - - var toolArgs = BuildUvxArgs(fromUrl, packageName); - - unity["command"] = uvxPath; - unity["args"] = JArray.FromObject(toolArgs.ToArray()); - - // Remove url/serverUrl if they exist from previous config - if (unity["url"] != null) unity.Remove("url"); - if (unity["serverUrl"] != null) unity.Remove("serverUrl"); - - if (isVSCode) - { - unity["type"] = "stdio"; - } - } - - // Remove type for non-VSCode clients (except Claude Code which needs it) - if (!isVSCode && client?.name != "Claude Code" && unity["type"] != null) - { - unity.Remove("type"); - } - - bool requiresEnv = client?.EnsureEnvObject == true; - bool stripEnv = client?.StripEnvWhenNotRequired == true; - - if (requiresEnv) - { - if (unity["env"] == null) - { - unity["env"] = new JObject(); - } - } - else if (stripEnv && unity["env"] != null) - { - unity.Remove("env"); - } - - if (client?.DefaultUnityFields != null) - { - foreach (var kvp in client.DefaultUnityFields) - { - if (unity[kvp.Key] == null) - { - unity[kvp.Key] = kvp.Value != null ? JToken.FromObject(kvp.Value) : JValue.CreateNull(); - } - } - } - } - - private static JObject EnsureObject(JObject parent, string name) - { - if (parent[name] is JObject o) return o; - var created = new JObject(); - parent[name] = created; - return created; - } - - private static IList BuildUvxArgs(string fromUrl, string packageName) - { - // Dev mode: force a fresh install/resolution (avoids stale cached builds while iterating). - // `--no-cache` avoids reading from cache; `--refresh` ensures metadata is revalidated. - // Note: --reinstall is not supported by uvx and will cause a warning. - // Keep ordering consistent with other uvx builders: dev flags first, then --from , then package name. - var args = new List(); - - // Use central helper that checks both DevModeForceServerRefresh AND local path detection. - if (AssetPathUtility.ShouldForceUvxRefresh()) - { - args.Add("--no-cache"); - args.Add("--refresh"); - } - - // Use centralized helper for beta server / prerelease args - foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList()) - { - args.Add(arg); - } - args.Add(packageName); - - args.Add("--transport"); - args.Add("stdio"); - - return args; - } - - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs.meta b/Assets/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs.meta deleted file mode 100644 index 1e46e14..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 5c07c3369f73943919d9e086a81d1dcc -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/ExecPath.cs b/Assets/MCPForUnity/Editor/Helpers/ExecPath.cs deleted file mode 100644 index 3801a03..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ExecPath.cs +++ /dev/null @@ -1,324 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using MCPForUnity.Editor.Constants; -using UnityEditor; - -namespace MCPForUnity.Editor.Helpers -{ - internal static class ExecPath - { - private const string PrefClaude = EditorPrefKeys.ClaudeCliPathOverride; - - // Resolve Claude CLI absolute path. Pref → env → common locations → PATH. - internal static string ResolveClaude() - { - try - { - string pref = EditorPrefs.GetString(PrefClaude, string.Empty); - if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref; - } - catch { } - - string env = Environment.GetEnvironmentVariable("CLAUDE_CLI"); - if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; - string[] candidates = - { - "/opt/homebrew/bin/claude", - "/usr/local/bin/claude", - Path.Combine(home, ".local", "bin", "claude"), - }; - foreach (string c in candidates) { if (File.Exists(c)) return c; } - // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude - string nvmClaude = ResolveClaudeFromNvm(home); - if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude; -#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX - return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"); -#else - return null; -#endif - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { -#if UNITY_EDITOR_WIN - // Common npm global locations - string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; - string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; - string[] candidates = - { - // Prefer .cmd (most reliable from non-interactive processes) - Path.Combine(appData, "npm", "claude.cmd"), - Path.Combine(localAppData, "npm", "claude.cmd"), - // Fall back to PowerShell shim if only .ps1 is present - Path.Combine(appData, "npm", "claude.ps1"), - Path.Combine(localAppData, "npm", "claude.ps1"), - }; - foreach (string c in candidates) { if (File.Exists(c)) return c; } - string fromWhere = FindInPathWindows("claude.exe") ?? FindInPathWindows("claude.cmd") ?? FindInPathWindows("claude.ps1") ?? FindInPathWindows("claude"); - if (!string.IsNullOrEmpty(fromWhere)) return fromWhere; -#endif - return null; - } - - // Linux - { - string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; - string[] candidates = - { - "/usr/local/bin/claude", - "/usr/bin/claude", - Path.Combine(home, ".local", "bin", "claude"), - }; - foreach (string c in candidates) { if (File.Exists(c)) return c; } - // Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude - string nvmClaude = ResolveClaudeFromNvm(home); - if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude; -#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX - return Which("claude", "/usr/local/bin:/usr/bin:/bin"); -#else - return null; -#endif - } - } - - // Attempt to resolve claude from NVM-managed Node installations, choosing the newest version - private static string ResolveClaudeFromNvm(string home) - { - try - { - if (string.IsNullOrEmpty(home)) return null; - string nvmNodeDir = Path.Combine(home, ".nvm", "versions", "node"); - if (!Directory.Exists(nvmNodeDir)) return null; - - string bestPath = null; - Version bestVersion = null; - foreach (string versionDir in Directory.EnumerateDirectories(nvmNodeDir)) - { - string name = Path.GetFileName(versionDir); - if (string.IsNullOrEmpty(name)) continue; - if (name.StartsWith("v", StringComparison.OrdinalIgnoreCase)) - { - // Extract numeric portion: e.g., v18.19.0-nightly -> 18.19.0 - string versionStr = name.Substring(1); - int dashIndex = versionStr.IndexOf('-'); - if (dashIndex > 0) - { - versionStr = versionStr.Substring(0, dashIndex); - } - if (Version.TryParse(versionStr, out Version parsed)) - { - string candidate = Path.Combine(versionDir, "bin", "claude"); - if (File.Exists(candidate)) - { - if (bestVersion == null || parsed > bestVersion) - { - bestVersion = parsed; - bestPath = candidate; - } - } - } - } - } - return bestPath; - } - catch { return null; } - } - - // Explicitly set the Claude CLI absolute path override in EditorPrefs - internal static void SetClaudeCliPath(string absolutePath) - { - try - { - if (!string.IsNullOrEmpty(absolutePath) && File.Exists(absolutePath)) - { - EditorPrefs.SetString(PrefClaude, absolutePath); - } - } - catch { } - } - - // Clear any previously set Claude CLI override path - internal static void ClearClaudeCliPath() - { - try - { - if (EditorPrefs.HasKey(PrefClaude)) - { - EditorPrefs.DeleteKey(PrefClaude); - } - } - catch { } - } - - internal static bool TryRun( - string file, - string args, - string workingDir, - out string stdout, - out string stderr, - int timeoutMs = 15000, - string extraPathPrepend = null) - { - stdout = string.Empty; - stderr = string.Empty; - try - { - // Handle PowerShell scripts on Windows by invoking through powershell.exe - bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && - file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase); - - var psi = new ProcessStartInfo - { - FileName = isPs1 ? "powershell.exe" : file, - Arguments = isPs1 - ? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim() - : args, - WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }; - if (!string.IsNullOrEmpty(extraPathPrepend)) - { - string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; - psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) - ? extraPathPrepend - : (extraPathPrepend + System.IO.Path.PathSeparator + currentPath); - } - - using var process = new Process { StartInfo = psi, EnableRaisingEvents = false }; - - var sb = new StringBuilder(); - var se = new StringBuilder(); - process.OutputDataReceived += (_, e) => { if (e.Data != null) sb.AppendLine(e.Data); }; - process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); }; - - if (!process.Start()) return false; - - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - - if (!process.WaitForExit(timeoutMs)) - { - try { process.Kill(); } catch { } - return false; - } - - // Ensure async buffers are flushed - process.WaitForExit(); - - stdout = sb.ToString(); - stderr = se.ToString(); - return process.ExitCode == 0; - } - catch - { - return false; - } - } - - /// - /// Cross-platform path lookup. Uses 'where' on Windows, 'which' on macOS/Linux. - /// Returns the full path if found, null otherwise. - /// - internal static string FindInPath(string executable, string extraPathPrepend = null) - { -#if UNITY_EDITOR_WIN - return FindInPathWindows(executable, extraPathPrepend); -#elif UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX - return Which(executable, extraPathPrepend ?? string.Empty); -#else - return null; -#endif - } - -#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX - private static string Which(string exe, string prependPath) - { - try - { - var psi = new ProcessStartInfo("/usr/bin/which", exe) - { - UseShellExecute = false, - RedirectStandardOutput = true, - CreateNoWindow = true, - }; - string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; - psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path); - - using var p = Process.Start(psi); - if (p == null) return null; - - var so = new StringBuilder(); - p.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); }; - p.BeginOutputReadLine(); - - if (!p.WaitForExit(1500)) - { - try { p.Kill(); } catch { } - return null; - } - - p.WaitForExit(); - string output = so.ToString().Trim(); - return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null; - } - catch { return null; } - } -#endif - -#if UNITY_EDITOR_WIN - private static string FindInPathWindows(string exe, string extraPathPrepend = null) - { - try - { - string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; - string effectivePath = string.IsNullOrEmpty(extraPathPrepend) - ? currentPath - : (string.IsNullOrEmpty(currentPath) ? extraPathPrepend : extraPathPrepend + Path.PathSeparator + currentPath); - - var psi = new ProcessStartInfo("where", exe) - { - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }; - if (!string.IsNullOrEmpty(effectivePath)) - { - psi.EnvironmentVariables["PATH"] = effectivePath; - } - - using var p = Process.Start(psi); - if (p == null) return null; - - var so = new StringBuilder(); - p.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); }; - p.BeginOutputReadLine(); - - if (!p.WaitForExit(1500)) - { - try { p.Kill(); } catch { } - return null; - } - - p.WaitForExit(); - string first = so.ToString() - .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) - .FirstOrDefault(); - return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null; - } - catch { return null; } - } -#endif - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/ExecPath.cs.meta b/Assets/MCPForUnity/Editor/Helpers/ExecPath.cs.meta deleted file mode 100644 index ce47b41..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ExecPath.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 8f2b7b3e9c3e4a0f9b2a1d4c7e6f5a12 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/ExecPath.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/GameObjectLookup.cs b/Assets/MCPForUnity/Editor/Helpers/GameObjectLookup.cs deleted file mode 100644 index bd23bbd..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/GameObjectLookup.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; -using UnityEngine.SceneManagement; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Utility class for finding and looking up GameObjects in the scene. - /// Provides search functionality by name, tag, layer, component, path, and instance ID. - /// - public static class GameObjectLookup - { - /// - /// Supported search methods for finding GameObjects. - /// - public enum SearchMethod - { - ByName, - ByTag, - ByLayer, - ByComponent, - ByPath, - ById - } - - /// - /// Parses a search method string into the enum value. - /// - public static SearchMethod ParseSearchMethod(string method) - { - if (string.IsNullOrEmpty(method)) - return SearchMethod.ByName; - - return method.ToLowerInvariant() switch - { - "by_name" => SearchMethod.ByName, - "by_tag" => SearchMethod.ByTag, - "by_layer" => SearchMethod.ByLayer, - "by_component" => SearchMethod.ByComponent, - "by_path" => SearchMethod.ByPath, - "by_id" => SearchMethod.ById, - _ => SearchMethod.ByName - }; - } - - /// - /// Finds a single GameObject based on the target and search method. - /// - /// The target identifier (name, ID, path, etc.) - /// The search method to use - /// Whether to include inactive objects - /// The found GameObject or null - public static GameObject FindByTarget(JToken target, string searchMethod, bool includeInactive = false) - { - if (target == null) - return null; - - var results = SearchGameObjects(searchMethod, target.ToString(), includeInactive, 1); - return results.Count > 0 ? FindById(results[0]) : null; - } - - /// - /// Finds a GameObject by its instance ID. - /// - public static GameObject FindById(int instanceId) - { -#pragma warning disable CS0618 // Type or member is obsolete - return EditorUtility.InstanceIDToObject(instanceId) as GameObject; -#pragma warning restore CS0618 - } - - /// - /// Searches for GameObjects and returns their instance IDs. - /// - /// The search method string (by_name, by_tag, etc.) - /// The term to search for - /// Whether to include inactive objects - /// Maximum number of results to return (0 = unlimited) - /// List of instance IDs - public static List SearchGameObjects(string searchMethod, string searchTerm, bool includeInactive = false, int maxResults = 0) - { - var method = ParseSearchMethod(searchMethod); - return SearchGameObjects(method, searchTerm, includeInactive, maxResults); - } - - /// - /// Searches for GameObjects and returns their instance IDs. - /// - /// The search method - /// The term to search for - /// Whether to include inactive objects - /// Maximum number of results to return (0 = unlimited) - /// List of instance IDs - public static List SearchGameObjects(SearchMethod method, string searchTerm, bool includeInactive = false, int maxResults = 0) - { - var results = new List(); - - switch (method) - { - case SearchMethod.ById: - if (int.TryParse(searchTerm, out int instanceId)) - { -#pragma warning disable CS0618 // Type or member is obsolete - var obj = EditorUtility.InstanceIDToObject(instanceId) as GameObject; -#pragma warning restore CS0618 - if (obj != null && (includeInactive || obj.activeInHierarchy)) - { - results.Add(instanceId); - } - } - break; - - case SearchMethod.ByName: - results.AddRange(SearchByName(searchTerm, includeInactive, maxResults)); - break; - - case SearchMethod.ByPath: - results.AddRange(SearchByPath(searchTerm, includeInactive)); - break; - - case SearchMethod.ByTag: - results.AddRange(SearchByTag(searchTerm, includeInactive, maxResults)); - break; - - case SearchMethod.ByLayer: - results.AddRange(SearchByLayer(searchTerm, includeInactive, maxResults)); - break; - - case SearchMethod.ByComponent: - results.AddRange(SearchByComponent(searchTerm, includeInactive, maxResults)); - break; - } - - return results; - } - - private static IEnumerable SearchByName(string name, bool includeInactive, int maxResults) - { - var allObjects = GetAllSceneObjects(includeInactive); - var matching = allObjects.Where(go => go.name == name); - - if (maxResults > 0) - matching = matching.Take(maxResults); - - return matching.Select(go => go.GetInstanceID()); - } - - private static IEnumerable SearchByPath(string path, bool includeInactive) - { - // Check Prefab Stage first - GameObject.Find() doesn't work in Prefab Stage - var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); - if (prefabStage != null) - { - // Use GetAllSceneObjects which already handles Prefab Stage - var allObjects = GetAllSceneObjects(includeInactive); - foreach (var go in allObjects) - { - if (MatchesPath(go, path)) - { - yield return go.GetInstanceID(); - } - } - yield break; - } - - // Normal scene mode - // NOTE: Unity's GameObject.Find(path) only finds ACTIVE GameObjects. - // If includeInactive=true, we need to search manually to find inactive objects. - if (includeInactive) - { - // Search manually to support inactive objects - var allObjects = GetAllSceneObjects(true); - foreach (var go in allObjects) - { - if (MatchesPath(go, path)) - { - yield return go.GetInstanceID(); - } - } - } - else - { - // Use GameObject.Find for active objects only (Unity API limitation) - var found = GameObject.Find(path); - if (found != null) - { - yield return found.GetInstanceID(); - } - } - } - - private static IEnumerable SearchByTag(string tag, bool includeInactive, int maxResults) - { - GameObject[] taggedObjects; - try - { - if (includeInactive) - { - // FindGameObjectsWithTag doesn't find inactive, so we need to iterate all - var allObjects = GetAllSceneObjects(true); - taggedObjects = allObjects.Where(go => go.CompareTag(tag)).ToArray(); - } - else - { - taggedObjects = GameObject.FindGameObjectsWithTag(tag); - } - } - catch (UnityException) - { - // Tag doesn't exist - yield break; - } - - var results = taggedObjects.AsEnumerable(); - if (maxResults > 0) - results = results.Take(maxResults); - - foreach (var go in results) - { - yield return go.GetInstanceID(); - } - } - - private static IEnumerable SearchByLayer(string layerName, bool includeInactive, int maxResults) - { - int layer = LayerMask.NameToLayer(layerName); - if (layer == -1) - { - // Try parsing as layer number - if (!int.TryParse(layerName, out layer) || layer < 0 || layer > 31) - { - yield break; - } - } - - var allObjects = GetAllSceneObjects(includeInactive); - var matching = allObjects.Where(go => go.layer == layer); - - if (maxResults > 0) - matching = matching.Take(maxResults); - - foreach (var go in matching) - { - yield return go.GetInstanceID(); - } - } - - private static IEnumerable SearchByComponent(string componentTypeName, bool includeInactive, int maxResults) - { - Type componentType = FindComponentType(componentTypeName); - if (componentType == null) - { - McpLog.Warn($"[GameObjectLookup] Component type '{componentTypeName}' not found."); - yield break; - } - - var allObjects = GetAllSceneObjects(includeInactive); - var count = 0; - - foreach (var go in allObjects) - { - if (go.GetComponent(componentType) != null) - { - yield return go.GetInstanceID(); - count++; - - if (maxResults > 0 && count >= maxResults) - yield break; - } - } - } - - /// - /// Gets all GameObjects in the current scene. - /// - public static IEnumerable GetAllSceneObjects(bool includeInactive) - { - // Check Prefab Stage first - var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); - if (prefabStage != null && prefabStage.prefabContentsRoot != null) - { - // Use Prefab Stage's prefabContentsRoot - foreach (var go in GetObjectAndDescendants(prefabStage.prefabContentsRoot, includeInactive)) - { - yield return go; - } - yield break; - } - - // Normal scene mode - var scene = SceneManager.GetActiveScene(); - if (!scene.IsValid()) - yield break; - - var rootObjects = scene.GetRootGameObjects(); - foreach (var root in rootObjects) - { - foreach (var go in GetObjectAndDescendants(root, includeInactive)) - { - yield return go; - } - } - } - - private static IEnumerable GetObjectAndDescendants(GameObject obj, bool includeInactive) - { - if (!includeInactive && !obj.activeInHierarchy) - yield break; - - yield return obj; - - foreach (Transform child in obj.transform) - { - foreach (var descendant in GetObjectAndDescendants(child.gameObject, includeInactive)) - { - yield return descendant; - } - } - } - - /// - /// Finds a component type by name, searching loaded assemblies. - /// - /// - /// Delegates to UnityTypeResolver.ResolveComponent() for unified type resolution. - /// - public static Type FindComponentType(string typeName) - { - return UnityTypeResolver.ResolveComponent(typeName); - } - - /// - /// Checks whether a GameObject matches a path or trailing path segment. - /// - internal static bool MatchesPath(GameObject go, string path) - { - if (go == null || string.IsNullOrEmpty(path)) - return false; - - var goPath = GetGameObjectPath(go); - return goPath == path || goPath.EndsWith("/" + path); - } - - /// - /// Gets the hierarchical path of a GameObject. - /// - public static string GetGameObjectPath(GameObject obj) - { - if (obj == null) - return string.Empty; - - var path = obj.name; - var parent = obj.transform.parent; - - while (parent != null) - { - path = parent.name + "/" + path; - parent = parent.parent; - } - - return path; - } - } -} - diff --git a/Assets/MCPForUnity/Editor/Helpers/GameObjectLookup.cs.meta b/Assets/MCPForUnity/Editor/Helpers/GameObjectLookup.cs.meta deleted file mode 100644 index 9f5ce05..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/GameObjectLookup.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4964205faa8dd4f8a960e58fd8c0d4f7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/GameObjectLookup.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs b/Assets/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs deleted file mode 100644 index 688546b..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs +++ /dev/null @@ -1,666 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MCPForUnity.Runtime.Serialization; // For Converters -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Handles serialization of GameObjects and Components for MCP responses. - /// Includes reflection helpers and caching for performance. - /// - public static class GameObjectSerializer - { - // --- Data Serialization --- - - /// - /// Creates a serializable representation of a GameObject. - /// - public static object GetGameObjectData(GameObject go) - { - if (go == null) - return null; - return new - { - name = go.name, - instanceID = go.GetInstanceID(), - tag = go.tag, - layer = go.layer, - activeSelf = go.activeSelf, - activeInHierarchy = go.activeInHierarchy, - isStatic = go.isStatic, - scenePath = go.scene.path, // Identify which scene it belongs to - transform = new // Serialize transform components carefully to avoid JSON issues - { - // Serialize Vector3 components individually to prevent self-referencing loops. - // The default serializer can struggle with properties like Vector3.normalized. - position = new - { - x = go.transform.position.x, - y = go.transform.position.y, - z = go.transform.position.z, - }, - localPosition = new - { - x = go.transform.localPosition.x, - y = go.transform.localPosition.y, - z = go.transform.localPosition.z, - }, - rotation = new - { - x = go.transform.rotation.eulerAngles.x, - y = go.transform.rotation.eulerAngles.y, - z = go.transform.rotation.eulerAngles.z, - }, - localRotation = new - { - x = go.transform.localRotation.eulerAngles.x, - y = go.transform.localRotation.eulerAngles.y, - z = go.transform.localRotation.eulerAngles.z, - }, - scale = new - { - x = go.transform.localScale.x, - y = go.transform.localScale.y, - z = go.transform.localScale.z, - }, - forward = new - { - x = go.transform.forward.x, - y = go.transform.forward.y, - z = go.transform.forward.z, - }, - up = new - { - x = go.transform.up.x, - y = go.transform.up.y, - z = go.transform.up.z, - }, - right = new - { - x = go.transform.right.x, - y = go.transform.right.y, - z = go.transform.right.z, - }, - }, - parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent - // Optionally include components, but can be large - // components = go.GetComponents().Select(c => GetComponentData(c)).ToList() - // Or just component names: - componentNames = go.GetComponents() - .Select(c => c.GetType().FullName) - .ToList(), - }; - } - - // --- Metadata Caching for Reflection --- - private class CachedMetadata - { - public readonly List SerializableProperties; - public readonly List SerializableFields; - - public CachedMetadata(List properties, List fields) - { - SerializableProperties = properties; - SerializableFields = fields; - } - } - // Key becomes Tuple - private static readonly Dictionary, CachedMetadata> _metadataCache = new Dictionary, CachedMetadata>(); - // --- End Metadata Caching --- - - /// - /// Checks if a type is or derives from a type with the specified full name. - /// Used to detect special-case components including their subclasses. - /// - private static bool IsOrDerivedFrom(Type type, string baseTypeFullName) - { - Type current = type; - while (current != null) - { - if (current.FullName == baseTypeFullName) - return true; - current = current.BaseType; - } - return false; - } - - /// - /// Serializes a UnityEngine.Object reference to a dictionary with name, instanceID, and assetPath. - /// Used for consistent serialization of asset references in special-case component handlers. - /// - /// The Unity object to serialize - /// Whether to include the asset path (default true) - /// A dictionary with the object's reference info, or null if obj is null - private static Dictionary SerializeAssetReference(UnityEngine.Object obj, bool includeAssetPath = true) - { - if (obj == null) return null; - - var result = new Dictionary - { - { "name", obj.name }, - { "instanceID", obj.GetInstanceID() } - }; - - if (includeAssetPath) - { - var assetPath = AssetDatabase.GetAssetPath(obj); - result["assetPath"] = string.IsNullOrEmpty(assetPath) ? null : assetPath; - } - - return result; - } - - /// - /// Creates a serializable representation of a Component, attempting to serialize - /// public properties and fields using reflection, with caching and control over non-public fields. - /// - // Add the flag parameter here - public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true) - { - // --- Add Early Logging --- - // McpLog.Info($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceID() ?? 0})"); - // --- End Early Logging --- - - if (c == null) return null; - Type componentType = c.GetType(); - - // --- Special handling for Transform to avoid reflection crashes and problematic properties --- - if (componentType == typeof(Transform)) - { - Transform tr = c as Transform; - // McpLog.Info($"[GetComponentData] Manually serializing Transform (ID: {tr.GetInstanceID()})"); - return new Dictionary - { - { "typeName", componentType.FullName }, - { "instanceID", tr.GetInstanceID() }, - // Manually extract known-safe properties. Avoid Quaternion 'rotation' and 'lossyScale'. - { "position", CreateTokenFromValue(tr.position, typeof(Vector3))?.ToObject() ?? new JObject() }, - { "localPosition", CreateTokenFromValue(tr.localPosition, typeof(Vector3))?.ToObject() ?? new JObject() }, - { "eulerAngles", CreateTokenFromValue(tr.eulerAngles, typeof(Vector3))?.ToObject() ?? new JObject() }, // Use Euler angles - { "localEulerAngles", CreateTokenFromValue(tr.localEulerAngles, typeof(Vector3))?.ToObject() ?? new JObject() }, - { "localScale", CreateTokenFromValue(tr.localScale, typeof(Vector3))?.ToObject() ?? new JObject() }, - { "right", CreateTokenFromValue(tr.right, typeof(Vector3))?.ToObject() ?? new JObject() }, - { "up", CreateTokenFromValue(tr.up, typeof(Vector3))?.ToObject() ?? new JObject() }, - { "forward", CreateTokenFromValue(tr.forward, typeof(Vector3))?.ToObject() ?? new JObject() }, - { "parentInstanceID", tr.parent?.gameObject.GetInstanceID() ?? 0 }, - { "rootInstanceID", tr.root?.gameObject.GetInstanceID() ?? 0 }, - { "childCount", tr.childCount }, - // Include standard Object/Component properties - { "name", tr.name }, - { "tag", tr.tag }, - { "gameObjectInstanceID", tr.gameObject?.GetInstanceID() ?? 0 } - }; - } - // --- End Special handling for Transform --- - - // --- Special handling for Camera to avoid matrix-related crashes --- - if (componentType == typeof(Camera)) - { - Camera cam = c as Camera; - var cameraProperties = new Dictionary(); - - // List of safe properties to serialize - var safeProperties = new Dictionary> - { - { "nearClipPlane", () => cam.nearClipPlane }, - { "farClipPlane", () => cam.farClipPlane }, - { "fieldOfView", () => cam.fieldOfView }, - { "renderingPath", () => (int)cam.renderingPath }, - { "actualRenderingPath", () => (int)cam.actualRenderingPath }, - { "allowHDR", () => cam.allowHDR }, - { "allowMSAA", () => cam.allowMSAA }, - { "allowDynamicResolution", () => cam.allowDynamicResolution }, - { "forceIntoRenderTexture", () => cam.forceIntoRenderTexture }, - { "orthographicSize", () => cam.orthographicSize }, - { "orthographic", () => cam.orthographic }, - { "opaqueSortMode", () => (int)cam.opaqueSortMode }, - { "transparencySortMode", () => (int)cam.transparencySortMode }, - { "depth", () => cam.depth }, - { "aspect", () => cam.aspect }, - { "cullingMask", () => cam.cullingMask }, - { "eventMask", () => cam.eventMask }, - { "backgroundColor", () => cam.backgroundColor }, - { "clearFlags", () => (int)cam.clearFlags }, - { "stereoEnabled", () => cam.stereoEnabled }, - { "stereoSeparation", () => cam.stereoSeparation }, - { "stereoConvergence", () => cam.stereoConvergence }, - { "enabled", () => cam.enabled }, - { "name", () => cam.name }, - { "tag", () => cam.tag }, - { "gameObject", () => new { name = cam.gameObject.name, instanceID = cam.gameObject.GetInstanceID() } } - }; - - foreach (var prop in safeProperties) - { - try - { - var value = prop.Value(); - if (value != null) - { - AddSerializableValue(cameraProperties, prop.Key, value.GetType(), value); - } - } - catch (Exception) - { - // Silently skip any property that fails - continue; - } - } - - return new Dictionary - { - { "typeName", componentType.FullName }, - { "instanceID", cam.GetInstanceID() }, - { "properties", cameraProperties } - }; - } - // --- End Special handling for Camera --- - - // --- Special handling for UIDocument to avoid infinite loops from VisualElement hierarchy (Issue #585) --- - // UIDocument.rootVisualElement contains circular parent/child references that cause infinite serialization loops. - // Use IsOrDerivedFrom to also catch subclasses of UIDocument. - if (IsOrDerivedFrom(componentType, "UnityEngine.UIElements.UIDocument")) - { - var uiDocProperties = new Dictionary(); - - try - { - // Get panelSettings reference safely - var panelSettingsProp = componentType.GetProperty("panelSettings"); - if (panelSettingsProp != null) - { - var panelSettings = panelSettingsProp.GetValue(c) as UnityEngine.Object; - uiDocProperties["panelSettings"] = SerializeAssetReference(panelSettings); - } - - // Get visualTreeAsset reference safely (the UXML file) - var visualTreeAssetProp = componentType.GetProperty("visualTreeAsset"); - if (visualTreeAssetProp != null) - { - var visualTreeAsset = visualTreeAssetProp.GetValue(c) as UnityEngine.Object; - uiDocProperties["visualTreeAsset"] = SerializeAssetReference(visualTreeAsset); - } - - // Get sortingOrder safely - var sortingOrderProp = componentType.GetProperty("sortingOrder"); - if (sortingOrderProp != null) - { - uiDocProperties["sortingOrder"] = sortingOrderProp.GetValue(c); - } - - // Get enabled state (from Behaviour base class) - var enabledProp = componentType.GetProperty("enabled"); - if (enabledProp != null) - { - uiDocProperties["enabled"] = enabledProp.GetValue(c); - } - - // Get parentUI reference safely (no asset path needed - it's a scene reference) - var parentUIProp = componentType.GetProperty("parentUI"); - if (parentUIProp != null) - { - var parentUI = parentUIProp.GetValue(c) as UnityEngine.Object; - uiDocProperties["parentUI"] = SerializeAssetReference(parentUI, includeAssetPath: false); - } - - // NOTE: rootVisualElement is intentionally skipped - it contains circular - // parent/child references that cause infinite serialization loops - uiDocProperties["_note"] = "rootVisualElement skipped to prevent circular reference loops"; - } - catch (Exception e) - { - McpLog.Warn($"[GetComponentData] Error reading UIDocument properties: {e.Message}"); - } - - // Return structure matches Camera special handling (typeName, instanceID, properties) - return new Dictionary - { - { "typeName", componentType.FullName }, - { "instanceID", c.GetInstanceID() }, - { "properties", uiDocProperties } - }; - } - // --- End Special handling for UIDocument --- - - var data = new Dictionary - { - { "typeName", componentType.FullName }, - { "instanceID", c.GetInstanceID() } - }; - - // --- Get Cached or Generate Metadata (using new cache key) --- - Tuple cacheKey = new Tuple(componentType, includeNonPublicSerializedFields); - if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData)) - { - var propertiesToCache = new List(); - var fieldsToCache = new List(); - - // Traverse the hierarchy from the component type up to MonoBehaviour - Type currentType = componentType; - while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object)) - { - // Get properties declared only at the current type level - BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; - foreach (var propInfo in currentType.GetProperties(propFlags)) - { - // Basic filtering (readable, not indexer, not transform which is handled elsewhere) - if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue; - // Add if not already added (handles overrides - keep the most derived version) - if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) - { - propertiesToCache.Add(propInfo); - } - } - - // Get fields declared only at the current type level (both public and non-public) - BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; - var declaredFields = currentType.GetFields(fieldFlags); - - // Process the declared Fields for caching - foreach (var fieldInfo in declaredFields) - { - if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields - - // Add if not already added (handles hiding - keep the most derived version) - if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue; - - bool shouldInclude = false; - if (includeNonPublicSerializedFields) - { - // If TRUE, include Public OR any NonPublic with [SerializeField] (private/protected/internal) - var hasSerializeField = fieldInfo.IsDefined(typeof(SerializeField), inherit: true); - shouldInclude = fieldInfo.IsPublic || (!fieldInfo.IsPublic && hasSerializeField); - } - else // includeNonPublicSerializedFields is FALSE - { - // If FALSE, include ONLY if it is explicitly Public. - shouldInclude = fieldInfo.IsPublic; - } - - if (shouldInclude) - { - fieldsToCache.Add(fieldInfo); - } - } - - // Move to the base type - currentType = currentType.BaseType; - } - // --- End Hierarchy Traversal --- - - cachedData = new CachedMetadata(propertiesToCache, fieldsToCache); - _metadataCache[cacheKey] = cachedData; // Add to cache with combined key - } - // --- End Get Cached or Generate Metadata --- - - // --- Use cached metadata --- - var serializablePropertiesOutput = new Dictionary(); - - // --- Add Logging Before Property Loop --- - // McpLog.Info($"[GetComponentData] Starting property loop for {componentType.Name}..."); - // --- End Logging Before Property Loop --- - - // Use cached properties - foreach (var propInfo in cachedData.SerializableProperties) - { - string propName = propInfo.Name; - - // --- Skip known obsolete/problematic Component shortcut properties --- - bool skipProperty = false; - if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" || - propName == "light" || propName == "animation" || propName == "constantForce" || - propName == "renderer" || propName == "audio" || propName == "networkView" || - propName == "collider" || propName == "collider2D" || propName == "hingeJoint" || - propName == "particleSystem" || - // Also skip potentially problematic Matrix properties prone to cycles/errors - propName == "worldToLocalMatrix" || propName == "localToWorldMatrix") - { - // McpLog.Info($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log - skipProperty = true; - } - // --- End Skip Generic Properties --- - - // --- Skip specific potentially problematic Camera properties --- - if (componentType == typeof(Camera) && - (propName == "pixelRect" || - propName == "rect" || - propName == "cullingMatrix" || - propName == "useOcclusionCulling" || - propName == "worldToCameraMatrix" || - propName == "projectionMatrix" || - propName == "nonJitteredProjectionMatrix" || - propName == "previousViewProjectionMatrix" || - propName == "cameraToWorldMatrix")) - { - // McpLog.Info($"[GetComponentData] Explicitly skipping Camera property: {propName}"); - skipProperty = true; - } - // --- End Skip Camera Properties --- - - // --- Skip specific potentially problematic Transform properties --- - if (componentType == typeof(Transform) && - (propName == "lossyScale" || - propName == "rotation" || - propName == "worldToLocalMatrix" || - propName == "localToWorldMatrix")) - { - // McpLog.Info($"[GetComponentData] Explicitly skipping Transform property: {propName}"); - skipProperty = true; - } - // --- End Skip Transform Properties --- - - // Skip if flagged - if (skipProperty) - { - continue; - } - - try - { - // --- Add detailed logging --- - // McpLog.Info($"[GetComponentData] Accessing: {componentType.Name}.{propName}"); - // --- End detailed logging --- - - // --- Special handling for material/mesh properties in edit mode --- - object value; - if (!Application.isPlaying && (propName == "material" || propName == "materials" || propName == "mesh")) - { - // In edit mode, use sharedMaterial/sharedMesh to avoid instantiation warnings - if ((propName == "material" || propName == "materials") && c is Renderer renderer) - { - if (propName == "material") - value = renderer.sharedMaterial; - else // materials - value = renderer.sharedMaterials; - } - else if (propName == "mesh" && c is MeshFilter meshFilter) - { - value = meshFilter.sharedMesh; - } - else - { - // Fallback to normal property access if type doesn't match - value = propInfo.GetValue(c); - } - } - else - { - value = propInfo.GetValue(c); - } - // --- End special handling --- - - Type propType = propInfo.PropertyType; - AddSerializableValue(serializablePropertiesOutput, propName, propType, value); - } - catch (Exception) - { - // McpLog.Warn($"Could not read property {propName} on {componentType.Name}"); - } - } - - // --- Add Logging Before Field Loop --- - // McpLog.Info($"[GetComponentData] Starting field loop for {componentType.Name}..."); - // --- End Logging Before Field Loop --- - - // Use cached fields - foreach (var fieldInfo in cachedData.SerializableFields) - { - try - { - // --- Add detailed logging for fields --- - // McpLog.Info($"[GetComponentData] Accessing Field: {componentType.Name}.{fieldInfo.Name}"); - // --- End detailed logging for fields --- - object value = fieldInfo.GetValue(c); - string fieldName = fieldInfo.Name; - Type fieldType = fieldInfo.FieldType; - AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value); - } - catch (Exception) - { - // McpLog.Warn($"Could not read field {fieldInfo.Name} on {componentType.Name}"); - } - } - // --- End Use cached metadata --- - - if (serializablePropertiesOutput.Count > 0) - { - data["properties"] = serializablePropertiesOutput; - } - - return data; - } - - // Helper function to decide how to serialize different types - private static void AddSerializableValue(Dictionary dict, string name, Type type, object value) - { - // Simplified: Directly use CreateTokenFromValue which uses the serializer - if (value == null) - { - dict[name] = null; - return; - } - - try - { - // Use the helper that employs our custom serializer settings - JToken token = CreateTokenFromValue(value, type); - if (token != null) // Check if serialization succeeded in the helper - { - // Convert JToken back to a basic object structure for the dictionary - dict[name] = ConvertJTokenToPlainObject(token); - } - // If token is null, it means serialization failed and a warning was logged. - } - catch (Exception e) - { - // Catch potential errors during JToken conversion or addition to dictionary - McpLog.Warn($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping."); - } - } - - // Helper to convert JToken back to basic object structure - private static object ConvertJTokenToPlainObject(JToken token) - { - if (token == null) return null; - - switch (token.Type) - { - case JTokenType.Object: - var objDict = new Dictionary(); - foreach (var prop in ((JObject)token).Properties()) - { - objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value); - } - return objDict; - - case JTokenType.Array: - var list = new List(); - foreach (var item in (JArray)token) - { - list.Add(ConvertJTokenToPlainObject(item)); - } - return list; - - case JTokenType.Integer: - return token.ToObject(); // Use long for safety - case JTokenType.Float: - return token.ToObject(); // Use double for safety - case JTokenType.String: - return token.ToObject(); - case JTokenType.Boolean: - return token.ToObject(); - case JTokenType.Date: - return token.ToObject(); - case JTokenType.Guid: - return token.ToObject(); - case JTokenType.Uri: - return token.ToObject(); - case JTokenType.TimeSpan: - return token.ToObject(); - case JTokenType.Bytes: - return token.ToObject(); - case JTokenType.Null: - return null; - case JTokenType.Undefined: - return null; // Treat undefined as null - - default: - // Fallback for simple value types not explicitly listed - if (token is JValue jValue && jValue.Value != null) - { - return jValue.Value; - } - // McpLog.Warn($"Unsupported JTokenType encountered: {token.Type}. Returning null."); - return null; - } - } - - // --- Define custom JsonSerializerSettings for OUTPUT --- - private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings - { - Converters = new List - { - new Vector3Converter(), - new Vector2Converter(), - new QuaternionConverter(), - new ColorConverter(), - new RectConverter(), - new BoundsConverter(), - new Matrix4x4Converter(), // Fix #478: Safe Matrix4x4 serialization for Cinemachine - new UnityEngineObjectConverter() // Handles serialization of references - }, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - // ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed - }; - private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings); - // --- End Define custom JsonSerializerSettings --- - - // Helper to create JToken using the output serializer - private static JToken CreateTokenFromValue(object value, Type type) - { - if (value == null) return JValue.CreateNull(); - - try - { - // Use the pre-configured OUTPUT serializer instance - return JToken.FromObject(value, _outputSerializer); - } - catch (JsonSerializationException e) - { - McpLog.Warn($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field."); - return null; // Indicate serialization failure - } - catch (Exception e) // Catch other unexpected errors - { - McpLog.Warn($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field."); - return null; // Indicate serialization failure - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs.meta b/Assets/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs.meta deleted file mode 100644 index 5a81e2e..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 64b8ff807bc9a401c82015cbafccffac -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/GameObjectSerializer.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs b/Assets/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs deleted file mode 100644 index 36d1680..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Models; -using MCPForUnity.Editor.Services; -using UnityEditor; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Helper methods for managing HTTP endpoint URLs used by the MCP bridge. - /// Ensures the stored value is always the base URL (without trailing path), - /// and provides convenience accessors for specific endpoints. - /// - /// HTTP Local and HTTP Remote use separate EditorPrefs keys so that switching - /// between scopes does not overwrite the other scope's URL. - /// - public static class HttpEndpointUtility - { - private const string LocalPrefKey = EditorPrefKeys.HttpBaseUrl; - private const string RemotePrefKey = EditorPrefKeys.HttpRemoteBaseUrl; - private const string DefaultLocalBaseUrl = "http://localhost:8080"; - private const string DefaultRemoteBaseUrl = "https://mc-82a58513564647608b491c00acc383c8.ecs.us-east-2.on.aws"; - - /// - /// Returns the normalized base URL for the currently active HTTP scope. - /// If the scope is "remote", returns the remote URL; otherwise returns the local URL. - /// - public static string GetBaseUrl() - { - return IsRemoteScope() ? GetRemoteBaseUrl() : GetLocalBaseUrl(); - } - - /// - /// Saves a user-provided URL to the currently active HTTP scope's pref. - /// - public static void SaveBaseUrl(string userValue) - { - if (IsRemoteScope()) - { - SaveRemoteBaseUrl(userValue); - } - else - { - SaveLocalBaseUrl(userValue); - } - } - - /// - /// Returns the normalized local HTTP base URL (always reads local pref). - /// - public static string GetLocalBaseUrl() - { - string stored = EditorPrefs.GetString(LocalPrefKey, DefaultLocalBaseUrl); - return NormalizeBaseUrl(stored, DefaultLocalBaseUrl); - } - - /// - /// Saves a user-provided URL to the local HTTP pref. - /// - public static void SaveLocalBaseUrl(string userValue) - { - string normalized = NormalizeBaseUrl(userValue, DefaultLocalBaseUrl); - EditorPrefs.SetString(LocalPrefKey, normalized); - } - - /// - /// Returns the normalized remote HTTP base URL (always reads remote pref). - /// Returns empty string if no remote URL is configured. - /// - public static string GetRemoteBaseUrl() - { - string stored = EditorPrefs.GetString(RemotePrefKey, DefaultRemoteBaseUrl); - if (string.IsNullOrWhiteSpace(stored)) - { - return DefaultRemoteBaseUrl; - } - return NormalizeBaseUrl(stored, DefaultRemoteBaseUrl); - } - - /// - /// Saves a user-provided URL to the remote HTTP pref. - /// - public static void SaveRemoteBaseUrl(string userValue) - { - if (string.IsNullOrWhiteSpace(userValue)) - { - EditorPrefs.SetString(RemotePrefKey, DefaultRemoteBaseUrl); - return; - } - string normalized = NormalizeBaseUrl(userValue, DefaultRemoteBaseUrl); - EditorPrefs.SetString(RemotePrefKey, normalized); - } - - /// - /// Builds the JSON-RPC endpoint for the currently active scope (base + /mcp). - /// - public static string GetMcpRpcUrl() - { - return AppendPathSegment(GetBaseUrl(), "mcp"); - } - - /// - /// Builds the local JSON-RPC endpoint (local base + /mcp). - /// - public static string GetLocalMcpRpcUrl() - { - return AppendPathSegment(GetLocalBaseUrl(), "mcp"); - } - - /// - /// Builds the remote JSON-RPC endpoint (remote base + /mcp). - /// Returns empty string if no remote URL is configured. - /// - public static string GetRemoteMcpRpcUrl() - { - string remoteBase = GetRemoteBaseUrl(); - return string.IsNullOrEmpty(remoteBase) ? string.Empty : AppendPathSegment(remoteBase, "mcp"); - } - - /// - /// Builds the endpoint used when POSTing custom-tool registration payloads. - /// - public static string GetRegisterToolsUrl() - { - return AppendPathSegment(GetBaseUrl(), "register-tools"); - } - - /// - /// Returns true if the active HTTP transport scope is "remote". - /// - public static bool IsRemoteScope() - { - string scope = EditorConfigurationCache.Instance.HttpTransportScope; - return string.Equals(scope, "remote", StringComparison.OrdinalIgnoreCase); - } - - /// - /// Returns the that matches the current server-side - /// transport selection (Stdio, Http, or HttpRemote). - /// Centralises the 3-way determination so callers avoid duplicated logic. - /// - public static ConfiguredTransport GetCurrentServerTransport() - { - bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; - if (!useHttp) return ConfiguredTransport.Stdio; - return IsRemoteScope() ? ConfiguredTransport.HttpRemote : ConfiguredTransport.Http; - } - - /// - /// Normalizes a URL so that we consistently store just the base (no trailing slash/path). - /// - private static string NormalizeBaseUrl(string value, string defaultUrl) - { - if (string.IsNullOrWhiteSpace(value)) - { - return defaultUrl; - } - - string trimmed = value.Trim(); - - // Ensure scheme exists; default to http:// if user omitted it. - if (!trimmed.Contains("://")) - { - trimmed = $"http://{trimmed}"; - } - - // Remove trailing slash segments. - trimmed = trimmed.TrimEnd('/'); - - // Strip trailing "/mcp" (case-insensitive) if provided. - if (trimmed.EndsWith("/mcp", StringComparison.OrdinalIgnoreCase)) - { - trimmed = trimmed[..^4]; - } - - return trimmed; - } - - private static string AppendPathSegment(string baseUrl, string segment) - { - return $"{baseUrl.TrimEnd('/')}/{segment}"; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs.meta b/Assets/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs.meta deleted file mode 100644 index 3574fc8..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 2051d90316ea345c09240c80c7138e3b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/MaterialOps.cs b/Assets/MCPForUnity/Editor/Helpers/MaterialOps.cs deleted file mode 100644 index 19f5028..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/MaterialOps.cs +++ /dev/null @@ -1,397 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MCPForUnity.Editor.Tools; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - public static class MaterialOps - { - /// - /// Applies a set of properties (JObject) to a material, handling aliases and structured formats. - /// - public static bool ApplyProperties(Material mat, JObject properties, JsonSerializer serializer) - { - if (mat == null || properties == null) - return false; - bool modified = false; - - // Helper for case-insensitive lookup - JToken GetValue(string key) - { - return properties.Properties() - .FirstOrDefault(p => string.Equals(p.Name, key, StringComparison.OrdinalIgnoreCase))?.Value; - } - - // --- Structured / Legacy Format Handling --- - // Example: Set shader - var shaderToken = GetValue("shader"); - if (shaderToken?.Type == JTokenType.String) - { - string shaderRequest = shaderToken.ToString(); - // Set shader - Shader newShader = RenderPipelineUtility.ResolveShader(shaderRequest); - if (newShader != null && mat.shader != newShader) - { - mat.shader = newShader; - modified = true; - } - } - - // Example: Set color property (structured) - var colorToken = GetValue("color"); - if (colorToken is JObject colorProps) - { - string propName = colorProps["name"]?.ToString() ?? GetMainColorPropertyName(mat); - if (colorProps["value"] is JArray colArr && colArr.Count >= 3) - { - try - { - Color newColor = ParseColor(colArr, serializer); - if (mat.HasProperty(propName)) - { - if (mat.GetColor(propName) != newColor) - { - mat.SetColor(propName, newColor); - modified = true; - } - } - } - catch (Exception ex) - { - McpLog.Warn($"[MaterialOps] Failed to parse color for property '{propName}': {ex.Message}"); - } - } - } - else if (colorToken is JArray colorArr) // Structured shorthand - { - string propName = GetMainColorPropertyName(mat); - try - { - Color newColor = ParseColor(colorArr, serializer); - if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor) - { - mat.SetColor(propName, newColor); - modified = true; - } - } - catch (Exception ex) - { - McpLog.Warn($"[MaterialOps] Failed to parse color array: {ex.Message}"); - } - } - - // Example: Set float property (structured) - var floatToken = GetValue("float"); - if (floatToken is JObject floatProps) - { - string propName = floatProps["name"]?.ToString(); - if (!string.IsNullOrEmpty(propName) && - (floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer)) - { - try - { - float newVal = floatProps["value"].ToObject(); - if (mat.HasProperty(propName) && mat.GetFloat(propName) != newVal) - { - mat.SetFloat(propName, newVal); - modified = true; - } - } - catch (Exception ex) - { - McpLog.Warn($"[MaterialOps] Failed to set float property '{propName}': {ex.Message}"); - } - } - } - - // Example: Set texture property (structured) - { - var texToken = GetValue("texture"); - if (texToken is JObject texProps) - { - string rawName = (texProps["name"] ?? texProps["Name"])?.ToString(); - string texPath = (texProps["path"] ?? texProps["Path"])?.ToString(); - if (!string.IsNullOrEmpty(texPath)) - { - var sanitizedPath = AssetPathUtility.SanitizeAssetPath(texPath); - var newTex = AssetDatabase.LoadAssetAtPath(sanitizedPath); - // Use ResolvePropertyName to handle aliases even for structured texture names - string candidateName = string.IsNullOrEmpty(rawName) ? "_BaseMap" : rawName; - string targetProp = ResolvePropertyName(mat, candidateName); - - if (!string.IsNullOrEmpty(targetProp) && mat.HasProperty(targetProp)) - { - if (mat.GetTexture(targetProp) != newTex) - { - mat.SetTexture(targetProp, newTex); - modified = true; - } - } - } - } - } - - // --- Direct Property Assignment (Flexible) --- - var reservedKeys = new HashSet(StringComparer.OrdinalIgnoreCase) { "shader", "color", "float", "texture" }; - - foreach (var prop in properties.Properties()) - { - if (reservedKeys.Contains(prop.Name)) continue; - string shaderProp = ResolvePropertyName(mat, prop.Name); - JToken v = prop.Value; - - if (TrySetShaderProperty(mat, shaderProp, v, serializer)) - { - modified = true; - } - } - - return modified; - } - - /// - /// Resolves common property aliases (e.g. "metallic" -> "_Metallic"). - /// - public static string ResolvePropertyName(Material mat, string name) - { - if (mat == null || string.IsNullOrEmpty(name)) return name; - string[] candidates; - var lower = name.ToLowerInvariant(); - switch (lower) - { - case "_color": candidates = new[] { "_Color", "_BaseColor" }; break; - case "_basecolor": candidates = new[] { "_BaseColor", "_Color" }; break; - case "_maintex": candidates = new[] { "_MainTex", "_BaseMap" }; break; - case "_basemap": candidates = new[] { "_BaseMap", "_MainTex" }; break; - case "_glossiness": candidates = new[] { "_Glossiness", "_Smoothness" }; break; - case "_smoothness": candidates = new[] { "_Smoothness", "_Glossiness" }; break; - // Friendly names → shader property names - case "metallic": candidates = new[] { "_Metallic" }; break; - case "smoothness": candidates = new[] { "_Smoothness", "_Glossiness" }; break; - case "albedo": candidates = new[] { "_BaseMap", "_MainTex" }; break; - default: candidates = new[] { name }; break; // keep original as-is - } - foreach (var candidate in candidates) - { - if (mat.HasProperty(candidate)) return candidate; - } - return name; - } - - /// - /// Auto-detects the main color property name for a material's shader. - /// - public static string GetMainColorPropertyName(Material mat) - { - if (mat == null || mat.shader == null) - return "_Color"; - - string[] commonColorProps = { "_BaseColor", "_Color", "_MainColor", "_Tint", "_TintColor" }; - foreach (var prop in commonColorProps) - { - if (mat.HasProperty(prop)) - return prop; - } - return "_Color"; - } - - /// - /// Tries to set a shader property on a material based on a JToken value. - /// Handles Colors, Vectors, Floats, Ints, Booleans, and Textures. - /// - public static bool TrySetShaderProperty(Material material, string propertyName, JToken value, JsonSerializer serializer) - { - if (material == null || string.IsNullOrEmpty(propertyName) || value == null) - return false; - - // Handle stringified JSON - if (value.Type == JTokenType.String) - { - string s = value.ToString(); - if (s.TrimStart().StartsWith("[") || s.TrimStart().StartsWith("{")) - { - try - { - JToken parsed = JToken.Parse(s); - return TrySetShaderProperty(material, propertyName, parsed, serializer); - } - catch { } - } - } - - // Use the serializer to convert the JToken value first - if (value is JArray jArray) - { - if (jArray.Count == 4) - { - if (material.HasProperty(propertyName)) - { - try { material.SetColor(propertyName, ParseColor(value, serializer)); return true; } - catch (Exception ex) - { - // Log at Debug level since we'll try other conversions - McpLog.Info($"[MaterialOps] SetColor attempt for '{propertyName}' failed: {ex.Message}"); - } - - try { Vector4 vec = value.ToObject(serializer); material.SetVector(propertyName, vec); return true; } - catch (Exception ex) - { - McpLog.Info($"[MaterialOps] SetVector (Vec4) attempt for '{propertyName}' failed: {ex.Message}"); - } - } - } - else if (jArray.Count == 3) - { - if (material.HasProperty(propertyName)) - { - try { material.SetColor(propertyName, ParseColor(value, serializer)); return true; } - catch (Exception ex) - { - McpLog.Info($"[MaterialOps] SetColor (Vec3) attempt for '{propertyName}' failed: {ex.Message}"); - } - } - } - else if (jArray.Count == 2) - { - if (material.HasProperty(propertyName)) - { - try { Vector2 vec = value.ToObject(serializer); material.SetVector(propertyName, vec); return true; } - catch (Exception ex) - { - McpLog.Info($"[MaterialOps] SetVector (Vec2) attempt for '{propertyName}' failed: {ex.Message}"); - } - } - } - } - else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer) - { - if (!material.HasProperty(propertyName)) - return false; - - try { material.SetFloat(propertyName, value.ToObject(serializer)); return true; } - catch (Exception ex) - { - McpLog.Info($"[MaterialOps] SetFloat attempt for '{propertyName}' failed: {ex.Message}"); - } - } - else if (value.Type == JTokenType.Boolean) - { - if (!material.HasProperty(propertyName)) - return false; - - try { material.SetFloat(propertyName, value.ToObject(serializer) ? 1f : 0f); return true; } - catch (Exception ex) - { - McpLog.Info($"[MaterialOps] SetFloat (bool) attempt for '{propertyName}' failed: {ex.Message}"); - } - } - else if (value.Type == JTokenType.String) - { - try - { - // Try loading as asset path first (most common case for strings in this context) - string path = value.ToString(); - if (!string.IsNullOrEmpty(path) && path.Contains("/")) // Heuristic: paths usually have slashes - { - // We need to handle texture assignment here. - // Since we don't have easy access to AssetDatabase here directly without using UnityEditor namespace (which is imported), - // we can try to load it. - var sanitizedPath = AssetPathUtility.SanitizeAssetPath(path); - Texture tex = AssetDatabase.LoadAssetAtPath(sanitizedPath); - if (tex != null && material.HasProperty(propertyName)) - { - material.SetTexture(propertyName, tex); - return true; - } - } - } - catch (Exception ex) - { - McpLog.Warn($"SetTexture (string path) for '{propertyName}' failed: {ex.Message}"); - } - } - - if (value.Type == JTokenType.Object) - { - try - { - Texture texture = value.ToObject(serializer); - if (texture != null && material.HasProperty(propertyName)) - { - material.SetTexture(propertyName, texture); - return true; - } - } - catch (Exception ex) - { - McpLog.Warn($"SetTexture (object) for '{propertyName}' failed: {ex.Message}"); - } - } - - McpLog.Warn( - $"[MaterialOps] Unsupported or failed conversion for material property '{propertyName}' from value: {value.ToString(Formatting.None)}" - ); - return false; - } - - /// - /// Helper to parse color from JToken (array or object). - /// - public static Color ParseColor(JToken token, JsonSerializer serializer) - { - if (token.Type == JTokenType.String) - { - string s = token.ToString(); - if (s.TrimStart().StartsWith("[") || s.TrimStart().StartsWith("{")) - { - try - { - return ParseColor(JToken.Parse(s), serializer); - } - catch { } - } - } - - if (token is JArray jArray) - { - if (jArray.Count == 4) - { - return new Color( - (float)jArray[0], - (float)jArray[1], - (float)jArray[2], - (float)jArray[3] - ); - } - else if (jArray.Count == 3) - { - return new Color( - (float)jArray[0], - (float)jArray[1], - (float)jArray[2], - 1f - ); - } - else - { - throw new ArgumentException("Color array must have 3 or 4 elements."); - } - } - - try - { - return token.ToObject(serializer); - } - catch (Exception ex) - { - McpLog.Warn($"[MaterialOps] Failed to parse color from token: {ex.Message}"); - throw; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/MaterialOps.cs.meta b/Assets/MCPForUnity/Editor/Helpers/MaterialOps.cs.meta deleted file mode 100644 index 5c52b49..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/MaterialOps.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: a59e8545e32664dae9a696d449f82c3d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/MaterialOps.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs b/Assets/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs deleted file mode 100644 index 61eccb1..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Dependencies; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Models; -using MCPForUnity.Editor.Services; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Shared helper for MCP client configuration management with sophisticated - /// logic for preserving existing configs and handling different client types - /// - public static class McpConfigurationHelper - { - private const string LOCK_CONFIG_KEY = EditorPrefKeys.LockCursorConfig; - - /// - /// Writes MCP configuration to the specified path using sophisticated logic - /// that preserves existing configuration and only writes when necessary - /// - public static string WriteMcpConfiguration(string configPath, McpClient mcpClient = null) - { - // 0) Respect explicit lock (hidden pref or UI toggle) - try - { - if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false)) - return "Skipped (locked)"; - } - catch { } - - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - - // Read existing config if it exists - string existingJson = "{}"; - if (File.Exists(configPath)) - { - try - { - existingJson = File.ReadAllText(configPath); - } - catch (Exception e) - { - McpLog.Warn($"Error reading existing config: {e.Message}."); - } - } - - // Parse the existing JSON while preserving all properties - dynamic existingConfig; - try - { - if (string.IsNullOrWhiteSpace(existingJson)) - { - existingConfig = new JObject(); - } - else - { - existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new JObject(); - } - } - catch - { - // If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object - if (!string.IsNullOrWhiteSpace(existingJson)) - { - McpLog.Warn("UnityMCP: Configuration file could not be parsed; rewriting server block."); - } - existingConfig = new JObject(); - } - - // Determine existing entry references (command/args) - string existingCommand = null; - string[] existingArgs = null; - bool isVSCode = (mcpClient?.IsVsCodeLayout == true); - try - { - if (isVSCode) - { - existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString(); - existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject(); - } - else - { - existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString(); - existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject(); - } - } - catch { } - - // 1) Start from existing, only fill gaps (prefer trusted resolver) - string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); - if (uvxPath == null) return "uv package manager not found. Please install uv first."; - - // Ensure containers exist and write back configuration - JObject existingRoot; - if (existingConfig is JObject eo) - existingRoot = eo; - else - existingRoot = JObject.FromObject(existingConfig); - - existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvxPath, mcpClient); - - string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); - - EnsureConfigDirectoryExists(configPath); - WriteAtomicFile(configPath, mergedJson); - - return "Configured successfully"; - } - - /// - /// Configures a Codex client with sophisticated TOML handling - /// - public static string ConfigureCodexClient(string configPath, McpClient mcpClient) - { - try - { - if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false)) - return "Skipped (locked)"; - } - catch { } - - string existingToml = string.Empty; - if (File.Exists(configPath)) - { - try - { - existingToml = File.ReadAllText(configPath); - } - catch (Exception e) - { - McpLog.Warn($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}"); - existingToml = string.Empty; - } - } - - string existingCommand = null; - string[] existingArgs = null; - if (!string.IsNullOrWhiteSpace(existingToml)) - { - CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs); - } - - string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); - if (uvxPath == null) - { - return "uv package manager not found. Please install uv first."; - } - - string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvxPath); - - EnsureConfigDirectoryExists(configPath); - WriteAtomicFile(configPath, updatedToml); - - return "Configured successfully"; - } - - /// - /// Gets the appropriate config file path for the given MCP client based on OS - /// - public static string GetClientConfigPath(McpClient mcpClient) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return mcpClient.windowsConfigPath; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return string.IsNullOrEmpty(mcpClient.macConfigPath) - ? mcpClient.linuxConfigPath - : mcpClient.macConfigPath; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return mcpClient.linuxConfigPath; - } - else - { - return mcpClient.linuxConfigPath; // fallback - } - } - - /// - /// Creates the directory for the config file if it doesn't exist - /// - public static void EnsureConfigDirectoryExists(string configPath) - { - Directory.CreateDirectory(Path.GetDirectoryName(configPath)); - } - - public static string ExtractUvxUrl(string[] args) - { - if (args == null) return null; - for (int i = 0; i < args.Length - 1; i++) - { - if (string.Equals(args[i], "--from", StringComparison.OrdinalIgnoreCase)) - { - return args[i + 1]; - } - } - return null; - } - - public static bool PathsEqual(string a, string b) - { - if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false; - try - { - string na = Path.GetFullPath(a.Trim()); - string nb = Path.GetFullPath(b.Trim()); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase); - } - return string.Equals(na, nb, StringComparison.Ordinal); - } - catch - { - return false; - } - } - - public static void WriteAtomicFile(string path, string contents) - { - string tmp = path + ".tmp"; - string backup = path + ".backup"; - bool writeDone = false; - try - { - File.WriteAllText(tmp, contents, new UTF8Encoding(false)); - try - { - File.Replace(tmp, path, backup); - writeDone = true; - } - catch (FileNotFoundException) - { - File.Move(tmp, path); - writeDone = true; - } - catch (PlatformNotSupportedException) - { - if (File.Exists(path)) - { - try - { - if (File.Exists(backup)) File.Delete(backup); - } - catch { } - File.Move(path, backup); - } - File.Move(tmp, path); - writeDone = true; - } - } - catch (Exception ex) - { - try - { - if (!writeDone && File.Exists(backup)) - { - try { File.Copy(backup, path, true); } catch { } - } - } - catch { } - throw new Exception($"Failed to write config file '{path}': {ex.Message}", ex); - } - finally - { - try { if (File.Exists(tmp)) File.Delete(tmp); } catch { } - try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { } - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs.meta b/Assets/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs.meta deleted file mode 100644 index 0a08ff9..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: e45ac2a13b4c1ba468b8e3aa67b292ca -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/McpJobStateStore.cs b/Assets/MCPForUnity/Editor/Helpers/McpJobStateStore.cs deleted file mode 100644 index 5db093b..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/McpJobStateStore.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.IO; -using Newtonsoft.Json; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Utility for persisting tool state across domain reloads. State is stored in - /// Library so it stays local to the project and is cleared by Unity as needed. - /// - public static class McpJobStateStore - { - private static string GetStatePath(string toolName) - { - if (string.IsNullOrEmpty(toolName)) - { - throw new ArgumentException("toolName cannot be null or empty", nameof(toolName)); - } - - var libraryPath = Path.Combine(Application.dataPath, "..", "Library"); - var fileName = $"McpState_{toolName}.json"; - return Path.GetFullPath(Path.Combine(libraryPath, fileName)); - } - - public static void SaveState(string toolName, T state) - { - var path = GetStatePath(toolName); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - var json = JsonConvert.SerializeObject(state ?? Activator.CreateInstance()); - File.WriteAllText(path, json); - } - - public static T LoadState(string toolName) - { - var path = GetStatePath(toolName); - if (!File.Exists(path)) - { - return default; - } - - try - { - var json = File.ReadAllText(path); - return JsonConvert.DeserializeObject(json); - } - catch (Exception) - { - return default; - } - } - - public static void ClearState(string toolName) - { - var path = GetStatePath(toolName); - if (File.Exists(path)) - { - File.Delete(path); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/McpJobStateStore.cs.meta b/Assets/MCPForUnity/Editor/Helpers/McpJobStateStore.cs.meta deleted file mode 100644 index a40d481..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/McpJobStateStore.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 28912085dd68342f8a9fda8a43c83a59 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/McpJobStateStore.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/McpLog.cs b/Assets/MCPForUnity/Editor/Helpers/McpLog.cs deleted file mode 100644 index ca9f771..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/McpLog.cs +++ /dev/null @@ -1,53 +0,0 @@ -using MCPForUnity.Editor.Constants; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - internal static class McpLog - { - private const string InfoPrefix = "MCP-FOR-UNITY:"; - private const string DebugPrefix = "MCP-FOR-UNITY:"; - private const string WarnPrefix = "MCP-FOR-UNITY:"; - private const string ErrorPrefix = "MCP-FOR-UNITY:"; - - private static volatile bool _debugEnabled = ReadDebugPreference(); - - private static bool IsDebugEnabled() => _debugEnabled; - - private static bool ReadDebugPreference() - { - try { return EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false); } - catch { return false; } - } - - public static void SetDebugLoggingEnabled(bool enabled) - { - _debugEnabled = enabled; - try { EditorPrefs.SetBool(EditorPrefKeys.DebugLogs, enabled); } - catch { } - } - - public static void Debug(string message) - { - if (!IsDebugEnabled()) return; - UnityEngine.Debug.Log($"{DebugPrefix} {message}"); - } - - public static void Info(string message, bool always = true) - { - if (!always && !IsDebugEnabled()) return; - UnityEngine.Debug.Log($"{InfoPrefix} {message}"); - } - - public static void Warn(string message) - { - UnityEngine.Debug.LogWarning($"{WarnPrefix} {message}"); - } - - public static void Error(string message) - { - UnityEngine.Debug.LogError($"{ErrorPrefix} {message}"); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/McpLog.cs.meta b/Assets/MCPForUnity/Editor/Helpers/McpLog.cs.meta deleted file mode 100644 index 4d088a8..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/McpLog.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 9e2c3f8a4f4f48d8a4c1b7b8e3f5a1c2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/McpLog.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/ObjectResolver.cs b/Assets/MCPForUnity/Editor/Helpers/ObjectResolver.cs deleted file mode 100644 index ee35170..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ObjectResolver.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Resolves Unity Objects by instruction (handles GameObjects, Components, Assets). - /// Extracted from ManageGameObject to eliminate cross-tool dependencies. - /// - public static class ObjectResolver - { - /// - /// Resolves any Unity Object by instruction. - /// - /// The type of Unity Object to resolve - /// JObject with "find" (required), "method" (optional), "component" (optional) - /// The resolved object, or null if not found - public static T Resolve(JObject instruction) where T : UnityEngine.Object - { - return Resolve(instruction, typeof(T)) as T; - } - - /// - /// Resolves any Unity Object by instruction. - /// - /// JObject with "find" (required), "method" (optional), "component" (optional) - /// The type of Unity Object to resolve - /// The resolved object, or null if not found - public static UnityEngine.Object Resolve(JObject instruction, Type targetType) - { - if (instruction == null) - return null; - - string findTerm = instruction["find"]?.ToString(); - string method = instruction["method"]?.ToString()?.ToLower(); - string componentName = instruction["component"]?.ToString(); - - if (string.IsNullOrEmpty(findTerm)) - { - McpLog.Warn("[ObjectResolver] Find instruction missing 'find' term."); - return null; - } - - // Use a flexible default search method if none provided - string searchMethodToUse = string.IsNullOrEmpty(method) ? "by_id_or_name_or_path" : method; - - // --- Asset Search --- - // Normalize path separators before checking asset paths - string normalizedPath = AssetPathUtility.NormalizeSeparators(findTerm); - - // If the target is an asset type, try AssetDatabase first - if (IsAssetType(targetType) || - (typeof(GameObject).IsAssignableFrom(targetType) && normalizedPath.StartsWith("Assets/"))) - { - UnityEngine.Object asset = TryLoadAsset(normalizedPath, targetType); - if (asset != null) - return asset; - // If still not found, fall through to scene search - } - - // --- Scene Object Search --- - GameObject foundGo = GameObjectLookup.FindByTarget(new JValue(findTerm), searchMethodToUse, includeInactive: false); - - if (foundGo == null) - { - return null; - } - - // Get the target object/component from the found GameObject - if (targetType == typeof(GameObject)) - { - return foundGo; - } - else if (typeof(Component).IsAssignableFrom(targetType)) - { - Type componentToGetType = targetType; - if (!string.IsNullOrEmpty(componentName)) - { - Type specificCompType = GameObjectLookup.FindComponentType(componentName); - if (specificCompType != null && typeof(Component).IsAssignableFrom(specificCompType)) - { - componentToGetType = specificCompType; - } - else - { - McpLog.Warn($"[ObjectResolver] Could not find component type '{componentName}'. Falling back to target type '{targetType.Name}'."); - } - } - - Component foundComp = foundGo.GetComponent(componentToGetType); - if (foundComp == null) - { - McpLog.Warn($"[ObjectResolver] Found GameObject '{foundGo.name}' but could not find component of type '{componentToGetType.Name}'."); - } - return foundComp; - } - else - { - McpLog.Warn($"[ObjectResolver] Find instruction handling not implemented for target type: {targetType.Name}"); - return null; - } - } - - /// - /// Convenience method to resolve a GameObject. - /// - public static GameObject ResolveGameObject(JToken target, string searchMethod = null) - { - if (target == null) - return null; - - // If target is a simple value, use GameObjectLookup directly - if (target.Type != JTokenType.Object) - { - return GameObjectLookup.FindByTarget(target, searchMethod ?? "by_id_or_name_or_path"); - } - - // If target is an instruction object - var instruction = target as JObject; - if (instruction != null) - { - return Resolve(instruction); - } - - return null; - } - - /// - /// Convenience method to resolve a Material. - /// - public static Material ResolveMaterial(string pathOrName) - { - if (string.IsNullOrEmpty(pathOrName)) - return null; - - var instruction = new JObject { ["find"] = pathOrName }; - return Resolve(instruction); - } - - /// - /// Convenience method to resolve a Texture. - /// - public static Texture ResolveTexture(string pathOrName) - { - if (string.IsNullOrEmpty(pathOrName)) - return null; - - var instruction = new JObject { ["find"] = pathOrName }; - return Resolve(instruction); - } - - // --- Private Helpers --- - - private static bool IsAssetType(Type type) - { - return typeof(Material).IsAssignableFrom(type) || - typeof(Texture).IsAssignableFrom(type) || - typeof(ScriptableObject).IsAssignableFrom(type) || - type.FullName?.StartsWith("UnityEngine.U2D") == true || - typeof(AudioClip).IsAssignableFrom(type) || - typeof(AnimationClip).IsAssignableFrom(type) || - typeof(Font).IsAssignableFrom(type) || - typeof(Shader).IsAssignableFrom(type) || - typeof(ComputeShader).IsAssignableFrom(type); - } - - private static UnityEngine.Object TryLoadAsset(string findTerm, Type targetType) - { - // Try loading directly by path first - UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(findTerm, targetType); - if (asset != null) - return asset; - - // Try generic load if type-specific failed - asset = AssetDatabase.LoadAssetAtPath(findTerm); - if (asset != null && targetType.IsAssignableFrom(asset.GetType())) - return asset; - - // Try finding by name/type using FindAssets - string searchFilter = $"t:{targetType.Name} {System.IO.Path.GetFileNameWithoutExtension(findTerm)}"; - string[] guids = AssetDatabase.FindAssets(searchFilter); - - if (guids.Length == 1) - { - asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[0]), targetType); - if (asset != null) - return asset; - } - else if (guids.Length > 1) - { - McpLog.Warn($"[ObjectResolver] Ambiguous asset find: Found {guids.Length} assets matching filter '{searchFilter}'. Provide a full path or unique name."); - return null; - } - - return null; - } - } -} - diff --git a/Assets/MCPForUnity/Editor/Helpers/ObjectResolver.cs.meta b/Assets/MCPForUnity/Editor/Helpers/ObjectResolver.cs.meta deleted file mode 100644 index a5767f5..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ObjectResolver.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: ad678f7b0a2e6458bbdb38a15d857acf -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/ObjectResolver.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/Pagination.cs b/Assets/MCPForUnity/Editor/Helpers/Pagination.cs deleted file mode 100644 index e1d1387..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/Pagination.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Standard pagination request for all paginated tool operations. - /// Provides consistent handling of page_size/pageSize and cursor/page_number parameters. - /// - public class PaginationRequest - { - /// - /// Number of items per page. Default is 50. - /// - public int PageSize { get; set; } = 50; - - /// - /// 0-based cursor position for the current page. - /// - public int Cursor { get; set; } = 0; - - /// - /// Creates a PaginationRequest from JObject parameters. - /// Accepts both snake_case and camelCase parameter names for flexibility. - /// Converts 1-based page_number to 0-based cursor if needed. - /// - public static PaginationRequest FromParams(JObject @params, int defaultPageSize = 50) - { - if (@params == null) - return new PaginationRequest { PageSize = defaultPageSize }; - - // Accept both page_size and pageSize - int pageSize = ParamCoercion.CoerceInt( - @params["page_size"] ?? @params["pageSize"], - defaultPageSize - ); - - // Accept both cursor (0-based) and page_number (convert 1-based to 0-based) - var cursorToken = @params["cursor"]; - var pageNumberToken = @params["page_number"] ?? @params["pageNumber"]; - - int cursor; - if (cursorToken != null) - { - cursor = ParamCoercion.CoerceInt(cursorToken, 0); - } - else if (pageNumberToken != null) - { - // Convert 1-based page_number to 0-based cursor - int pageNumber = ParamCoercion.CoerceInt(pageNumberToken, 1); - cursor = (pageNumber - 1) * pageSize; - if (cursor < 0) cursor = 0; - } - else - { - cursor = 0; - } - - return new PaginationRequest - { - PageSize = pageSize > 0 ? pageSize : defaultPageSize, - Cursor = cursor - }; - } - } - - /// - /// Standard pagination response for all paginated tool operations. - /// Provides consistent response structure across all tools. - /// - /// The type of items in the paginated list - public class PaginationResponse - { - /// - /// The items on the current page. - /// - [JsonProperty("items")] - public List Items { get; set; } = new List(); - - /// - /// The cursor position for the current page (0-based). - /// - [JsonProperty("cursor")] - public int Cursor { get; set; } - - /// - /// The cursor for the next page, or null if this is the last page. - /// - [JsonProperty("nextCursor")] - public int? NextCursor { get; set; } - - /// - /// Total number of items across all pages. - /// - [JsonProperty("totalCount")] - public int TotalCount { get; set; } - - /// - /// Number of items per page. - /// - [JsonProperty("pageSize")] - public int PageSize { get; set; } - - /// - /// Whether there are more items after this page. - /// - [JsonProperty("hasMore")] - public bool HasMore => NextCursor.HasValue; - - /// - /// Creates a PaginationResponse from a full list of items and pagination parameters. - /// - /// The full list of items to paginate - /// The pagination request parameters - /// A paginated response with the appropriate slice of items - public static PaginationResponse Create(IList allItems, PaginationRequest request) - { - int totalCount = allItems.Count; - int cursor = request.Cursor; - int pageSize = request.PageSize; - - // Clamp cursor to valid range - if (cursor < 0) cursor = 0; - if (cursor > totalCount) cursor = totalCount; - - // Get the page of items - var items = new List(); - int endIndex = System.Math.Min(cursor + pageSize, totalCount); - for (int i = cursor; i < endIndex; i++) - { - items.Add(allItems[i]); - } - - // Calculate next cursor - int? nextCursor = endIndex < totalCount ? endIndex : (int?)null; - - return new PaginationResponse - { - Items = items, - Cursor = cursor, - NextCursor = nextCursor, - TotalCount = totalCount, - PageSize = pageSize - }; - } - } -} - diff --git a/Assets/MCPForUnity/Editor/Helpers/Pagination.cs.meta b/Assets/MCPForUnity/Editor/Helpers/Pagination.cs.meta deleted file mode 100644 index 855a8b3..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/Pagination.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 745564d5894d74c0ca24db39c77bab2c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/Pagination.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/ParamCoercion.cs b/Assets/MCPForUnity/Editor/Helpers/ParamCoercion.cs deleted file mode 100644 index d19d7bf..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ParamCoercion.cs +++ /dev/null @@ -1,363 +0,0 @@ -using System; -using System.Globalization; -using Newtonsoft.Json.Linq; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Utility class for coercing JSON parameter values to strongly-typed values. - /// Handles various input formats (strings, numbers, booleans) gracefully. - /// - public static class ParamCoercion - { - /// - /// Coerces a JToken to an integer value, handling strings and floats. - /// - /// The JSON token to coerce - /// Default value if coercion fails - /// The coerced integer value or default - public static int CoerceInt(JToken token, int defaultValue) - { - if (token == null || token.Type == JTokenType.Null) - return defaultValue; - - try - { - if (token.Type == JTokenType.Integer) - return token.Value(); - - var s = token.ToString().Trim(); - if (s.Length == 0) - return defaultValue; - - if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)) - return i; - - if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d)) - return (int)d; - } - catch - { - // Swallow and return default - } - - return defaultValue; - } - - /// - /// Coerces a JToken to a nullable integer value. - /// Returns null if token is null, empty, or cannot be parsed. - /// - /// The JSON token to coerce - /// The coerced integer value or null - public static int? CoerceIntNullable(JToken token) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - if (token.Type == JTokenType.Integer) - return token.Value(); - - var s = token.ToString().Trim(); - if (s.Length == 0) - return null; - - if (int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)) - return i; - - if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d)) - return (int)d; - } - catch - { - // Swallow and return null - } - - return null; - } - - /// - /// Coerces a JToken to a boolean value, handling strings like "true", "1", etc. - /// - /// The JSON token to coerce - /// Default value if coercion fails - /// The coerced boolean value or default - public static bool CoerceBool(JToken token, bool defaultValue) - { - if (token == null || token.Type == JTokenType.Null) - return defaultValue; - - try - { - if (token.Type == JTokenType.Boolean) - return token.Value(); - - var s = token.ToString().Trim().ToLowerInvariant(); - if (s.Length == 0) - return defaultValue; - - if (bool.TryParse(s, out var b)) - return b; - - if (s == "1" || s == "yes" || s == "on") - return true; - - if (s == "0" || s == "no" || s == "off") - return false; - } - catch - { - // Swallow and return default - } - - return defaultValue; - } - - /// - /// Coerces a JToken to a nullable boolean value. - /// Returns null if token is null, empty, or cannot be parsed. - /// - /// The JSON token to coerce - /// The coerced boolean value or null - public static bool? CoerceBoolNullable(JToken token) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - if (token.Type == JTokenType.Boolean) - return token.Value(); - - var s = token.ToString().Trim().ToLowerInvariant(); - if (s.Length == 0) - return null; - - if (bool.TryParse(s, out var b)) - return b; - - if (s == "1" || s == "yes" || s == "on") - return true; - - if (s == "0" || s == "no" || s == "off") - return false; - } - catch - { - // Swallow and return null - } - - return null; - } - - /// - /// Coerces a JToken to a float value, handling strings and integers. - /// - /// The JSON token to coerce - /// Default value if coercion fails - /// The coerced float value or default - public static float CoerceFloat(JToken token, float defaultValue) - { - if (token == null || token.Type == JTokenType.Null) - return defaultValue; - - try - { - if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer) - return token.Value(); - - var s = token.ToString().Trim(); - if (s.Length == 0) - return defaultValue; - - if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var f)) - return f; - } - catch - { - // Swallow and return default - } - - return defaultValue; - } - - /// - /// Coerces a JToken to a nullable float value. - /// Returns null if token is null, empty, or cannot be parsed. - /// - /// The JSON token to coerce - /// The coerced float value or null - public static float? CoerceFloatNullable(JToken token) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer) - return token.Value(); - - var s = token.ToString().Trim(); - if (s.Length == 0) - return null; - - if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var f)) - return f; - } - catch - { - // Swallow and return null - } - - return null; - } - - /// - /// Coerces a JToken to a string value, with null handling. - /// - /// The JSON token to coerce - /// Default value if null or empty - /// The string value or default - public static string CoerceString(JToken token, string defaultValue = null) - { - if (token == null || token.Type == JTokenType.Null) - return defaultValue; - - var s = token.ToString(); - return string.IsNullOrEmpty(s) ? defaultValue : s; - } - - /// - /// Coerces a JToken to an enum value, handling strings. - /// - /// The enum type - /// The JSON token to coerce - /// Default value if coercion fails - /// The coerced enum value or default - public static T CoerceEnum(JToken token, T defaultValue) where T : struct, Enum - { - if (token == null || token.Type == JTokenType.Null) - return defaultValue; - - try - { - var s = token.ToString().Trim(); - if (s.Length == 0) - return defaultValue; - - if (Enum.TryParse(s, ignoreCase: true, out var result)) - return result; - } - catch - { - // Swallow and return default - } - - return defaultValue; - } - - /// - /// Checks if a JToken represents a numeric value (integer or float). - /// Useful for validating JSON values before parsing. - /// - /// The JSON token to check - /// True if the token is an integer or float, false otherwise - public static bool IsNumericToken(JToken token) - { - return token != null && (token.Type == JTokenType.Integer || token.Type == JTokenType.Float); - } - - /// - /// Validates that an optional field in a JObject is numeric if present. - /// Used for dry-run validation of complex type formats. - /// - /// The JSON object containing the field - /// The name of the field to validate - /// Output error message if validation fails - /// True if the field is absent, null, or numeric; false if present but non-numeric - public static bool ValidateNumericField(JObject obj, string fieldName, out string error) - { - error = null; - var token = obj[fieldName]; - if (token == null || token.Type == JTokenType.Null) - { - return true; // Field not present, valid (will use default) - } - if (!IsNumericToken(token)) - { - error = $"must be a number, got {token.Type}"; - return false; - } - return true; - } - - /// - /// Validates that an optional field in a JObject is an integer if present. - /// Used for dry-run validation of complex type formats. - /// - /// The JSON object containing the field - /// The name of the field to validate - /// Output error message if validation fails - /// True if the field is absent, null, or integer; false if present but non-integer - public static bool ValidateIntegerField(JObject obj, string fieldName, out string error) - { - error = null; - var token = obj[fieldName]; - if (token == null || token.Type == JTokenType.Null) - { - return true; // Field not present, valid - } - if (token.Type != JTokenType.Integer) - { - error = $"must be an integer, got {token.Type}"; - return false; - } - return true; - } - - /// - /// Normalizes a property name by removing separators and converting to camelCase. - /// Handles common naming variations from LLMs and humans. - /// Examples: - /// "Use Gravity" → "useGravity" - /// "is_kinematic" → "isKinematic" - /// "max-angular-velocity" → "maxAngularVelocity" - /// "Angular Drag" → "angularDrag" - /// - /// The property name to normalize - /// The normalized camelCase property name - public static string NormalizePropertyName(string input) - { - if (string.IsNullOrEmpty(input)) - return input; - - // Split on common separators: space, underscore, dash - var parts = input.Split(new[] { ' ', '_', '-' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 0) - return input; - - // First word is lowercase, subsequent words are Title case (camelCase) - var sb = new System.Text.StringBuilder(); - for (int i = 0; i < parts.Length; i++) - { - string part = parts[i]; - if (i == 0) - { - // First word: all lowercase - sb.Append(part.ToLowerInvariant()); - } - else - { - // Subsequent words: capitalize first letter, lowercase rest - sb.Append(char.ToUpperInvariant(part[0])); - if (part.Length > 1) - sb.Append(part.Substring(1).ToLowerInvariant()); - } - } - return sb.ToString(); - } - } -} - diff --git a/Assets/MCPForUnity/Editor/Helpers/ParamCoercion.cs.meta b/Assets/MCPForUnity/Editor/Helpers/ParamCoercion.cs.meta deleted file mode 100644 index 288ef17..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ParamCoercion.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: db54fbbe3ac7f429fbf808f72831374a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/ParamCoercion.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/PortManager.cs b/Assets/MCPForUnity/Editor/Helpers/PortManager.cs deleted file mode 100644 index de46fd8..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/PortManager.cs +++ /dev/null @@ -1,345 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using MCPForUnity.Editor.Constants; -using Newtonsoft.Json; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Manages dynamic port allocation and persistent storage for MCP for Unity - /// - public static class PortManager - { - private static bool IsDebugEnabled() - { - try { return EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false); } - catch { return false; } - } - - 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; - } - - /// - /// Get the port to use from storage, or return the default if none has been saved yet. - /// - /// Port number to use - public static int GetPortWithFallback() - { - var storedConfig = GetStoredPortConfig(); - if (storedConfig != null && - storedConfig.unity_port > 0 && - string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase)) - { - return storedConfig.unity_port; - } - - return DefaultPort; - } - - /// - /// Discover and save a new available port (used by Auto-Connect button) - /// - /// New available port - public static int DiscoverNewPort() - { - int newPort = FindAvailablePort(); - SavePort(newPort); - if (IsDebugEnabled()) McpLog.Info($"Discovered and saved new port: {newPort}"); - return newPort; - } - - /// - /// Persist a user-selected port and return the value actually stored. - /// If is unavailable, the next available port is chosen instead. - /// - public static int SetPreferredPort(int port) - { - if (port <= 0) - { - throw new ArgumentOutOfRangeException(nameof(port), "Port must be positive."); - } - - if (!IsPortAvailable(port)) - { - throw new InvalidOperationException($"Port {port} is already in use."); - } - - SavePort(port); - return port; - } - - /// - /// Find an available port starting from the default port - /// - /// Available port number - private static int FindAvailablePort() - { - // Always try default port first - if (IsPortAvailable(DefaultPort)) - { - if (IsDebugEnabled()) McpLog.Info($"Using default port {DefaultPort}"); - return DefaultPort; - } - - if (IsDebugEnabled()) McpLog.Info($"Default port {DefaultPort} is in use, searching for alternative..."); - - // Search for alternatives - for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++) - { - if (IsPortAvailable(port)) - { - if (IsDebugEnabled()) McpLog.Info($"Found available port {port}"); - return port; - } - } - - throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}"); - } - - /// - /// Check if a specific port is available for binding - /// - /// Port to check - /// True if port is available - public static bool IsPortAvailable(int port) - { - // Start with quick loopback check - try - { - var testListener = new TcpListener(IPAddress.Loopback, port); - testListener.Start(); - testListener.Stop(); - } - catch (SocketException) - { - return false; - } - -#if UNITY_EDITOR_OSX - // On macOS, the OS might report the port as available (SO_REUSEADDR) even if another process - // is using it, unless we also check active connections or try a stricter bind. - // Double check by trying to Connect to it. If we CAN connect, it's NOT available. - try - { - using var client = new TcpClient(); - var connectTask = client.ConnectAsync(IPAddress.Loopback, port); - // If we connect successfully, someone is listening -> Not available - if (connectTask.Wait(50) && client.Connected) - { - if (IsDebugEnabled()) McpLog.Info($"[PortManager] Port {port} bind succeeded but connection also succeeded -> Not available (Conflict)."); - return false; - } - } - catch - { - // Connection failed -> likely available (or firewall blocked, but we assume available) - if (IsDebugEnabled()) McpLog.Info($"[PortManager] Port {port} connection failed -> likely available."); - } -#endif - - return true; - } - - /// - /// Check if a port is currently being used by MCP for Unity - /// This helps avoid unnecessary port changes when Unity itself is using the port - /// - /// Port to check - /// True if port appears to be used by MCP for Unity - public static bool IsPortUsedByMCPForUnity(int port) - { - try - { - // Try to make a quick connection to see if it's an MCP for Unity server - using var client = new TcpClient(); - var connectTask = client.ConnectAsync(IPAddress.Loopback, port); - if (connectTask.Wait(100)) // 100ms timeout - { - // If connection succeeded, it's likely the MCP for Unity server - return client.Connected; - } - return false; - } - catch - { - return false; - } - } - - /// - /// Wait for a port to become available for a limited amount of time. - /// Used to bridge the gap during domain reload when the old listener - /// hasn't released the socket yet. - /// - private static bool WaitForPortRelease(int port, int timeoutMs) - { - int waited = 0; - const int step = 100; - while (waited < timeoutMs) - { - if (IsPortAvailable(port)) - { - return true; - } - - // If the port is in use by an MCP instance, continue waiting briefly - if (!IsPortUsedByMCPForUnity(port)) - { - // In use by something else; don't keep waiting - return false; - } - - Thread.Sleep(step); - waited += step; - } - return IsPortAvailable(port); - } - - /// - /// Save port to persistent storage - /// - /// Port to save - 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 = GetRegistryFilePath(); - string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented); - // Write to hashed, project-scoped file - File.WriteAllText(registryFile, json, new System.Text.UTF8Encoding(false)); - // Also write to legacy stable filename to avoid hash/case drift across reloads - string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName); - File.WriteAllText(legacy, json, new System.Text.UTF8Encoding(false)); - - if (IsDebugEnabled()) McpLog.Info($"Saved port {port} to storage"); - } - catch (Exception ex) - { - McpLog.Warn($"Could not save port to storage: {ex.Message}"); - } - } - - /// - /// Load port from persistent storage - /// - /// Stored port number, or 0 if not found - private static int LoadStoredPort() - { - try - { - string registryFile = GetRegistryFilePath(); - - if (!File.Exists(registryFile)) - { - // Backwards compatibility: try the legacy file name - string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName); - if (!File.Exists(legacy)) - { - return 0; - } - registryFile = legacy; - } - - string json = File.ReadAllText(registryFile); - var portConfig = JsonConvert.DeserializeObject(json); - - return portConfig?.unity_port ?? 0; - } - catch (Exception ex) - { - McpLog.Warn($"Could not load port from storage: {ex.Message}"); - return 0; - } - } - - /// - /// Get the current stored port configuration - /// - /// Port configuration if exists, null otherwise - public static PortConfig GetStoredPortConfig() - { - try - { - string registryFile = GetRegistryFilePath(); - - if (!File.Exists(registryFile)) - { - // Backwards compatibility: try the legacy file - string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName); - if (!File.Exists(legacy)) - { - return null; - } - registryFile = legacy; - } - - string json = File.ReadAllText(registryFile); - return JsonConvert.DeserializeObject(json); - } - catch (Exception ex) - { - McpLog.Warn($"Could not load port config: {ex.Message}"); - return null; - } - } - - private static string GetRegistryDirectory() - { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp"); - } - - private static string GetRegistryFilePath() - { - string dir = GetRegistryDirectory(); - string hash = ComputeProjectHash(Application.dataPath); - string fileName = $"unity-mcp-port-{hash}.json"; - return Path.Combine(dir, fileName); - } - - private static string ComputeProjectHash(string input) - { - try - { - using SHA1 sha1 = SHA1.Create(); - byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty); - byte[] hashBytes = sha1.ComputeHash(bytes); - var sb = new StringBuilder(); - foreach (byte b in hashBytes) - { - sb.Append(b.ToString("x2")); - } - return sb.ToString()[..8]; // short, sufficient for filenames - } - catch - { - return "default"; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/PortManager.cs.meta b/Assets/MCPForUnity/Editor/Helpers/PortManager.cs.meta deleted file mode 100644 index 5d51d8d..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/PortManager.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 28c39813a10b4331afc764a04089cbef -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/PortManager.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/PrefabUtilityHelper.cs b/Assets/MCPForUnity/Editor/Helpers/PrefabUtilityHelper.cs deleted file mode 100644 index 88397fb..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/PrefabUtilityHelper.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Provides common utility methods for working with Unity Prefab assets. - /// - public static class PrefabUtilityHelper - { - /// - /// Gets the GUID for a prefab asset path. - /// - /// The Unity asset path (e.g., "Assets/Prefabs/MyPrefab.prefab") - /// The GUID string, or null if the path is invalid. - public static string GetPrefabGUID(string assetPath) - { - if (string.IsNullOrEmpty(assetPath)) - { - return null; - } - - try - { - return AssetDatabase.AssetPathToGUID(assetPath); - } - catch (Exception ex) - { - McpLog.Warn($"Failed to get GUID for asset path '{assetPath}': {ex.Message}"); - return null; - } - } - - /// - /// Gets variant information if the prefab is a variant. - /// - /// The prefab GameObject to check. - /// A tuple containing (isVariant, parentPath, parentGuid). - public static (bool isVariant, string parentPath, string parentGuid) GetVariantInfo(GameObject prefabAsset) - { - if (prefabAsset == null) - { - return (false, null, null); - } - - try - { - PrefabAssetType assetType = PrefabUtility.GetPrefabAssetType(prefabAsset); - if (assetType != PrefabAssetType.Variant) - { - return (false, null, null); - } - - GameObject parentAsset = PrefabUtility.GetCorrespondingObjectFromSource(prefabAsset); - if (parentAsset == null) - { - return (true, null, null); - } - - string parentPath = AssetDatabase.GetAssetPath(parentAsset); - string parentGuid = GetPrefabGUID(parentPath); - - return (true, parentPath, parentGuid); - } - catch (Exception ex) - { - McpLog.Warn($"Failed to get variant info for '{prefabAsset.name}': {ex.Message}"); - return (false, null, null); - } - } - - /// - /// Gets the list of component type names on a GameObject. - /// - /// The GameObject to inspect. - /// A list of component type full names. - public static List GetComponentTypeNames(GameObject obj) - { - var typeNames = new List(); - - if (obj == null) - { - return typeNames; - } - - try - { - var components = obj.GetComponents(); - foreach (var component in components) - { - if (component != null) - { - typeNames.Add(component.GetType().FullName); - } - } - } - catch (Exception ex) - { - McpLog.Warn($"Failed to get component types for '{obj.name}': {ex.Message}"); - } - - return typeNames; - } - - /// - /// Recursively counts all children in the hierarchy. - /// - /// The root transform to count from. - /// Total number of children in the hierarchy. - public static int CountChildrenRecursive(Transform transform) - { - if (transform == null) - { - return 0; - } - - int count = transform.childCount; - for (int i = 0; i < transform.childCount; i++) - { - count += CountChildrenRecursive(transform.GetChild(i)); - } - return count; - } - - /// - /// Gets the source prefab path for a nested prefab instance. - /// - /// The GameObject to check. - /// The asset path of the source prefab, or null if not a nested prefab. - public static string GetNestedPrefabPath(GameObject gameObject) - { - if (gameObject == null || !PrefabUtility.IsAnyPrefabInstanceRoot(gameObject)) - { - return null; - } - - try - { - var sourcePrefab = PrefabUtility.GetCorrespondingObjectFromSource(gameObject); - if (sourcePrefab != null) - { - return AssetDatabase.GetAssetPath(sourcePrefab); - } - } - catch (Exception ex) - { - McpLog.Warn($"Failed to get nested prefab path for '{gameObject.name}': {ex.Message}"); - } - - return null; - } - - /// - /// Gets the nesting depth of a prefab instance within the prefab hierarchy. - /// Returns 0 for main prefab root, 1 for first-level nested, 2 for second-level, etc. - /// Returns -1 for non-prefab-root objects. - /// - /// The GameObject to analyze. - /// The root transform of the main prefab asset. - /// Nesting depth (0=main root, 1+=nested), or -1 if not a prefab root. - public static int GetPrefabNestingDepth(GameObject gameObject, Transform mainPrefabRoot) - { - if (gameObject == null) - return -1; - - // Main prefab root - if (gameObject.transform == mainPrefabRoot) - return 0; - - // Not a prefab instance root - if (!PrefabUtility.IsAnyPrefabInstanceRoot(gameObject)) - return -1; - - // Calculate depth by walking up the hierarchy - int depth = 0; - Transform current = gameObject.transform; - - while (current != null && current != mainPrefabRoot) - { - if (PrefabUtility.IsAnyPrefabInstanceRoot(current.gameObject)) - { - depth++; - } - current = current.parent; - } - - return depth; - } - - /// - /// Gets the parent prefab path for a nested prefab instance. - /// Returns null for main prefab root or non-prefab objects. - /// - /// The GameObject to analyze. - /// The root transform of the main prefab asset. - /// The asset path of the parent prefab, or null if none. - public static string GetParentPrefabPath(GameObject gameObject, Transform mainPrefabRoot) - { - if (gameObject == null || gameObject.transform == mainPrefabRoot) - return null; - - if (!PrefabUtility.IsAnyPrefabInstanceRoot(gameObject)) - return null; - - // Walk up the hierarchy to find the parent prefab instance - Transform current = gameObject.transform.parent; - - while (current != null && current != mainPrefabRoot) - { - if (PrefabUtility.IsAnyPrefabInstanceRoot(current.gameObject)) - { - return GetNestedPrefabPath(current.gameObject); - } - current = current.parent; - } - - // Parent is the main prefab root - get its asset path - if (mainPrefabRoot != null) - { - return AssetDatabase.GetAssetPath(mainPrefabRoot.gameObject); - } - - return null; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/PrefabUtilityHelper.cs.meta b/Assets/MCPForUnity/Editor/Helpers/PrefabUtilityHelper.cs.meta deleted file mode 100644 index 38be4c2..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/PrefabUtilityHelper.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: ebe2be77e64f4d4f811614b198210017 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/PrefabUtilityHelper.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs b/Assets/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs deleted file mode 100644 index 34a5391..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; -using MCPForUnity.Editor.Constants; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Provides shared utilities for deriving deterministic project identity information - /// used by transport clients (hash, name, persistent session id). - /// - [InitializeOnLoad] - internal static class ProjectIdentityUtility - { - private const string SessionPrefKey = EditorPrefKeys.SessionId; - private static bool _legacyKeyCleared; - private static string _cachedProjectName = "Unknown"; - private static string _cachedProjectHash = "default"; - private static string _fallbackSessionId; - private static bool _cacheScheduled; - - static ProjectIdentityUtility() - { - ScheduleCacheRefresh(); - EditorApplication.projectChanged += ScheduleCacheRefresh; - } - - private static void ScheduleCacheRefresh() - { - if (_cacheScheduled) - { - return; - } - - _cacheScheduled = true; - EditorApplication.delayCall += CacheIdentityOnMainThread; - } - - private static void CacheIdentityOnMainThread() - { - EditorApplication.delayCall -= CacheIdentityOnMainThread; - _cacheScheduled = false; - UpdateIdentityCache(); - } - - private static void UpdateIdentityCache() - { - try - { - string dataPath = Application.dataPath; - if (string.IsNullOrEmpty(dataPath)) - { - return; - } - - _cachedProjectHash = ComputeProjectHash(dataPath); - _cachedProjectName = ComputeProjectName(dataPath); - } - catch - { - // Ignore and keep defaults - } - } - - /// - /// Returns the SHA1 hash of the current project path (truncated to 16 characters). - /// Matches the legacy hash used by the stdio bridge and server registry. - /// - public static string GetProjectHash() - { - EnsureIdentityCache(); - return _cachedProjectHash; - } - - /// - /// Returns a human friendly project name derived from the Assets directory path, - /// or "Unknown" if the name cannot be determined. - /// - public static string GetProjectName() - { - EnsureIdentityCache(); - return _cachedProjectName; - } - - private static string ComputeProjectHash(string dataPath) - { - try - { - using SHA1 sha1 = SHA1.Create(); - byte[] bytes = Encoding.UTF8.GetBytes(dataPath); - byte[] hashBytes = sha1.ComputeHash(bytes); - var sb = new StringBuilder(); - foreach (byte b in hashBytes) - { - sb.Append(b.ToString("x2")); - } - return sb.ToString(0, Math.Min(16, sb.Length)).ToLowerInvariant(); - } - catch - { - return "default"; - } - } - - private static string ComputeProjectName(string dataPath) - { - try - { - string projectPath = dataPath; - projectPath = projectPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - if (projectPath.EndsWith("Assets", StringComparison.OrdinalIgnoreCase)) - { - projectPath = projectPath[..^6].TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - } - - string name = Path.GetFileName(projectPath); - return string.IsNullOrEmpty(name) ? "Unknown" : name; - } - catch - { - return "Unknown"; - } - } - - /// - /// Persists a server-assigned session id. - /// Safe to call from background threads. - /// - public static void SetSessionId(string sessionId) - { - if (string.IsNullOrEmpty(sessionId)) - { - return; - } - - EditorApplication.delayCall += () => - { - try - { - string projectHash = GetProjectHash(); - string projectSpecificKey = $"{SessionPrefKey}_{projectHash}"; - EditorPrefs.SetString(projectSpecificKey, sessionId); - } - catch (Exception ex) - { - McpLog.Warn($"Failed to persist session ID: {ex.Message}"); - } - }; - } - - /// - /// Retrieves a persistent session id for the plugin, creating one if absent. - /// The session id is unique per project (scoped by project hash). - /// - public static string GetOrCreateSessionId() - { - try - { - // Make the session ID project-specific by including the project hash in the key - string projectHash = GetProjectHash(); - string projectSpecificKey = $"{SessionPrefKey}_{projectHash}"; - - string sessionId = EditorPrefs.GetString(projectSpecificKey, string.Empty); - if (string.IsNullOrEmpty(sessionId)) - { - sessionId = Guid.NewGuid().ToString(); - EditorPrefs.SetString(projectSpecificKey, sessionId); - } - return sessionId; - } - catch - { - // If prefs are unavailable (e.g. during batch tests) fall back to runtime guid. - if (string.IsNullOrEmpty(_fallbackSessionId)) - { - _fallbackSessionId = Guid.NewGuid().ToString(); - } - - return _fallbackSessionId; - } - } - - /// - /// Clears the persisted session id (mainly for tests). - /// - public static void ResetSessionId() - { - try - { - // Clear the project-specific session ID - string projectHash = GetProjectHash(); - string projectSpecificKey = $"{SessionPrefKey}_{projectHash}"; - - if (EditorPrefs.HasKey(projectSpecificKey)) - { - EditorPrefs.DeleteKey(projectSpecificKey); - } - - if (!_legacyKeyCleared && EditorPrefs.HasKey(SessionPrefKey)) - { - EditorPrefs.DeleteKey(SessionPrefKey); - _legacyKeyCleared = true; - } - - _fallbackSessionId = null; - } - catch - { - // Ignore - } - } - - private static void EnsureIdentityCache() - { - // When Application.dataPath is unavailable (e.g., batch mode) we fall back to - // hashing the current working directory/Assets path so each project still - // derives a deterministic, per-project session id rather than sharing "default". - if (!string.IsNullOrEmpty(_cachedProjectHash) && _cachedProjectHash != "default") - { - return; - } - - UpdateIdentityCache(); - - if (!string.IsNullOrEmpty(_cachedProjectHash) && _cachedProjectHash != "default") - { - return; - } - - string fallback = TryComputeFallbackProjectHash(); - if (!string.IsNullOrEmpty(fallback)) - { - _cachedProjectHash = fallback; - } - } - - private static string TryComputeFallbackProjectHash() - { - try - { - string workingDirectory = Directory.GetCurrentDirectory(); - if (string.IsNullOrEmpty(workingDirectory)) - { - return "default"; - } - - // Normalise trailing separators so hashes remain stable - workingDirectory = workingDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - return ComputeProjectHash(Path.Combine(workingDirectory, "Assets")); - } - catch - { - return "default"; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs.meta b/Assets/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs.meta deleted file mode 100644 index a3f95b9..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 936e878ce1275453bae5e0cf03bd9d30 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/ProjectIdentityUtility.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/PropertyConversion.cs b/Assets/MCPForUnity/Editor/Helpers/PropertyConversion.cs deleted file mode 100644 index 0e3af0a..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/PropertyConversion.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using MCPForUnity.Editor.Helpers; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Unified property conversion from JSON to Unity types. - /// Uses UnityJsonSerializer for consistent type handling. - /// - public static class PropertyConversion - { - /// - /// Converts a JToken to the specified target type using Unity type converters. - /// - /// The JSON token to convert - /// The target type to convert to - /// The converted object, or null if conversion fails - public static object ConvertToType(JToken token, Type targetType) - { - if (token == null || token.Type == JTokenType.Null) - { - if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null) - { - McpLog.Warn($"[PropertyConversion] Cannot assign null to non-nullable value type {targetType.Name}. Returning default value."); - return Activator.CreateInstance(targetType); - } - return null; - } - - try - { - // Use the shared Unity serializer with custom converters - return token.ToObject(targetType, UnityJsonSerializer.Instance); - } - catch (Exception ex) - { - McpLog.Error($"Error converting token to {targetType.FullName}: {ex.Message}\nToken: {token.ToString(Formatting.None)}"); - throw; - } - } - - /// - /// Tries to convert a JToken to the specified target type. - /// Returns null and logs warning on failure (does not throw). - /// - public static object TryConvertToType(JToken token, Type targetType) - { - try - { - return ConvertToType(token, targetType); - } - catch - { - return null; - } - } - - /// - /// Generic version of ConvertToType. - /// - public static T ConvertTo(JToken token) - { - return (T)ConvertToType(token, typeof(T)); - } - - /// - /// Converts a JToken to a Unity asset by loading from path. - /// - /// JToken containing asset path - /// Expected asset type - /// The loaded asset, or null if not found - public static UnityEngine.Object LoadAssetFromToken(JToken token, Type targetType) - { - if (token == null || token.Type != JTokenType.String) - return null; - - string assetPath = AssetPathUtility.SanitizeAssetPath(token.ToString()); - UnityEngine.Object loadedAsset = AssetDatabase.LoadAssetAtPath(assetPath, targetType); - - if (loadedAsset == null) - { - McpLog.Warn($"[PropertyConversion] Could not load asset of type {targetType.Name} from path: {assetPath}"); - } - - return loadedAsset; - } - } -} - diff --git a/Assets/MCPForUnity/Editor/Helpers/PropertyConversion.cs.meta b/Assets/MCPForUnity/Editor/Helpers/PropertyConversion.cs.meta deleted file mode 100644 index 111af3f..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/PropertyConversion.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4b4187d5b338a453fbe0baceaeea6bcd -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/PropertyConversion.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs b/Assets/MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs deleted file mode 100644 index 2065d17..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.Rendering; -using UnityEditor; - -namespace MCPForUnity.Editor.Helpers -{ - internal static class RenderPipelineUtility - { - internal enum PipelineKind - { - BuiltIn, - Universal, - HighDefinition, - Custom - } - - internal enum VFXComponentType - { - ParticleSystem, - LineRenderer, - TrailRenderer - } - - private static Dictionary s_DefaultVFXMaterials = new Dictionary(); - - private static readonly string[] BuiltInLitShaders = { "Standard", "Legacy Shaders/Diffuse" }; - private static readonly string[] BuiltInUnlitShaders = { "Unlit/Color", "Unlit/Texture" }; - private static readonly string[] UrpLitShaders = { "Universal Render Pipeline/Lit", "Universal Render Pipeline/Simple Lit" }; - private static readonly string[] UrpUnlitShaders = { "Universal Render Pipeline/Unlit" }; - private static readonly string[] HdrpLitShaders = { "HDRP/Lit", "High Definition Render Pipeline/Lit" }; - private static readonly string[] HdrpUnlitShaders = { "HDRP/Unlit", "High Definition Render Pipeline/Unlit" }; - - internal static PipelineKind GetActivePipeline() - { - var asset = GraphicsSettings.currentRenderPipeline; - if (asset == null) - { - return PipelineKind.BuiltIn; - } - - var typeName = asset.GetType().FullName ?? string.Empty; - if (typeName.IndexOf("HighDefinition", StringComparison.OrdinalIgnoreCase) >= 0 || - typeName.IndexOf("HDRP", StringComparison.OrdinalIgnoreCase) >= 0) - { - return PipelineKind.HighDefinition; - } - - if (typeName.IndexOf("Universal", StringComparison.OrdinalIgnoreCase) >= 0 || - typeName.IndexOf("URP", StringComparison.OrdinalIgnoreCase) >= 0) - { - return PipelineKind.Universal; - } - - return PipelineKind.Custom; - } - - internal static Shader ResolveShader(string requestedNameOrAlias) - { - var pipeline = GetActivePipeline(); - - if (!string.IsNullOrWhiteSpace(requestedNameOrAlias)) - { - var alias = requestedNameOrAlias.Trim(); - var aliasMatch = ResolveAlias(alias, pipeline); - if (aliasMatch != null) - { - WarnIfPipelineMismatch(aliasMatch.name, pipeline); - return aliasMatch; - } - - var direct = Shader.Find(alias); - if (direct != null) - { - WarnIfPipelineMismatch(direct.name, pipeline); - return direct; - } - - McpLog.Warn($"Shader '{alias}' not found. Falling back to {pipeline} defaults."); - } - - var fallback = ResolveDefaultLitShader(pipeline) - ?? ResolveDefaultLitShader(PipelineKind.BuiltIn) - ?? Shader.Find("Unlit/Color"); - - if (fallback != null) - { - WarnIfPipelineMismatch(fallback.name, pipeline); - } - - return fallback; - } - - internal static Shader ResolveDefaultLitShader(PipelineKind pipeline) - { - return pipeline switch - { - PipelineKind.HighDefinition => TryFindShader(HdrpLitShaders) ?? TryFindShader(UrpLitShaders), - PipelineKind.Universal => TryFindShader(UrpLitShaders) ?? TryFindShader(HdrpLitShaders), - PipelineKind.Custom => TryFindShader(BuiltInLitShaders) ?? TryFindShader(UrpLitShaders) ?? TryFindShader(HdrpLitShaders), - _ => TryFindShader(BuiltInLitShaders) ?? Shader.Find("Unlit/Color") - }; - } - - internal static Shader ResolveDefaultUnlitShader(PipelineKind pipeline) - { - return pipeline switch - { - PipelineKind.HighDefinition => TryFindShader(HdrpUnlitShaders) ?? TryFindShader(UrpUnlitShaders) ?? TryFindShader(BuiltInUnlitShaders), - PipelineKind.Universal => TryFindShader(UrpUnlitShaders) ?? TryFindShader(HdrpUnlitShaders) ?? TryFindShader(BuiltInUnlitShaders), - PipelineKind.Custom => TryFindShader(BuiltInUnlitShaders) ?? TryFindShader(UrpUnlitShaders) ?? TryFindShader(HdrpUnlitShaders), - _ => TryFindShader(BuiltInUnlitShaders) - }; - } - - private static Shader ResolveAlias(string alias, PipelineKind pipeline) - { - if (string.Equals(alias, "lit", StringComparison.OrdinalIgnoreCase) || - string.Equals(alias, "default", StringComparison.OrdinalIgnoreCase) || - string.Equals(alias, "default_lit", StringComparison.OrdinalIgnoreCase) || - string.Equals(alias, "standard", StringComparison.OrdinalIgnoreCase)) - { - return ResolveDefaultLitShader(pipeline); - } - - if (string.Equals(alias, "unlit", StringComparison.OrdinalIgnoreCase)) - { - return ResolveDefaultUnlitShader(pipeline); - } - - if (string.Equals(alias, "urp_lit", StringComparison.OrdinalIgnoreCase)) - { - return TryFindShader(UrpLitShaders); - } - - if (string.Equals(alias, "hdrp_lit", StringComparison.OrdinalIgnoreCase)) - { - return TryFindShader(HdrpLitShaders); - } - - if (string.Equals(alias, "built_in_lit", StringComparison.OrdinalIgnoreCase)) - { - return TryFindShader(BuiltInLitShaders); - } - - return null; - } - - private static Shader TryFindShader(params string[] shaderNames) - { - foreach (var shaderName in shaderNames) - { - var shader = Shader.Find(shaderName); - if (shader != null) - { - return shader; - } - } - return null; - } - - private static void WarnIfPipelineMismatch(string shaderName, PipelineKind activePipeline) - { - if (string.IsNullOrEmpty(shaderName)) - { - return; - } - - var lowerName = shaderName.ToLowerInvariant(); - bool shaderLooksUrp = lowerName.Contains("universal render pipeline") || lowerName.Contains("urp/"); - bool shaderLooksHdrp = lowerName.Contains("high definition render pipeline") || lowerName.Contains("hdrp/"); - bool shaderLooksBuiltin = lowerName.Contains("standard") || lowerName.Contains("legacy shaders/"); - bool shaderLooksSrp = shaderLooksUrp || shaderLooksHdrp; - - switch (activePipeline) - { - case PipelineKind.HighDefinition: - if (shaderLooksUrp) - { - McpLog.Warn($"[RenderPipelineUtility] Active pipeline is HDRP but shader '{shaderName}' looks URP-based. Asset may appear incorrect."); - } - else if (shaderLooksBuiltin && !shaderLooksHdrp) - { - McpLog.Warn($"[RenderPipelineUtility] Active pipeline is HDRP but shader '{shaderName}' looks Built-in. Consider using an HDRP shader for correct results."); - } - break; - case PipelineKind.Universal: - if (shaderLooksHdrp) - { - McpLog.Warn($"[RenderPipelineUtility] Active pipeline is URP but shader '{shaderName}' looks HDRP-based. Asset may appear incorrect."); - } - else if (shaderLooksBuiltin && !shaderLooksUrp) - { - McpLog.Warn($"[RenderPipelineUtility] Active pipeline is URP but shader '{shaderName}' looks Built-in. Consider using a URP shader for correct results."); - } - break; - case PipelineKind.BuiltIn: - if (shaderLooksSrp) - { - McpLog.Warn($"[RenderPipelineUtility] Active pipeline is Built-in but shader '{shaderName}' targets URP/HDRP. Asset may not render as expected."); - } - break; - } - } - - internal static Material GetOrCreateDefaultVFXMaterial(VFXComponentType componentType) - { - var pipeline = GetActivePipeline(); - string cacheKey = $"{pipeline}_{componentType}"; - - if (s_DefaultVFXMaterials.TryGetValue(cacheKey, out Material cachedMaterial) && cachedMaterial != null) - { - return cachedMaterial; - } - - Material material = null; - - if (pipeline == PipelineKind.BuiltIn) - { - string builtinPath = componentType == VFXComponentType.ParticleSystem - ? "Default-Particle.mat" - : "Default-Line.mat"; - - material = AssetDatabase.GetBuiltinExtraResource(builtinPath); - } - - if (material == null) - { - Shader shader = ResolveDefaultUnlitShader(pipeline); - if (shader == null) - { - shader = Shader.Find("Unlit/Color"); - } - - if (shader != null) - { - material = new Material(shader); - material.name = $"Auto_Default_{componentType}_{pipeline}"; - - // Set default color (white is standard for VFX) - if (material.HasProperty("_Color")) - { - material.SetColor("_Color", Color.white); - } - if (material.HasProperty("_BaseColor")) - { - material.SetColor("_BaseColor", Color.white); - } - - if (componentType == VFXComponentType.ParticleSystem) - { - material.renderQueue = 3000; - if (material.HasProperty("_Mode")) - { - material.SetFloat("_Mode", 2); - } - if (material.HasProperty("_SrcBlend")) - { - material.SetFloat("_SrcBlend", (float)UnityEngine.Rendering.BlendMode.SrcAlpha); - } - if (material.HasProperty("_DstBlend")) - { - material.SetFloat("_DstBlend", (float)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); - } - if (material.HasProperty("_ZWrite")) - { - material.SetFloat("_ZWrite", 0); - } - } - - McpLog.Info($"[RenderPipelineUtility] Created default VFX material for {componentType} using {shader.name}"); - } - } - - if (material != null) - { - s_DefaultVFXMaterials[cacheKey] = material; - } - - return material; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs.meta b/Assets/MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs.meta deleted file mode 100644 index 8405344..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 5a0a1cfd55ab4bc99c74c52854f6bdf3 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/RenderPipelineUtility.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/RendererHelpers.cs b/Assets/MCPForUnity/Editor/Helpers/RendererHelpers.cs deleted file mode 100644 index 07e39a5..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/RendererHelpers.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; -using UnityEngine; -using UnityEditor; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Utility class for common Renderer property operations. - /// Used by ManageVFX for ParticleSystem, LineRenderer, and TrailRenderer components. - /// - public static class RendererHelpers - { - /// - /// Ensures a renderer has a material assigned. If not, auto-assigns a default material - /// based on the render pipeline and component type. - /// - /// The renderer to check - public static void EnsureMaterial(Renderer renderer) - { - if (renderer == null || renderer.sharedMaterial != null) - { - return; - } - - RenderPipelineUtility.VFXComponentType? componentType = null; - if (renderer is ParticleSystemRenderer) - { - componentType = RenderPipelineUtility.VFXComponentType.ParticleSystem; - } - else if (renderer is LineRenderer) - { - componentType = RenderPipelineUtility.VFXComponentType.LineRenderer; - } - else if (renderer is TrailRenderer) - { - componentType = RenderPipelineUtility.VFXComponentType.TrailRenderer; - } - - if (componentType.HasValue) - { - Material defaultMat = RenderPipelineUtility.GetOrCreateDefaultVFXMaterial(componentType.Value); - if (defaultMat != null) - { - Undo.RecordObject(renderer, "Assign default VFX material"); - EditorUtility.SetDirty(renderer); - renderer.sharedMaterial = defaultMat; - } - } - } - - /// - /// Applies common Renderer properties (shadows, lighting, probes, sorting, rendering layer). - /// Used by ParticleSetRenderer, LineSetProperties, TrailSetProperties. - /// - public static void ApplyCommonRendererProperties(Renderer renderer, JObject @params, List changes) - { - // Shadows - if (@params["shadowCastingMode"] != null && Enum.TryParse(@params["shadowCastingMode"].ToString(), true, out var shadowMode)) - { renderer.shadowCastingMode = shadowMode; changes.Add("shadowCastingMode"); } - if (@params["receiveShadows"] != null) { renderer.receiveShadows = @params["receiveShadows"].ToObject(); changes.Add("receiveShadows"); } - // Note: shadowBias is only available on specific renderer types (e.g., ParticleSystemRenderer), not base Renderer - - // Lighting and probes - if (@params["lightProbeUsage"] != null && Enum.TryParse(@params["lightProbeUsage"].ToString(), true, out var probeUsage)) - { renderer.lightProbeUsage = probeUsage; changes.Add("lightProbeUsage"); } - if (@params["reflectionProbeUsage"] != null && Enum.TryParse(@params["reflectionProbeUsage"].ToString(), true, out var reflectionUsage)) - { renderer.reflectionProbeUsage = reflectionUsage; changes.Add("reflectionProbeUsage"); } - - // Motion vectors - if (@params["motionVectorGenerationMode"] != null && Enum.TryParse(@params["motionVectorGenerationMode"].ToString(), true, out var motionMode)) - { renderer.motionVectorGenerationMode = motionMode; changes.Add("motionVectorGenerationMode"); } - - // Sorting - if (@params["sortingOrder"] != null) { renderer.sortingOrder = @params["sortingOrder"].ToObject(); changes.Add("sortingOrder"); } - if (@params["sortingLayerName"] != null) { renderer.sortingLayerName = @params["sortingLayerName"].ToString(); changes.Add("sortingLayerName"); } - if (@params["sortingLayerID"] != null) { renderer.sortingLayerID = @params["sortingLayerID"].ToObject(); changes.Add("sortingLayerID"); } - - // Rendering layer mask (for SRP) - if (@params["renderingLayerMask"] != null) { renderer.renderingLayerMask = @params["renderingLayerMask"].ToObject(); changes.Add("renderingLayerMask"); } - } - - /// - /// Gets common Renderer properties for GetInfo methods. - /// - public static object GetCommonRendererInfo(Renderer renderer) - { - return new - { - shadowCastingMode = renderer.shadowCastingMode.ToString(), - receiveShadows = renderer.receiveShadows, - lightProbeUsage = renderer.lightProbeUsage.ToString(), - reflectionProbeUsage = renderer.reflectionProbeUsage.ToString(), - sortingOrder = renderer.sortingOrder, - sortingLayerName = renderer.sortingLayerName, - renderingLayerMask = renderer.renderingLayerMask - }; - } - - - /// - /// Sets width properties for LineRenderer or TrailRenderer. - /// - /// JSON parameters containing width, startWidth, endWidth, widthCurve, widthMultiplier - /// List to track changed properties - /// Action to set start width - /// Action to set end width - /// Action to set width curve - /// Action to set width multiplier - /// Function to parse animation curve from JToken - public static void ApplyWidthProperties(JObject @params, List changes, - Action setStartWidth, Action setEndWidth, - Action setWidthCurve, Action setWidthMultiplier, - Func parseAnimationCurve) - { - if (@params["width"] != null) - { - float w = @params["width"].ToObject(); - setStartWidth(w); - setEndWidth(w); - changes.Add("width"); - } - if (@params["startWidth"] != null) { setStartWidth(@params["startWidth"].ToObject()); changes.Add("startWidth"); } - if (@params["endWidth"] != null) { setEndWidth(@params["endWidth"].ToObject()); changes.Add("endWidth"); } - if (@params["widthCurve"] != null) { setWidthCurve(parseAnimationCurve(@params["widthCurve"], 1f)); changes.Add("widthCurve"); } - if (@params["widthMultiplier"] != null) { setWidthMultiplier(@params["widthMultiplier"].ToObject()); changes.Add("widthMultiplier"); } - } - - /// - /// Sets color properties for LineRenderer or TrailRenderer. - /// - /// JSON parameters containing color, startColor, endColor, gradient - /// List to track changed properties - /// Action to set start color - /// Action to set end color - /// Action to set gradient - /// Function to parse color from JToken - /// Function to parse gradient from JToken - /// If true, sets end color alpha to 0 when using single color - public static void ApplyColorProperties(JObject @params, List changes, - Action setStartColor, Action setEndColor, - Action setGradient, - Func parseColor, Func parseGradient, - bool fadeEndAlpha = false) - { - if (@params["color"] != null) - { - Color c = parseColor(@params["color"]); - setStartColor(c); - setEndColor(fadeEndAlpha ? new Color(c.r, c.g, c.b, 0f) : c); - changes.Add("color"); - } - if (@params["startColor"] != null) { setStartColor(parseColor(@params["startColor"])); changes.Add("startColor"); } - if (@params["endColor"] != null) { setEndColor(parseColor(@params["endColor"])); changes.Add("endColor"); } - if (@params["gradient"] != null) { setGradient(parseGradient(@params["gradient"])); changes.Add("gradient"); } - } - - - /// - /// Sets material for a Renderer. - /// - /// The renderer to set material on - /// JSON parameters containing materialPath - /// Name for the undo operation - /// Function to find material by path - /// If true, auto-assigns default material when materialPath is not provided - public static object SetRendererMaterial(Renderer renderer, JObject @params, string undoName, Func findMaterial, bool autoAssignDefault = true) - { - if (renderer == null) return new { success = false, message = "Renderer not found" }; - - string path = @params["materialPath"]?.ToString(); - - if (string.IsNullOrEmpty(path)) - { - if (!autoAssignDefault) - { - return new { success = false, message = "materialPath required" }; - } - - RenderPipelineUtility.VFXComponentType? componentType = null; - if (renderer is ParticleSystemRenderer) - { - componentType = RenderPipelineUtility.VFXComponentType.ParticleSystem; - } - else if (renderer is LineRenderer) - { - componentType = RenderPipelineUtility.VFXComponentType.LineRenderer; - } - else if (renderer is TrailRenderer) - { - componentType = RenderPipelineUtility.VFXComponentType.TrailRenderer; - } - - if (componentType.HasValue) - { - Material defaultMat = RenderPipelineUtility.GetOrCreateDefaultVFXMaterial(componentType.Value); - if (defaultMat != null) - { - Undo.RecordObject(renderer, undoName); - renderer.sharedMaterial = defaultMat; - EditorUtility.SetDirty(renderer); - return new { success = true, message = $"Auto-assigned default material: {defaultMat.name}" }; - } - } - - return new { success = false, message = "materialPath required" }; - } - - Material mat = findMaterial(path); - if (mat == null) return new { success = false, message = $"Material not found: {path}" }; - - Undo.RecordObject(renderer, undoName); - renderer.sharedMaterial = mat; - EditorUtility.SetDirty(renderer); - - return new { success = true, message = $"Set material to {mat.name}" }; - } - - - /// - /// Applies Line/Trail specific properties (loop, alignment, textureMode, etc.). - /// - public static void ApplyLineTrailProperties(JObject @params, List changes, - Action setLoop, Action setUseWorldSpace, - Action setNumCornerVertices, Action setNumCapVertices, - Action setAlignment, Action setTextureMode, - Action setGenerateLightingData) - { - if (@params["loop"] != null && setLoop != null) { setLoop(@params["loop"].ToObject()); changes.Add("loop"); } - if (@params["useWorldSpace"] != null && setUseWorldSpace != null) { setUseWorldSpace(@params["useWorldSpace"].ToObject()); changes.Add("useWorldSpace"); } - if (@params["numCornerVertices"] != null && setNumCornerVertices != null) { setNumCornerVertices(@params["numCornerVertices"].ToObject()); changes.Add("numCornerVertices"); } - if (@params["numCapVertices"] != null && setNumCapVertices != null) { setNumCapVertices(@params["numCapVertices"].ToObject()); changes.Add("numCapVertices"); } - if (@params["alignment"] != null && setAlignment != null && Enum.TryParse(@params["alignment"].ToString(), true, out var align)) { setAlignment(align); changes.Add("alignment"); } - if (@params["textureMode"] != null && setTextureMode != null && Enum.TryParse(@params["textureMode"].ToString(), true, out var texMode)) { setTextureMode(texMode); changes.Add("textureMode"); } - if (@params["generateLightingData"] != null && setGenerateLightingData != null) { setGenerateLightingData(@params["generateLightingData"].ToObject()); changes.Add("generateLightingData"); } - } - - } -} - diff --git a/Assets/MCPForUnity/Editor/Helpers/RendererHelpers.cs.meta b/Assets/MCPForUnity/Editor/Helpers/RendererHelpers.cs.meta deleted file mode 100644 index a403ee9..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/RendererHelpers.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 8f3a7e2d5c1b4a9e6d0f8c3b2a1e5d7c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/RendererHelpers.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/Response.cs b/Assets/MCPForUnity/Editor/Helpers/Response.cs deleted file mode 100644 index 6c6c299..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/Response.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Newtonsoft.Json; - -namespace MCPForUnity.Editor.Helpers -{ - public interface IMcpResponse - { - [JsonProperty("success")] - bool Success { get; } - } - - public sealed class SuccessResponse : IMcpResponse - { - [JsonProperty("success")] - public bool Success => true; - - [JsonIgnore] - public bool success => Success; // Backward-compatible casing for reflection-based tests - - [JsonProperty("message")] - public string Message { get; } - - [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] - public object Data { get; } - - [JsonIgnore] - public object data => Data; - - public SuccessResponse(string message, object data = null) - { - Message = message; - Data = data; - } - } - - public sealed class ErrorResponse : IMcpResponse - { - [JsonProperty("success")] - public bool Success => false; - - [JsonIgnore] - public bool success => Success; // Backward-compatible casing for reflection-based tests - - [JsonProperty("code", NullValueHandling = NullValueHandling.Ignore)] - public string Code { get; } - - [JsonIgnore] - public string code => Code; - - [JsonProperty("error")] - public string Error { get; } - - [JsonIgnore] - public string error => Error; - - [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] - public object Data { get; } - - [JsonIgnore] - public object data => Data; - - public ErrorResponse(string messageOrCode, object data = null) - { - Code = messageOrCode; - Error = messageOrCode; - Data = data; - } - } - - public sealed class PendingResponse : IMcpResponse - { - [JsonProperty("success")] - public bool Success => true; - - [JsonIgnore] - public bool success => Success; // Backward-compatible casing for reflection-based tests - - [JsonProperty("_mcp_status")] - public string Status => "pending"; - - [JsonIgnore] - public string _mcp_status => Status; - - [JsonProperty("_mcp_poll_interval")] - public double PollIntervalSeconds { get; } - - [JsonIgnore] - public double _mcp_poll_interval => PollIntervalSeconds; - - [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)] - public string Message { get; } - - [JsonIgnore] - public string message => Message; - - [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] - public object Data { get; } - - [JsonIgnore] - public object data => Data; - - public PendingResponse(string message = "", double pollIntervalSeconds = 1.0, object data = null) - { - Message = string.IsNullOrEmpty(message) ? null : message; - PollIntervalSeconds = pollIntervalSeconds; - Data = data; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/Response.cs.meta b/Assets/MCPForUnity/Editor/Helpers/Response.cs.meta deleted file mode 100644 index 4f5c61d..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/Response.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 80c09a76b944f8c4691e06c4d76c4be8 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/Response.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/StringCaseUtility.cs b/Assets/MCPForUnity/Editor/Helpers/StringCaseUtility.cs deleted file mode 100644 index 04b498a..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/StringCaseUtility.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Utility class for converting between naming conventions (snake_case, camelCase). - /// Consolidates previously duplicated implementations from ToolParams, ManageVFX, - /// BatchExecute, CommandRegistry, and ToolDiscoveryService. - /// - public static class StringCaseUtility - { - /// - /// Checks whether a type belongs to the built-in MCP for Unity package. - /// Returns true when the type's namespace starts with - /// or its assembly is MCPForUnity.Editor. - /// - public static bool IsBuiltInMcpType(Type type, string assemblyName, string builtInNamespacePrefix) - { - if (type != null && !string.IsNullOrEmpty(type.Namespace) - && type.Namespace.StartsWith(builtInNamespacePrefix, StringComparison.Ordinal)) - { - return true; - } - - if (!string.IsNullOrEmpty(assemblyName) - && assemblyName.Equals("MCPForUnity.Editor", StringComparison.Ordinal)) - { - return true; - } - - return false; - } - - /// - /// Converts a camelCase string to snake_case. - /// Example: "searchMethod" -> "search_method", "param1Value" -> "param1_value" - /// - /// The camelCase string to convert - /// The snake_case equivalent, or original string if null/empty - public static string ToSnakeCase(string str) - { - if (string.IsNullOrEmpty(str)) - return str; - - return Regex.Replace(str, "([a-z0-9])([A-Z])", "$1_$2").ToLowerInvariant(); - } - - /// - /// Converts a snake_case string to camelCase. - /// Example: "search_method" -> "searchMethod" - /// - /// The snake_case string to convert - /// The camelCase equivalent, or original string if null/empty or no underscores - public static string ToCamelCase(string str) - { - if (string.IsNullOrEmpty(str) || !str.Contains("_")) - return str; - - var parts = str.Split('_'); - if (parts.Length == 0) - return str; - - // First part stays lowercase, rest get capitalized - var first = parts[0]; - var rest = string.Concat(parts.Skip(1).Select(part => - string.IsNullOrEmpty(part) ? "" : char.ToUpperInvariant(part[0]) + part.Substring(1))); - - return first + rest; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/StringCaseUtility.cs.meta b/Assets/MCPForUnity/Editor/Helpers/StringCaseUtility.cs.meta deleted file mode 100644 index 25b1b30..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/StringCaseUtility.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: f22b312318ade42c4bb6b5dfddacecfa -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/StringCaseUtility.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/TelemetryHelper.cs b/Assets/MCPForUnity/Editor/Helpers/TelemetryHelper.cs deleted file mode 100644 index 6e092d4..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/TelemetryHelper.cs +++ /dev/null @@ -1,226 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Services.Transport.Transports; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Unity Bridge telemetry helper for collecting usage analytics - /// Following privacy-first approach with easy opt-out mechanisms - /// - public static class TelemetryHelper - { - private const string TELEMETRY_DISABLED_KEY = EditorPrefKeys.TelemetryDisabled; - private const string CUSTOMER_UUID_KEY = EditorPrefKeys.CustomerUuid; - private static Action> s_sender; - - /// - /// Check if telemetry is enabled (can be disabled via Environment Variable or EditorPrefs) - /// - public static bool IsEnabled - { - get - { - // Check environment variables first - var envDisable = Environment.GetEnvironmentVariable("DISABLE_TELEMETRY"); - if (!string.IsNullOrEmpty(envDisable) && - (envDisable.ToLower() == "true" || envDisable == "1")) - { - return false; - } - - var unityMcpDisable = Environment.GetEnvironmentVariable("UNITY_MCP_DISABLE_TELEMETRY"); - if (!string.IsNullOrEmpty(unityMcpDisable) && - (unityMcpDisable.ToLower() == "true" || unityMcpDisable == "1")) - { - return false; - } - - // Honor protocol-wide opt-out as well - var mcpDisable = Environment.GetEnvironmentVariable("MCP_DISABLE_TELEMETRY"); - if (!string.IsNullOrEmpty(mcpDisable) && - (mcpDisable.Equals("true", StringComparison.OrdinalIgnoreCase) || mcpDisable == "1")) - { - return false; - } - - // Check EditorPrefs - return !UnityEditor.EditorPrefs.GetBool(TELEMETRY_DISABLED_KEY, false); - } - } - - /// - /// Get or generate customer UUID for anonymous tracking - /// - public static string GetCustomerUUID() - { - var uuid = UnityEditor.EditorPrefs.GetString(CUSTOMER_UUID_KEY, ""); - if (string.IsNullOrEmpty(uuid)) - { - uuid = System.Guid.NewGuid().ToString(); - UnityEditor.EditorPrefs.SetString(CUSTOMER_UUID_KEY, uuid); - } - return uuid; - } - - /// - /// Disable telemetry (stored in EditorPrefs) - /// - public static void DisableTelemetry() - { - UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, true); - } - - /// - /// Enable telemetry (stored in EditorPrefs) - /// - public static void EnableTelemetry() - { - UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, false); - } - - /// - /// Send telemetry data to MCP server for processing - /// This is a lightweight bridge - the actual telemetry logic is in the MCP server - /// - public static void RecordEvent(string eventType, Dictionary data = null) - { - if (!IsEnabled) - return; - - try - { - var telemetryData = new Dictionary - { - ["event_type"] = eventType, - ["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - ["customer_uuid"] = GetCustomerUUID(), - ["unity_version"] = Application.unityVersion, - ["platform"] = Application.platform.ToString(), - ["source"] = "unity_bridge" - }; - - if (data != null) - { - telemetryData["data"] = data; - } - - // Send to MCP server via existing bridge communication - // The MCP server will handle actual telemetry transmission - SendTelemetryToMcpServer(telemetryData); - } - catch (Exception e) - { - // Never let telemetry errors interfere with functionality - if (IsDebugEnabled()) - { - McpLog.Warn($"Telemetry error (non-blocking): {e.Message}"); - } - } - } - - /// - /// Allows the bridge to register a concrete sender for telemetry payloads. - /// - public static void RegisterTelemetrySender(Action> sender) - { - Interlocked.Exchange(ref s_sender, sender); - } - - public static void UnregisterTelemetrySender() - { - Interlocked.Exchange(ref s_sender, null); - } - - /// - /// Record bridge startup event - /// - public static void RecordBridgeStartup() - { - RecordEvent("bridge_startup", new Dictionary - { - ["bridge_version"] = AssetPathUtility.GetPackageVersion(), - ["auto_connect"] = StdioBridgeHost.IsAutoConnectMode() - }); - } - - /// - /// Record bridge connection event - /// - public static void RecordBridgeConnection(bool success, string error = null) - { - var data = new Dictionary - { - ["success"] = success - }; - - if (!string.IsNullOrEmpty(error)) - { - data["error"] = error.Substring(0, Math.Min(200, error.Length)); - } - - RecordEvent("bridge_connection", data); - } - - /// - /// Record tool execution from Unity side - /// - public static void RecordToolExecution(string toolName, bool success, float durationMs, string error = null) - { - var data = new Dictionary - { - ["tool_name"] = toolName, - ["success"] = success, - ["duration_ms"] = Math.Round(durationMs, 2) - }; - - if (!string.IsNullOrEmpty(error)) - { - data["error"] = error.Substring(0, Math.Min(200, error.Length)); - } - - RecordEvent("tool_execution_unity", data); - } - - private static void SendTelemetryToMcpServer(Dictionary telemetryData) - { - var sender = Volatile.Read(ref s_sender); - if (sender != null) - { - try - { - sender(telemetryData); - return; - } - catch (Exception e) - { - if (IsDebugEnabled()) - { - McpLog.Warn($"Telemetry sender error (non-blocking): {e.Message}"); - } - } - } - - // Fallback: log when debug is enabled - if (IsDebugEnabled()) - { - McpLog.Info($"Telemetry: {telemetryData["event_type"]}"); - } - } - - private static bool IsDebugEnabled() - { - try - { - return UnityEditor.EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false); - } - catch - { - return false; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/TelemetryHelper.cs.meta b/Assets/MCPForUnity/Editor/Helpers/TelemetryHelper.cs.meta deleted file mode 100644 index 6789274..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/TelemetryHelper.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b8f3c2d1e7a94f6c8a9b5e3d2c1a0f9e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/TelemetryHelper.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/TextureOps.cs b/Assets/MCPForUnity/Editor/Helpers/TextureOps.cs deleted file mode 100644 index b062b47..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/TextureOps.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Newtonsoft.Json.Linq; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - public static class TextureOps - { - public static byte[] EncodeTexture(Texture2D texture, string assetPath) - { - if (texture == null) - return null; - - string extension = Path.GetExtension(assetPath); - if (string.IsNullOrEmpty(extension)) - { - McpLog.Warn($"[TextureOps] No file extension for '{assetPath}', defaulting to PNG."); - return texture.EncodeToPNG(); - } - - switch (extension.ToLowerInvariant()) - { - case ".png": - return texture.EncodeToPNG(); - case ".jpg": - case ".jpeg": - return texture.EncodeToJPG(); - default: - McpLog.Warn($"[TextureOps] Unsupported extension '{extension}' for '{assetPath}', defaulting to PNG."); - return texture.EncodeToPNG(); - } - } - - public static void FillTexture(Texture2D texture, Color32 color) - { - if (texture == null) - return; - - Color32[] pixels = new Color32[texture.width * texture.height]; - for (int i = 0; i < pixels.Length; i++) - { - pixels[i] = color; - } - texture.SetPixels32(pixels); - } - - public static Color32 ParseColor32(JArray colorArray) - { - if (colorArray == null || colorArray.Count < 3) - return new Color32(255, 255, 255, 255); - - byte r = (byte)Mathf.Clamp(colorArray[0].ToObject(), 0, 255); - byte g = (byte)Mathf.Clamp(colorArray[1].ToObject(), 0, 255); - byte b = (byte)Mathf.Clamp(colorArray[2].ToObject(), 0, 255); - byte a = colorArray.Count > 3 ? (byte)Mathf.Clamp(colorArray[3].ToObject(), 0, 255) : (byte)255; - - return new Color32(r, g, b, a); - } - - public static List ParsePalette(JArray paletteArray) - { - if (paletteArray == null) - return null; - - List palette = new List(); - foreach (var item in paletteArray) - { - if (item is JArray colorArray) - { - palette.Add(ParseColor32(colorArray)); - } - } - return palette.Count > 0 ? palette : null; - } - - public static void ApplyPixelData(Texture2D texture, JToken pixelsToken, int width, int height) - { - ApplyPixelDataToRegion(texture, pixelsToken, 0, 0, width, height); - } - - public static void ApplyPixelDataToRegion(Texture2D texture, JToken pixelsToken, int offsetX, int offsetY, int regionWidth, int regionHeight) - { - if (texture == null || pixelsToken == null) - return; - - int textureWidth = texture.width; - int textureHeight = texture.height; - - if (pixelsToken is JArray pixelArray) - { - int index = 0; - for (int y = 0; y < regionHeight && index < pixelArray.Count; y++) - { - for (int x = 0; x < regionWidth && index < pixelArray.Count; x++) - { - var pixelColor = pixelArray[index] as JArray; - if (pixelColor != null) - { - int px = offsetX + x; - int py = offsetY + y; - if (px >= 0 && px < textureWidth && py >= 0 && py < textureHeight) - { - texture.SetPixel(px, py, ParseColor32(pixelColor)); - } - } - index++; - } - } - - int expectedCount = regionWidth * regionHeight; - if (pixelArray.Count != expectedCount) - { - McpLog.Warn($"[TextureOps] Pixel array size mismatch: expected {expectedCount} entries, got {pixelArray.Count}"); - } - } - else if (pixelsToken.Type == JTokenType.String) - { - string pixelString = pixelsToken.ToString(); - string base64 = pixelString.StartsWith("base64:") ? pixelString.Substring(7) : pixelString; - if (!pixelString.StartsWith("base64:")) - { - McpLog.Warn("[TextureOps] Base64 pixel data missing 'base64:' prefix; attempting to decode."); - } - - byte[] rawData = Convert.FromBase64String(base64); - - // Assume RGBA32 format: 4 bytes per pixel - int expectedBytes = regionWidth * regionHeight * 4; - if (rawData.Length == expectedBytes) - { - int pixelIndex = 0; - for (int y = 0; y < regionHeight; y++) - { - for (int x = 0; x < regionWidth; x++) - { - int px = offsetX + x; - int py = offsetY + y; - if (px >= 0 && px < textureWidth && py >= 0 && py < textureHeight) - { - int byteIndex = pixelIndex * 4; - Color32 color = new Color32( - rawData[byteIndex], - rawData[byteIndex + 1], - rawData[byteIndex + 2], - rawData[byteIndex + 3] - ); - texture.SetPixel(px, py, color); - } - pixelIndex++; - } - } - } - else - { - McpLog.Warn($"[TextureOps] Base64 data size mismatch: expected {expectedBytes} bytes, got {rawData.Length}"); - } - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/TextureOps.cs.meta b/Assets/MCPForUnity/Editor/Helpers/TextureOps.cs.meta deleted file mode 100644 index b5269a6..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/TextureOps.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 864ea682d797466a84b6b951f6c4e4ba -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/TextureOps.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/ToolParams.cs b/Assets/MCPForUnity/Editor/Helpers/ToolParams.cs deleted file mode 100644 index 681ae48..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ToolParams.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Newtonsoft.Json.Linq; -using System; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Unified parameter validation and extraction wrapper for MCP tools. - /// Eliminates repetitive IsNullOrEmpty checks and provides consistent error messages. - /// - public class ToolParams - { - private readonly JObject _params; - - public ToolParams(JObject @params) - { - _params = @params ?? throw new ArgumentNullException(nameof(@params)); - } - - /// - /// Get required string parameter. Returns ErrorResponse if missing or empty. - /// - public Result GetRequired(string key, string errorMessage = null) - { - var value = GetString(key); - if (string.IsNullOrEmpty(value)) - { - return Result.Error( - errorMessage ?? $"'{key}' parameter is required." - ); - } - return Result.Success(value); - } - - /// - /// Get optional string parameter with default value. - /// Supports both snake_case and camelCase automatically. - /// - public string Get(string key, string defaultValue = null) - { - return GetString(key) ?? defaultValue; - } - - /// - /// Get optional int parameter. - /// - public int? GetInt(string key, int? defaultValue = null) - { - var str = GetString(key); - if (string.IsNullOrEmpty(str)) return defaultValue; - return int.TryParse(str, out var result) ? result : defaultValue; - } - - /// - /// Get optional bool parameter. - /// Supports both snake_case and camelCase automatically. - /// - public bool GetBool(string key, bool defaultValue = false) - { - return ParamCoercion.CoerceBool(GetToken(key), defaultValue); - } - - /// - /// Get optional float parameter. - /// - public float? GetFloat(string key, float? defaultValue = null) - { - var str = GetString(key); - if (string.IsNullOrEmpty(str)) return defaultValue; - return float.TryParse(str, out var result) ? result : defaultValue; - } - - /// - /// Check if parameter exists (even if null). - /// Supports both snake_case and camelCase automatically. - /// - public bool Has(string key) - { - return GetToken(key) != null; - } - - /// - /// Get raw JToken for complex types. - /// Supports both snake_case and camelCase automatically. - /// - public JToken GetRaw(string key) - { - return GetToken(key); - } - - /// - /// Get raw JToken with snake_case/camelCase fallback. - /// - private JToken GetToken(string key) - { - // Try exact match first - var token = _params[key]; - if (token != null) return token; - - // Try snake_case if camelCase was provided - var snakeKey = ToSnakeCase(key); - if (snakeKey != key) - { - token = _params[snakeKey]; - if (token != null) return token; - } - - // Try camelCase if snake_case was provided - var camelKey = ToCamelCase(key); - if (camelKey != key) - { - token = _params[camelKey]; - } - - return token; - } - - private string GetString(string key) - { - // Try exact match first - var value = _params[key]?.ToString(); - if (value != null) return value; - - // Try snake_case if camelCase was provided - var snakeKey = ToSnakeCase(key); - if (snakeKey != key) - { - value = _params[snakeKey]?.ToString(); - if (value != null) return value; - } - - // Try camelCase if snake_case was provided - var camelKey = ToCamelCase(key); - if (camelKey != key) - { - value = _params[camelKey]?.ToString(); - } - - return value; - } - - private static string ToSnakeCase(string str) => StringCaseUtility.ToSnakeCase(str); - - private static string ToCamelCase(string str) => StringCaseUtility.ToCamelCase(str); - } - - /// - /// Result type for operations that can fail with an error message. - /// - public class Result - { - public bool IsSuccess { get; } - public T Value { get; } - public string ErrorMessage { get; } - - private Result(bool isSuccess, T value, string errorMessage) - { - IsSuccess = isSuccess; - Value = value; - ErrorMessage = errorMessage; - } - - public static Result Success(T value) => new Result(true, value, null); - public static Result Error(string errorMessage) => new Result(false, default, errorMessage); - - /// - /// Get value or return ErrorResponse. - /// - public object GetOrError(out T value) - { - if (IsSuccess) - { - value = Value; - return null; - } - value = default; - return new ErrorResponse(ErrorMessage); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Helpers/ToolParams.cs.meta b/Assets/MCPForUnity/Editor/Helpers/ToolParams.cs.meta deleted file mode 100644 index 4a13d15..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/ToolParams.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 404b09ea3e2714e1babd16f5705ac788 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/ToolParams.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/UnityJsonSerializer.cs b/Assets/MCPForUnity/Editor/Helpers/UnityJsonSerializer.cs deleted file mode 100644 index 83b650f..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/UnityJsonSerializer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using MCPForUnity.Runtime.Serialization; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Shared JsonSerializer with Unity type converters. - /// Extracted from ManageGameObject to eliminate cross-tool dependencies. - /// - public static class UnityJsonSerializer - { - /// - /// Shared JsonSerializer instance with converters for Unity types. - /// Use this for all JToken-to-Unity-type conversions. - /// - public static readonly JsonSerializer Instance = JsonSerializer.Create(new JsonSerializerSettings - { - Converters = new List - { - new Vector2Converter(), - new Vector3Converter(), - new Vector4Converter(), - new QuaternionConverter(), - new ColorConverter(), - new RectConverter(), - new BoundsConverter(), - new UnityEngineObjectConverter() - } - }); - } -} - diff --git a/Assets/MCPForUnity/Editor/Helpers/UnityJsonSerializer.cs.meta b/Assets/MCPForUnity/Editor/Helpers/UnityJsonSerializer.cs.meta deleted file mode 100644 index 48c23f4..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/UnityJsonSerializer.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 24d94c9c030bd4ff1ab208c748f26b01 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/UnityJsonSerializer.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs b/Assets/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs deleted file mode 100644 index feb0b5c..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; -#if UNITY_EDITOR -using UnityEditor; -using UnityEditor.Compilation; -#endif - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Unified type resolution for Unity types (Components, ScriptableObjects, etc.). - /// Extracted from ComponentResolver in ManageGameObject and ResolveType in ManageScriptableObject. - /// Features: caching, prioritizes Player assemblies over Editor assemblies, uses TypeCache. - /// - public static class UnityTypeResolver - { - private static readonly Dictionary CacheByFqn = new(StringComparer.Ordinal); - private static readonly Dictionary CacheByName = new(StringComparer.Ordinal); - - /// - /// Resolves a type by name, with optional base type constraint. - /// Caches results for performance. Prefers runtime assemblies over Editor assemblies. - /// - /// The short name or fully-qualified name of the type - /// The resolved type, or null if not found - /// Error message if resolution failed - /// Optional base type constraint (e.g., typeof(Component)) - /// True if type was resolved successfully - public static bool TryResolve(string typeName, out Type type, out string error, Type requiredBaseType = null) - { - error = string.Empty; - type = null; - - if (string.IsNullOrWhiteSpace(typeName)) - { - error = "Type name cannot be null or empty"; - return false; - } - - // Check caches - if (CacheByFqn.TryGetValue(typeName, out type) && PassesConstraint(type, requiredBaseType)) - return true; - if (!typeName.Contains(".") && CacheByName.TryGetValue(typeName, out type) && PassesConstraint(type, requiredBaseType)) - return true; - - // Try direct Type.GetType - type = Type.GetType(typeName, throwOnError: false); - if (type != null && PassesConstraint(type, requiredBaseType)) - { - Cache(type); - return true; - } - - // Search loaded assemblies (prefer Player assemblies) - var candidates = FindCandidates(typeName, requiredBaseType); - if (candidates.Count == 1) - { - type = candidates[0]; - Cache(type); - return true; - } - if (candidates.Count > 1) - { - error = FormatAmbiguityError(typeName, candidates); - type = null; - return false; - } - -#if UNITY_EDITOR - // Last resort: TypeCache (fast index) - if (requiredBaseType != null) - { - var tc = TypeCache.GetTypesDerivedFrom(requiredBaseType) - .Where(t => NamesMatch(t, typeName)); - candidates = PreferPlayer(tc).ToList(); - if (candidates.Count == 1) - { - type = candidates[0]; - Cache(type); - return true; - } - if (candidates.Count > 1) - { - error = FormatAmbiguityError(typeName, candidates); - type = null; - return false; - } - } -#endif - - error = $"Type '{typeName}' not found in loaded runtime assemblies. " + - "Use a fully-qualified name (Namespace.TypeName) and ensure the script compiled."; - type = null; - return false; - } - - /// - /// Convenience method to resolve a Component type. - /// - public static Type ResolveComponent(string typeName) - { - if (TryResolve(typeName, out Type type, out _, typeof(Component))) - return type; - return null; - } - - /// - /// Convenience method to resolve a ScriptableObject type. - /// - public static Type ResolveScriptableObject(string typeName) - { - if (TryResolve(typeName, out Type type, out _, typeof(ScriptableObject))) - return type; - return null; - } - - /// - /// Convenience method to resolve any type without constraints. - /// - public static Type ResolveAny(string typeName) - { - if (TryResolve(typeName, out Type type, out _, null)) - return type; - return null; - } - - // --- Private Helpers --- - - private static bool PassesConstraint(Type type, Type requiredBaseType) - { - if (type == null) return false; - if (requiredBaseType == null) return true; - return requiredBaseType.IsAssignableFrom(type); - } - - private static bool NamesMatch(Type t, string query) => - t.Name.Equals(query, StringComparison.Ordinal) || - (t.FullName?.Equals(query, StringComparison.Ordinal) ?? false); - - private static void Cache(Type t) - { - if (t == null) return; - if (t.FullName != null) CacheByFqn[t.FullName] = t; - CacheByName[t.Name] = t; - } - - private static List FindCandidates(string query, Type requiredBaseType) - { - bool isShort = !query.Contains('.'); - var loaded = AppDomain.CurrentDomain.GetAssemblies(); - -#if UNITY_EDITOR - // Names of Player (runtime) script assemblies - var playerAsmNames = new HashSet( - CompilationPipeline.GetAssemblies(AssembliesType.Player).Select(a => a.name), - StringComparer.Ordinal); - - var playerAsms = loaded.Where(a => playerAsmNames.Contains(a.GetName().Name)); - var editorAsms = loaded.Except(playerAsms); -#else - var playerAsms = loaded; - var editorAsms = Array.Empty(); -#endif - - Func match = isShort - ? (t => t.Name.Equals(query, StringComparison.Ordinal)) - : (t => t.FullName?.Equals(query, StringComparison.Ordinal) ?? false); - - var fromPlayer = playerAsms.SelectMany(SafeGetTypes) - .Where(t => PassesConstraint(t, requiredBaseType)) - .Where(match); - var fromEditor = editorAsms.SelectMany(SafeGetTypes) - .Where(t => PassesConstraint(t, requiredBaseType)) - .Where(match); - - // Prefer Player over Editor - var candidates = fromPlayer.ToList(); - if (candidates.Count == 0) - candidates = fromEditor.ToList(); - - return candidates; - } - - private static IEnumerable SafeGetTypes(System.Reflection.Assembly assembly) - { - try { return assembly.GetTypes(); } - catch (ReflectionTypeLoadException rtle) { return rtle.Types.Where(t => t != null); } - catch { return Enumerable.Empty(); } - } - - private static IEnumerable PreferPlayer(IEnumerable types) - { -#if UNITY_EDITOR - var playerAsmNames = new HashSet( - CompilationPipeline.GetAssemblies(AssembliesType.Player).Select(a => a.name), - StringComparer.Ordinal); - - var list = types.ToList(); - var fromPlayer = list.Where(t => playerAsmNames.Contains(t.Assembly.GetName().Name)).ToList(); - return fromPlayer.Count > 0 ? fromPlayer : list; -#else - return types; -#endif - } - - private static string FormatAmbiguityError(string query, List candidates) - { - var names = string.Join(", ", candidates.Take(5).Select(t => t.FullName)); - if (candidates.Count > 5) names += $" ... ({candidates.Count - 5} more)"; - return $"Ambiguous type reference '{query}'. Found {candidates.Count} matches: [{names}]. Use a fully-qualified name."; - } - } -} - diff --git a/Assets/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs.meta b/Assets/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs.meta deleted file mode 100644 index 87ca45e..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 2cdf06f869b124741af31f27b25742db -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/UnityTypeResolver.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Helpers/VectorParsing.cs b/Assets/MCPForUnity/Editor/Helpers/VectorParsing.cs deleted file mode 100644 index 0e81cca..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/VectorParsing.cs +++ /dev/null @@ -1,731 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; -using UnityEngine; - -namespace MCPForUnity.Editor.Helpers -{ - /// - /// Utility class for parsing JSON tokens into Unity vector, math, and animation types. - /// Supports both array format [x, y, z] and object format {x: 1, y: 2, z: 3}. - /// - public static class VectorParsing - { - /// - /// Parses a JToken (array or object) into a Vector3. - /// - /// The JSON token to parse - /// The parsed Vector3 or null if parsing fails - public static Vector3? ParseVector3(JToken token) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - // Array format: [x, y, z] - if (token is JArray array && array.Count >= 3) - { - return new Vector3( - array[0].ToObject(), - array[1].ToObject(), - array[2].ToObject() - ); - } - - // Object format: {x: 1, y: 2, z: 3} - if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("z")) - { - return new Vector3( - obj["x"].ToObject(), - obj["y"].ToObject(), - obj["z"].ToObject() - ); - } - } - catch (Exception ex) - { - McpLog.Warn($"[VectorParsing] Failed to parse Vector3 from '{token}': {ex.Message}"); - } - - return null; - } - - /// - /// Parses a JToken into a Vector3, returning a default value if parsing fails. - /// - public static Vector3 ParseVector3OrDefault(JToken token, Vector3 defaultValue = default) - { - return ParseVector3(token) ?? defaultValue; - } - - /// - /// Parses a JToken (array or object) into a Vector2. - /// - /// The JSON token to parse - /// The parsed Vector2 or null if parsing fails - public static Vector2? ParseVector2(JToken token) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - // Array format: [x, y] - if (token is JArray array && array.Count >= 2) - { - return new Vector2( - array[0].ToObject(), - array[1].ToObject() - ); - } - - // Object format: {x: 1, y: 2} - if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y")) - { - return new Vector2( - obj["x"].ToObject(), - obj["y"].ToObject() - ); - } - } - catch (Exception ex) - { - McpLog.Warn($"[VectorParsing] Failed to parse Vector2 from '{token}': {ex.Message}"); - } - - return null; - } - - /// - /// Parses a JToken (array or object) into a Vector4. - /// - /// The JSON token to parse - /// The parsed Vector4 or null if parsing fails - public static Vector4? ParseVector4(JToken token) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - // Array format: [x, y, z, w] - if (token is JArray array && array.Count >= 4) - { - return new Vector4( - array[0].ToObject(), - array[1].ToObject(), - array[2].ToObject(), - array[3].ToObject() - ); - } - - // Object format: {x: 1, y: 2, z: 3, w: 4} - if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") && - obj.ContainsKey("z") && obj.ContainsKey("w")) - { - return new Vector4( - obj["x"].ToObject(), - obj["y"].ToObject(), - obj["z"].ToObject(), - obj["w"].ToObject() - ); - } - } - catch (Exception ex) - { - Debug.LogWarning($"[VectorParsing] Failed to parse Vector4 from '{token}': {ex.Message}"); - } - - return null; - } - - /// - /// Parses a JToken (array or object) into a Quaternion. - /// Supports both euler angles [x, y, z] and quaternion components [x, y, z, w]. - /// Note: Raw quaternion components are NOT normalized. Callers should normalize if needed - /// for operations like interpolation where non-unit quaternions cause issues. - /// - /// The JSON token to parse - /// If true, treats 3-element arrays as euler angles - /// The parsed Quaternion or null if parsing fails - public static Quaternion? ParseQuaternion(JToken token, bool asEulerAngles = true) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - if (token is JArray array) - { - // Quaternion components: [x, y, z, w] - if (array.Count >= 4) - { - return new Quaternion( - array[0].ToObject(), - array[1].ToObject(), - array[2].ToObject(), - array[3].ToObject() - ); - } - - // Euler angles: [x, y, z] - if (array.Count >= 3 && asEulerAngles) - { - return Quaternion.Euler( - array[0].ToObject(), - array[1].ToObject(), - array[2].ToObject() - ); - } - } - - // Object format: {x: 0, y: 0, z: 0, w: 1} - if (token is JObject obj) - { - if (obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("z") && obj.ContainsKey("w")) - { - return new Quaternion( - obj["x"].ToObject(), - obj["y"].ToObject(), - obj["z"].ToObject(), - obj["w"].ToObject() - ); - } - - // Euler format in object: {x: 45, y: 90, z: 0} (as euler angles) - if (obj.ContainsKey("x") && obj.ContainsKey("y") && obj.ContainsKey("z") && asEulerAngles) - { - return Quaternion.Euler( - obj["x"].ToObject(), - obj["y"].ToObject(), - obj["z"].ToObject() - ); - } - } - } - catch (Exception ex) - { - McpLog.Warn($"[VectorParsing] Failed to parse Quaternion from '{token}': {ex.Message}"); - } - - return null; - } - - /// - /// Parses a JToken (array or object) into a Color. - /// Supports both [r, g, b, a] and {r: 1, g: 1, b: 1, a: 1} formats. - /// - /// The JSON token to parse - /// The parsed Color or null if parsing fails - public static Color? ParseColor(JToken token) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - // Array format: [r, g, b, a] or [r, g, b] - if (token is JArray array) - { - if (array.Count >= 4) - { - return new Color( - array[0].ToObject(), - array[1].ToObject(), - array[2].ToObject(), - array[3].ToObject() - ); - } - if (array.Count >= 3) - { - return new Color( - array[0].ToObject(), - array[1].ToObject(), - array[2].ToObject(), - 1f // Default alpha - ); - } - } - - // Object format: {r: 1, g: 1, b: 1, a: 1} - if (token is JObject obj && obj.ContainsKey("r") && obj.ContainsKey("g") && obj.ContainsKey("b")) - { - float a = obj.ContainsKey("a") ? obj["a"].ToObject() : 1f; - return new Color( - obj["r"].ToObject(), - obj["g"].ToObject(), - obj["b"].ToObject(), - a - ); - } - } - catch (Exception ex) - { - McpLog.Warn($"[VectorParsing] Failed to parse Color from '{token}': {ex.Message}"); - } - - return null; - } - - /// - /// Parses a JToken into a Color, returning Color.white if parsing fails and no default is specified. - /// - public static Color ParseColorOrDefault(JToken token) => ParseColor(token) ?? Color.white; - - /// - /// Parses a JToken into a Color, returning the specified default if parsing fails. - /// - public static Color ParseColorOrDefault(JToken token, Color defaultValue) => ParseColor(token) ?? defaultValue; - - /// - /// Parses a JToken into a Vector4, returning a default value if parsing fails. - /// Added for ManageVFX refactoring. - /// - public static Vector4 ParseVector4OrDefault(JToken token, Vector4 defaultValue = default) - { - return ParseVector4(token) ?? defaultValue; - } - - /// - /// Parses a JToken into a Gradient. - /// Supports formats: - /// - Simple: {startColor: [r,g,b,a], endColor: [r,g,b,a]} - /// - Full: {colorKeys: [{color: [r,g,b,a], time: 0.0}, ...], alphaKeys: [{alpha: 1.0, time: 0.0}, ...]} - /// Added for ManageVFX refactoring. - /// - /// The JSON token to parse - /// The parsed Gradient or null if parsing fails - public static Gradient ParseGradient(JToken token) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - Gradient gradient = new Gradient(); - - if (token is JObject obj) - { - // Simple format: {startColor: ..., endColor: ...} - if (obj.ContainsKey("startColor")) - { - Color startColor = ParseColorOrDefault(obj["startColor"]); - Color endColor = ParseColorOrDefault(obj["endColor"] ?? obj["startColor"]); - float startAlpha = obj["startAlpha"]?.ToObject() ?? startColor.a; - float endAlpha = obj["endAlpha"]?.ToObject() ?? endColor.a; - - gradient.SetKeys( - new GradientColorKey[] { new GradientColorKey(startColor, 0f), new GradientColorKey(endColor, 1f) }, - new GradientAlphaKey[] { new GradientAlphaKey(startAlpha, 0f), new GradientAlphaKey(endAlpha, 1f) } - ); - return gradient; - } - - // Full format: {colorKeys: [...], alphaKeys: [...]} - var colorKeys = new List(); - var alphaKeys = new List(); - - if (obj["colorKeys"] is JArray colorKeysArr) - { - foreach (var key in colorKeysArr) - { - Color color = ParseColorOrDefault(key["color"]); - float time = key["time"]?.ToObject() ?? 0f; - colorKeys.Add(new GradientColorKey(color, time)); - } - } - - if (obj["alphaKeys"] is JArray alphaKeysArr) - { - foreach (var key in alphaKeysArr) - { - float alpha = key["alpha"]?.ToObject() ?? 1f; - float time = key["time"]?.ToObject() ?? 0f; - alphaKeys.Add(new GradientAlphaKey(alpha, time)); - } - } - - // Ensure at least 2 keys - if (colorKeys.Count == 0) - { - colorKeys.Add(new GradientColorKey(Color.white, 0f)); - colorKeys.Add(new GradientColorKey(Color.white, 1f)); - } - - if (alphaKeys.Count == 0) - { - alphaKeys.Add(new GradientAlphaKey(1f, 0f)); - alphaKeys.Add(new GradientAlphaKey(1f, 1f)); - } - - gradient.SetKeys(colorKeys.ToArray(), alphaKeys.ToArray()); - return gradient; - } - } - catch (Exception ex) - { - McpLog.Warn($"[VectorParsing] Failed to parse Gradient from '{token}': {ex.Message}"); - } - - return null; - } - - /// - /// Parses a JToken into a Gradient, returning a default gradient if parsing fails. - /// Added for ManageVFX refactoring. - /// - public static Gradient ParseGradientOrDefault(JToken token) - { - var result = ParseGradient(token); - if (result != null) return result; - - // Return default white gradient - var gradient = new Gradient(); - gradient.SetKeys( - new GradientColorKey[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, - new GradientAlphaKey[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) } - ); - return gradient; - } - - /// - /// Parses a JToken into an AnimationCurve. - /// - /// Supported formats: - /// - /// Constant: 1.0 (number) - Creates constant curve at that value - /// Simple: {start: 0.0, end: 1.0} or {startValue: 0.0, endValue: 1.0} - /// Full: {keys: [{time: 0, value: 1, inTangent: 0, outTangent: 0}, ...]} - /// - /// - /// Keyframe field defaults (for Full format): - /// - /// time (float): Default: 0 - /// value (float): Default: 1 (note: differs from ManageScriptableObject which uses 0) - /// inTangent (float): Default: 0 - /// outTangent (float): Default: 0 - /// - /// - /// Note: This method is used by ManageVFX. For ScriptableObject patching, - /// see which has slightly different defaults. - /// - /// The JSON token to parse - /// The parsed AnimationCurve or null if parsing fails - public static AnimationCurve ParseAnimationCurve(JToken token) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - // Constant value: just a number - if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer) - { - return AnimationCurve.Constant(0f, 1f, token.ToObject()); - } - - if (token is JObject obj) - { - // Full format: {keys: [...]} - if (obj["keys"] is JArray keys) - { - AnimationCurve curve = new AnimationCurve(); - foreach (var key in keys) - { - float time = key["time"]?.ToObject() ?? 0f; - float value = key["value"]?.ToObject() ?? 1f; - float inTangent = key["inTangent"]?.ToObject() ?? 0f; - float outTangent = key["outTangent"]?.ToObject() ?? 0f; - curve.AddKey(new Keyframe(time, value, inTangent, outTangent)); - } - return curve; - } - - // Simple format: {start: 0.0, end: 1.0} or {startValue: 0.0, endValue: 1.0} - if (obj.ContainsKey("start") || obj.ContainsKey("startValue") || obj.ContainsKey("end") || obj.ContainsKey("endValue")) - { - float startValue = obj["start"]?.ToObject() ?? obj["startValue"]?.ToObject() ?? 1f; - float endValue = obj["end"]?.ToObject() ?? obj["endValue"]?.ToObject() ?? 1f; - AnimationCurve curve = new AnimationCurve(); - curve.AddKey(0f, startValue); - curve.AddKey(1f, endValue); - return curve; - } - } - } - catch (Exception ex) - { - McpLog.Warn($"[VectorParsing] Failed to parse AnimationCurve from '{token}': {ex.Message}"); - } - - return null; - } - - /// - /// Parses a JToken into an AnimationCurve, returning a constant curve if parsing fails. - /// Added for ManageVFX refactoring. - /// - /// The JSON token to parse - /// The constant value for the default curve - public static AnimationCurve ParseAnimationCurveOrDefault(JToken token, float defaultValue = 1f) - { - return ParseAnimationCurve(token) ?? AnimationCurve.Constant(0f, 1f, defaultValue); - } - - /// - /// Validates AnimationCurve JSON format without parsing it. - /// Used by dry-run validation to provide early feedback on format errors. - /// - /// Validated formats: - /// - /// Wrapped: { "keys": [ { "time": 0, "value": 1.0 }, ... ] } - /// Direct array: [ { "time": 0, "value": 1.0 }, ... ] - /// Null/empty: Valid (will set empty curve) - /// - /// - /// The JSON value to validate - /// Output message describing validation result or error - /// True if format is valid, false otherwise - public static bool ValidateAnimationCurveFormat(JToken valueToken, out string message) - { - message = null; - - if (valueToken == null || valueToken.Type == JTokenType.Null) - { - message = "Value format valid (will set empty curve)."; - return true; - } - - JArray keysArray = null; - - if (valueToken is JObject curveObj) - { - keysArray = curveObj["keys"] as JArray; - if (keysArray == null) - { - message = "AnimationCurve object requires 'keys' array. Expected: { \"keys\": [ { \"time\": 0, \"value\": 0 }, ... ] }"; - return false; - } - } - else if (valueToken is JArray directArray) - { - keysArray = directArray; - } - else - { - message = "AnimationCurve requires object with 'keys' or array of keyframes. " + - "Expected: { \"keys\": [ { \"time\": 0, \"value\": 0, \"inSlope\": 0, \"outSlope\": 0 }, ... ] }"; - return false; - } - - // Validate each keyframe - for (int i = 0; i < keysArray.Count; i++) - { - var keyToken = keysArray[i]; - if (keyToken is not JObject keyObj) - { - message = $"Keyframe at index {i} must be an object with 'time' and 'value'."; - return false; - } - - // Validate numeric fields if present - string[] numericFields = { "time", "value", "inSlope", "outSlope", "inTangent", "outTangent", "inWeight", "outWeight" }; - foreach (var field in numericFields) - { - if (!ParamCoercion.ValidateNumericField(keyObj, field, out var fieldError)) - { - message = $"Keyframe[{i}].{field}: {fieldError}"; - return false; - } - } - - if (!ParamCoercion.ValidateIntegerField(keyObj, "weightedMode", out var weightedModeError)) - { - message = $"Keyframe[{i}].weightedMode: {weightedModeError}"; - return false; - } - } - - message = $"Value format valid (AnimationCurve with {keysArray.Count} keyframes). " + - "Note: Missing keyframe fields default to 0 (time, value, inSlope, outSlope, inWeight, outWeight)."; - return true; - } - - /// - /// Validates Quaternion JSON format without parsing it. - /// Used by dry-run validation to provide early feedback on format errors. - /// - /// Validated formats: - /// - /// Euler array: [x, y, z] - 3 numeric elements - /// Raw quaternion: [x, y, z, w] - 4 numeric elements - /// Object: { "x": 0, "y": 0, "z": 0, "w": 1 } - /// Explicit euler: { "euler": [x, y, z] } - /// Null/empty: Valid (will set identity) - /// - /// - /// The JSON value to validate - /// Output message describing validation result or error - /// True if format is valid, false otherwise - public static bool ValidateQuaternionFormat(JToken valueToken, out string message) - { - message = null; - - if (valueToken == null || valueToken.Type == JTokenType.Null) - { - message = "Value format valid (will set identity quaternion)."; - return true; - } - - if (valueToken is JArray arr) - { - if (arr.Count == 3) - { - // Validate Euler angles [x, y, z] - for (int i = 0; i < 3; i++) - { - if (!ParamCoercion.IsNumericToken(arr[i])) - { - message = $"Euler angle at index {i} must be a number."; - return false; - } - } - message = "Value format valid (Quaternion from Euler angles [x, y, z])."; - return true; - } - else if (arr.Count == 4) - { - // Validate raw quaternion [x, y, z, w] - for (int i = 0; i < 4; i++) - { - if (!ParamCoercion.IsNumericToken(arr[i])) - { - message = $"Quaternion component at index {i} must be a number."; - return false; - } - } - message = "Value format valid (Quaternion from [x, y, z, w])."; - return true; - } - else - { - message = "Quaternion array must have 3 elements (Euler angles) or 4 elements (x, y, z, w)."; - return false; - } - } - else if (valueToken is JObject obj) - { - // Check for explicit euler property - if (obj["euler"] is JArray eulerArr) - { - if (eulerArr.Count != 3) - { - message = "Quaternion euler array must have exactly 3 elements [x, y, z]."; - return false; - } - for (int i = 0; i < 3; i++) - { - if (!ParamCoercion.IsNumericToken(eulerArr[i])) - { - message = $"Euler angle at index {i} must be a number."; - return false; - } - } - message = "Value format valid (Quaternion from { euler: [x, y, z] })."; - return true; - } - - // Object format { x, y, z, w } - if (obj["x"] != null && obj["y"] != null && obj["z"] != null && obj["w"] != null) - { - if (!ParamCoercion.IsNumericToken(obj["x"]) || !ParamCoercion.IsNumericToken(obj["y"]) || - !ParamCoercion.IsNumericToken(obj["z"]) || !ParamCoercion.IsNumericToken(obj["w"])) - { - message = "Quaternion { x, y, z, w } fields must all be numbers."; - return false; - } - message = "Value format valid (Quaternion from { x, y, z, w })."; - return true; - } - - message = "Quaternion object must have { x, y, z, w } or { euler: [x, y, z] }."; - return false; - } - else - { - message = "Quaternion requires array [x,y,z] (Euler), [x,y,z,w] (raw), or object { x, y, z, w }."; - return false; - } - } - - /// - /// Parses a JToken into a Rect. - /// Supports {x, y, width, height} format. - /// - public static Rect? ParseRect(JToken token) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - if (token is JObject obj && - obj.ContainsKey("x") && obj.ContainsKey("y") && - obj.ContainsKey("width") && obj.ContainsKey("height")) - { - return new Rect( - obj["x"].ToObject(), - obj["y"].ToObject(), - obj["width"].ToObject(), - obj["height"].ToObject() - ); - } - - // Array format: [x, y, width, height] - if (token is JArray array && array.Count >= 4) - { - return new Rect( - array[0].ToObject(), - array[1].ToObject(), - array[2].ToObject(), - array[3].ToObject() - ); - } - } - catch (Exception ex) - { - McpLog.Warn($"[VectorParsing] Failed to parse Rect from '{token}': {ex.Message}"); - } - - return null; - } - - /// - /// Parses a JToken into a Bounds. - /// Supports {center: {x,y,z}, size: {x,y,z}} format. - /// - public static Bounds? ParseBounds(JToken token) - { - if (token == null || token.Type == JTokenType.Null) - return null; - - try - { - if (token is JObject obj && obj.ContainsKey("center") && obj.ContainsKey("size")) - { - var center = ParseVector3(obj["center"]) ?? Vector3.zero; - var size = ParseVector3(obj["size"]) ?? Vector3.zero; - return new Bounds(center, size); - } - } - catch (Exception ex) - { - McpLog.Warn($"[VectorParsing] Failed to parse Bounds from '{token}': {ex.Message}"); - } - - return null; - } - } -} - diff --git a/Assets/MCPForUnity/Editor/Helpers/VectorParsing.cs.meta b/Assets/MCPForUnity/Editor/Helpers/VectorParsing.cs.meta deleted file mode 100644 index cd89f27..0000000 --- a/Assets/MCPForUnity/Editor/Helpers/VectorParsing.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: ca2205caede3744aebda9f6da2fa2c22 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Helpers/VectorParsing.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/MCPForUnity.Editor.asmdef b/Assets/MCPForUnity/Editor/MCPForUnity.Editor.asmdef deleted file mode 100644 index 9685029..0000000 --- a/Assets/MCPForUnity/Editor/MCPForUnity.Editor.asmdef +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "MCPForUnity.Editor", - "rootNamespace": "MCPForUnity.Editor", - "references": [ - "MCPForUnity.Runtime", - "Newtonsoft.Json" - ], - "includePlatforms": [ - "Editor" - ], - "excludePlatforms": [], - "overrideReferences": false, - "precompiledReferences": [ - "Newtonsoft.Json.dll" - ], - "autoReferenced": true, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false -} \ No newline at end of file diff --git a/Assets/MCPForUnity/Editor/MCPForUnity.Editor.asmdef.meta b/Assets/MCPForUnity/Editor/MCPForUnity.Editor.asmdef.meta deleted file mode 100644 index 4004b88..0000000 --- a/Assets/MCPForUnity/Editor/MCPForUnity.Editor.asmdef.meta +++ /dev/null @@ -1,14 +0,0 @@ -fileFormatVersion: 2 -guid: 98f702da6ca044be59a864a9419c4eab -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/MCPForUnity.Editor.asmdef - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/McpCiBoot.cs b/Assets/MCPForUnity/Editor/McpCiBoot.cs deleted file mode 100644 index c8e8c19..0000000 --- a/Assets/MCPForUnity/Editor/McpCiBoot.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Services.Transport.Transports; -using UnityEditor; - -namespace MCPForUnity.Editor -{ - public static class McpCiBoot - { - public static void StartStdioForCi() - { - try - { - EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false); - } - catch { /* ignore */ } - - StdioBridgeHost.StartAutoConnect(); - } - } -} diff --git a/Assets/MCPForUnity/Editor/McpCiBoot.cs.meta b/Assets/MCPForUnity/Editor/McpCiBoot.cs.meta deleted file mode 100644 index c1353f3..0000000 --- a/Assets/MCPForUnity/Editor/McpCiBoot.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: ef9dca277ab34ba1b136d8dcd45de948 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/McpCiBoot.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/MenuItems.meta b/Assets/MCPForUnity/Editor/MenuItems.meta deleted file mode 100644 index ad5fb5e..0000000 --- a/Assets/MCPForUnity/Editor/MenuItems.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 9e7f37616736f4d3cbd8bdbc626f5ab9 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs b/Assets/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs deleted file mode 100644 index c280d95..0000000 --- a/Assets/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs +++ /dev/null @@ -1,36 +0,0 @@ -using MCPForUnity.Editor.Setup; -using MCPForUnity.Editor.Windows; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.MenuItems -{ - public static class MCPForUnityMenu - { - [MenuItem("Window/MCP For Unity/Toggle MCP Window %#m", priority = 1)] - public static void ToggleMCPWindow() - { - if (MCPForUnityEditorWindow.HasAnyOpenWindow()) - { - MCPForUnityEditorWindow.CloseAllOpenWindows(); - } - else - { - MCPForUnityEditorWindow.ShowWindow(); - } - } - - [MenuItem("Window/MCP For Unity/Local Setup Window", priority = 2)] - public static void ShowSetupWindow() - { - SetupWindowService.ShowSetupWindow(); - } - - - [MenuItem("Window/MCP For Unity/Edit EditorPrefs", priority = 3)] - public static void ShowEditorPrefsWindow() - { - EditorPrefsWindow.ShowWindow(); - } - } -} diff --git a/Assets/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs.meta b/Assets/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs.meta deleted file mode 100644 index 7a8a06a..0000000 --- a/Assets/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 42b27c415aa084fe6a9cc6cf03979d36 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/MenuItems/MCPForUnityMenu.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Migrations.meta b/Assets/MCPForUnity/Editor/Migrations.meta deleted file mode 100644 index 62d67f0..0000000 --- a/Assets/MCPForUnity/Editor/Migrations.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 8bb6a578d4df4e2daa0bd1aa1fa492d5 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Migrations/LegacyServerSrcMigration.cs b/Assets/MCPForUnity/Editor/Migrations/LegacyServerSrcMigration.cs deleted file mode 100644 index 4fbeac8..0000000 --- a/Assets/MCPForUnity/Editor/Migrations/LegacyServerSrcMigration.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Migrations -{ - /// - /// Detects legacy embedded-server preferences and migrates configs to the new uvx/stdio path once. - /// - [InitializeOnLoad] - internal static class LegacyServerSrcMigration - { - private const string ServerSrcKey = EditorPrefKeys.ServerSrc; - private const string UseEmbeddedKey = EditorPrefKeys.UseEmbeddedServer; - - static LegacyServerSrcMigration() - { - if (Application.isBatchMode) - return; - - EditorApplication.delayCall += RunMigrationIfNeeded; - } - - private static void RunMigrationIfNeeded() - { - EditorApplication.delayCall -= RunMigrationIfNeeded; - - bool hasServerSrc = EditorPrefs.HasKey(ServerSrcKey); - bool hasUseEmbedded = EditorPrefs.HasKey(UseEmbeddedKey); - - if (!hasServerSrc && !hasUseEmbedded) - { - return; - } - - try - { - McpLog.Info("Detected legacy embedded MCP server configuration. Updating all client configs..."); - - var summary = MCPServiceLocator.Client.ConfigureAllDetectedClients(); - - if (summary.FailureCount > 0) - { - McpLog.Warn($"Legacy configuration migration finished with errors ({summary.GetSummaryMessage()}). details:"); - if (summary.Messages != null) - { - foreach (var message in summary.Messages) - { - McpLog.Warn($" {message}"); - } - } - McpLog.Warn("Legacy keys will be removed to prevent migration loop. Please configure failing clients manually."); - } - else - { - McpLog.Info($"Legacy configuration migration complete ({summary.GetSummaryMessage()})"); - } - - if (hasServerSrc) - { - EditorPrefs.DeleteKey(ServerSrcKey); - McpLog.Info(" ✓ Removed legacy key: MCPForUnity.ServerSrc"); - } - - if (hasUseEmbedded) - { - EditorPrefs.DeleteKey(UseEmbeddedKey); - McpLog.Info(" ✓ Removed legacy key: MCPForUnity.UseEmbeddedServer"); - } - } - catch (Exception ex) - { - McpLog.Error($"Legacy MCP server migration failed: {ex.Message}"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Migrations/LegacyServerSrcMigration.cs.meta b/Assets/MCPForUnity/Editor/Migrations/LegacyServerSrcMigration.cs.meta deleted file mode 100644 index fcbd496..0000000 --- a/Assets/MCPForUnity/Editor/Migrations/LegacyServerSrcMigration.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4436b2149abf4b0d8014f81cd29a2bd0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Migrations/LegacyServerSrcMigration.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs b/Assets/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs deleted file mode 100644 index 850e273..0000000 --- a/Assets/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using MCPForUnity.Editor.Clients; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Models; -using MCPForUnity.Editor.Services; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Migrations -{ - /// - /// Keeps stdio MCP clients in sync with the current package version by rewriting their configs when the package updates. - /// - [InitializeOnLoad] - internal static class StdIoVersionMigration - { - private const string LastUpgradeKey = EditorPrefKeys.LastStdIoUpgradeVersion; - - static StdIoVersionMigration() - { - if (Application.isBatchMode) - return; - - EditorApplication.delayCall += RunMigrationIfNeeded; - } - - private static void RunMigrationIfNeeded() - { - EditorApplication.delayCall -= RunMigrationIfNeeded; - - string currentVersion = AssetPathUtility.GetPackageVersion(); - if (string.IsNullOrEmpty(currentVersion) || string.Equals(currentVersion, "unknown", StringComparison.OrdinalIgnoreCase)) - { - return; - } - - string lastUpgradeVersion = string.Empty; - try { lastUpgradeVersion = EditorPrefs.GetString(LastUpgradeKey, string.Empty); } catch { } - - if (string.Equals(lastUpgradeVersion, currentVersion, StringComparison.OrdinalIgnoreCase)) - { - return; // Already refreshed for this package version - } - - bool hadFailures = false; - bool touchedAny = false; - - var configurators = McpClientRegistry.All.OfType().ToList(); - foreach (var configurator in configurators) - { - try - { - if (!configurator.SupportsAutoConfigure) - continue; - - // Handle CLI-based configurators (e.g., Claude Code CLI) - // CheckStatus with attemptAutoRewrite=true will auto-reregister if version mismatch - if (configurator is ClaudeCliMcpConfigurator cliConfigurator) - { - var previousStatus = configurator.Status; - configurator.CheckStatus(attemptAutoRewrite: true); - if (configurator.Status != previousStatus) - { - touchedAny = true; - } - continue; - } - - // Handle JSON file-based configurators - if (!ConfigUsesStdIo(configurator.Client)) - continue; - - MCPServiceLocator.Client.ConfigureClient(configurator); - touchedAny = true; - } - catch (Exception ex) - { - hadFailures = true; - McpLog.Warn($"Failed to refresh stdio config for {configurator.DisplayName}: {ex.Message}"); - } - } - - if (!touchedAny) - { - // Nothing needed refreshing; still record version so we don't rerun every launch - try { EditorPrefs.SetString(LastUpgradeKey, currentVersion); } catch { } - return; - } - - if (hadFailures) - { - McpLog.Warn("Stdio MCP upgrade encountered errors; will retry next session."); - return; - } - - try - { - EditorPrefs.SetString(LastUpgradeKey, currentVersion); - } - catch { } - - McpLog.Info($"Updated stdio MCP configs to package version {currentVersion}."); - } - - private static bool ConfigUsesStdIo(McpClient client) - { - return JsonConfigUsesStdIo(client); - } - - private static bool JsonConfigUsesStdIo(McpClient client) - { - string configPath = McpConfigurationHelper.GetClientConfigPath(client); - if (string.IsNullOrEmpty(configPath) || !File.Exists(configPath)) - { - return false; - } - - try - { - var root = JObject.Parse(File.ReadAllText(configPath)); - - JToken unityNode = null; - if (client.IsVsCodeLayout) - { - unityNode = root.SelectToken("servers.unityMCP") - ?? root.SelectToken("mcp.servers.unityMCP"); - } - else - { - unityNode = root.SelectToken("mcpServers.unityMCP"); - } - - if (unityNode == null) return false; - - return unityNode["command"] != null; - } - catch - { - return false; - } - } - - } -} diff --git a/Assets/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs.meta b/Assets/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs.meta deleted file mode 100644 index f857648..0000000 --- a/Assets/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: f1d589c8c8684e6f919ffb393c4b4db5 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Models.meta b/Assets/MCPForUnity/Editor/Models.meta deleted file mode 100644 index 8540456..0000000 --- a/Assets/MCPForUnity/Editor/Models.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 16d3ab36890b6c14f9afeabee30e03e3 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Models/Command.cs b/Assets/MCPForUnity/Editor/Models/Command.cs deleted file mode 100644 index 02a89d8..0000000 --- a/Assets/MCPForUnity/Editor/Models/Command.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace MCPForUnity.Editor.Models -{ - /// - /// Represents a command received from the MCP client - /// - public class Command - { - /// - /// The type of command to execute - /// - public string type { get; set; } - - /// - /// The parameters for the command - /// - public JObject @params { get; set; } - } -} - diff --git a/Assets/MCPForUnity/Editor/Models/Command.cs.meta b/Assets/MCPForUnity/Editor/Models/Command.cs.meta deleted file mode 100644 index 84ccdfa..0000000 --- a/Assets/MCPForUnity/Editor/Models/Command.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 6754c84e5deb74749bc3a19e0c9aa280 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Models/Command.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Models/MCPConfigServer.cs b/Assets/MCPForUnity/Editor/Models/MCPConfigServer.cs deleted file mode 100644 index 25f1163..0000000 --- a/Assets/MCPForUnity/Editor/Models/MCPConfigServer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace MCPForUnity.Editor.Models -{ - [Serializable] - public class McpConfigServer - { - [JsonProperty("command")] - public string command; - - [JsonProperty("args")] - public string[] args; - - // VSCode expects a transport type; include only when explicitly set - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public string type; - - // URL for HTTP transport mode - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public string url; - } -} diff --git a/Assets/MCPForUnity/Editor/Models/MCPConfigServer.cs.meta b/Assets/MCPForUnity/Editor/Models/MCPConfigServer.cs.meta deleted file mode 100644 index e95749d..0000000 --- a/Assets/MCPForUnity/Editor/Models/MCPConfigServer.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 5fae9d995f514e9498e9613e2cdbeca9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Models/MCPConfigServer.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Models/MCPConfigServers.cs b/Assets/MCPForUnity/Editor/Models/MCPConfigServers.cs deleted file mode 100644 index d5065a1..0000000 --- a/Assets/MCPForUnity/Editor/Models/MCPConfigServers.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace MCPForUnity.Editor.Models -{ - [Serializable] - public class McpConfigServers - { - [JsonProperty("unityMCP")] - public McpConfigServer unityMCP; - } -} diff --git a/Assets/MCPForUnity/Editor/Models/MCPConfigServers.cs.meta b/Assets/MCPForUnity/Editor/Models/MCPConfigServers.cs.meta deleted file mode 100644 index baa574d..0000000 --- a/Assets/MCPForUnity/Editor/Models/MCPConfigServers.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: bcb583553e8173b49be71a5c43bd9502 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Models/MCPConfigServers.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Models/McpClient.cs b/Assets/MCPForUnity/Editor/Models/McpClient.cs deleted file mode 100644 index 832bb8a..0000000 --- a/Assets/MCPForUnity/Editor/Models/McpClient.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; - -namespace MCPForUnity.Editor.Models -{ - public class McpClient - { - public string name; - public string windowsConfigPath; - public string macConfigPath; - public string linuxConfigPath; - public string configStatus; - public McpStatus status = McpStatus.NotConfigured; - public ConfiguredTransport configuredTransport = ConfiguredTransport.Unknown; - - // Capability flags/config for JSON-based configurators - public bool IsVsCodeLayout; // Whether the config file follows VS Code layout (env object at root) - public bool SupportsHttpTransport = true; // Whether the MCP server supports HTTP transport - public bool EnsureEnvObject; // Whether to ensure the env object is present in the config - public bool StripEnvWhenNotRequired; // Whether to strip the env object when not required - public string HttpUrlProperty = "url"; // The property name for the HTTP URL in the config - public Dictionary DefaultUnityFields = new(); - - // Helper method to convert the enum to a display string - public string GetStatusDisplayString() - { - return status switch - { - McpStatus.NotConfigured => "Not Configured", - McpStatus.Configured => "Configured", - McpStatus.Running => "Running", - McpStatus.Connected => "Connected", - McpStatus.IncorrectPath => "Incorrect Path", - McpStatus.CommunicationError => "Communication Error", - McpStatus.NoResponse => "No Response", - McpStatus.UnsupportedOS => "Unsupported OS", - McpStatus.MissingConfig => "Missing MCPForUnity Config", - McpStatus.Error => configStatus?.StartsWith("Error:") == true ? configStatus : "Error", - _ => "Unknown", - }; - } - - // Helper method to set both status enum and string for backward compatibility - public void SetStatus(McpStatus newStatus, string errorDetails = null) - { - status = newStatus; - - if (newStatus == McpStatus.Error && !string.IsNullOrEmpty(errorDetails)) - { - configStatus = $"Error: {errorDetails}"; - } - else - { - configStatus = GetStatusDisplayString(); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Models/McpClient.cs.meta b/Assets/MCPForUnity/Editor/Models/McpClient.cs.meta deleted file mode 100644 index 46ab969..0000000 --- a/Assets/MCPForUnity/Editor/Models/McpClient.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b1afa56984aec0d41808edcebf805e6a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Models/McpClient.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Models/McpConfig.cs b/Assets/MCPForUnity/Editor/Models/McpConfig.cs deleted file mode 100644 index 9ddf9d0..0000000 --- a/Assets/MCPForUnity/Editor/Models/McpConfig.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace MCPForUnity.Editor.Models -{ - [Serializable] - public class McpConfig - { - [JsonProperty("mcpServers")] - public McpConfigServers mcpServers; - } -} diff --git a/Assets/MCPForUnity/Editor/Models/McpConfig.cs.meta b/Assets/MCPForUnity/Editor/Models/McpConfig.cs.meta deleted file mode 100644 index 28c3991..0000000 --- a/Assets/MCPForUnity/Editor/Models/McpConfig.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: c17c09908f0c1524daa8b6957ce1f7f5 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Models/McpConfig.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Models/McpStatus.cs b/Assets/MCPForUnity/Editor/Models/McpStatus.cs deleted file mode 100644 index c23bc81..0000000 --- a/Assets/MCPForUnity/Editor/Models/McpStatus.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace MCPForUnity.Editor.Models -{ - // Enum representing the various status states for MCP clients - public enum McpStatus - { - NotConfigured, // Not set up yet - Configured, // Successfully configured - Running, // Service is running - Connected, // Successfully connected - IncorrectPath, // Configuration has incorrect paths - CommunicationError, // Connected but communication issues - NoResponse, // Connected but not responding - MissingConfig, // Config file exists but missing required elements - UnsupportedOS, // OS is not supported - Error, // General error state - } - - /// - /// Represents the transport type a client is configured to use. - /// Used to detect mismatches between server and client transport settings. - /// - public enum ConfiguredTransport - { - Unknown, // Could not determine transport type - Stdio, // Client configured for stdio transport - Http, // Client configured for HTTP local transport - HttpRemote // Client configured for HTTP remote-hosted transport - } -} - diff --git a/Assets/MCPForUnity/Editor/Models/McpStatus.cs.meta b/Assets/MCPForUnity/Editor/Models/McpStatus.cs.meta deleted file mode 100644 index 3215a61..0000000 --- a/Assets/MCPForUnity/Editor/Models/McpStatus.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: aa63057c9e5282d4887352578bf49971 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Models/McpStatus.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Resources.meta b/Assets/MCPForUnity/Editor/Resources.meta deleted file mode 100644 index 8d921df..0000000 --- a/Assets/MCPForUnity/Editor/Resources.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: a6f5bafffbb0f48c2a33ad9470bb1e2d -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Resources/Editor.meta b/Assets/MCPForUnity/Editor/Resources/Editor.meta deleted file mode 100644 index 5c252d1..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Editor.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 266967ec2e1df44209bf46ec6037d61d -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs b/Assets/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs deleted file mode 100644 index 13a5564..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; - -namespace MCPForUnity.Editor.Resources.Editor -{ - /// - /// Provides information about the currently active editor tool. - /// - [McpForUnityResource("get_active_tool")] - public static class ActiveTool - { - public static object HandleCommand(JObject @params) - { - try - { - Tool currentTool = UnityEditor.Tools.current; - string toolName = currentTool.ToString(); - bool customToolActive = UnityEditor.Tools.current == Tool.Custom; - string activeToolName = customToolActive ? EditorTools.GetActiveToolName() : toolName; - - var toolInfo = new - { - activeTool = activeToolName, - isCustom = customToolActive, - pivotMode = UnityEditor.Tools.pivotMode.ToString(), - pivotRotation = UnityEditor.Tools.pivotRotation.ToString(), - handleRotation = new - { - x = UnityEditor.Tools.handleRotation.eulerAngles.x, - y = UnityEditor.Tools.handleRotation.eulerAngles.y, - z = UnityEditor.Tools.handleRotation.eulerAngles.z - }, - handlePosition = new - { - x = UnityEditor.Tools.handlePosition.x, - y = UnityEditor.Tools.handlePosition.y, - z = UnityEditor.Tools.handlePosition.z - } - }; - - return new SuccessResponse("Retrieved active tool information.", toolInfo); - } - catch (Exception e) - { - return new ErrorResponse($"Error getting active tool: {e.Message}"); - } - } - } - - // Helper class for custom tool names - internal static class EditorTools - { - public static string GetActiveToolName() - { - if (UnityEditor.Tools.current == Tool.Custom) - { - return "Unknown Custom Tool"; - } - return UnityEditor.Tools.current.ToString(); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs.meta b/Assets/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs.meta deleted file mode 100644 index 9f1ccd2..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 6e78b6227ab7742a8a4f679ee6a8a212 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Resources/Editor/ActiveTool.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Resources/Editor/EditorState.cs b/Assets/MCPForUnity/Editor/Resources/Editor/EditorState.cs deleted file mode 100644 index b58c888..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Editor/EditorState.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; -using Newtonsoft.Json.Linq; - -namespace MCPForUnity.Editor.Resources.Editor -{ - /// - /// Provides dynamic editor state information that changes frequently. - /// - [McpForUnityResource("get_editor_state")] - public static class EditorState - { - public static object HandleCommand(JObject @params) - { - try - { - var snapshot = EditorStateCache.GetSnapshot(); - return new SuccessResponse("Retrieved editor state.", snapshot); - } - catch (Exception e) - { - return new ErrorResponse($"Error getting editor state: {e.Message}"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Resources/Editor/EditorState.cs.meta b/Assets/MCPForUnity/Editor/Resources/Editor/EditorState.cs.meta deleted file mode 100644 index f3a0a21..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Editor/EditorState.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: f7c6df54e014c44fdb0cd3f65a479e37 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Resources/Editor/EditorState.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Resources/Editor/Selection.cs b/Assets/MCPForUnity/Editor/Resources/Editor/Selection.cs deleted file mode 100644 index 022d9c4..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Editor/Selection.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Linq; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; - -namespace MCPForUnity.Editor.Resources.Editor -{ - /// - /// Provides detailed information about the current editor selection. - /// - [McpForUnityResource("get_selection")] - public static class Selection - { - public static object HandleCommand(JObject @params) - { - try - { - var selectionInfo = new - { - activeObject = UnityEditor.Selection.activeObject?.name, - activeGameObject = UnityEditor.Selection.activeGameObject?.name, - activeTransform = UnityEditor.Selection.activeTransform?.name, - activeInstanceID = UnityEditor.Selection.activeInstanceID, - count = UnityEditor.Selection.count, - objects = UnityEditor.Selection.objects - .Select(obj => new - { - name = obj?.name, - type = obj?.GetType().FullName, - instanceID = obj?.GetInstanceID() - }) - .ToList(), - gameObjects = UnityEditor.Selection.gameObjects - .Select(go => new - { - name = go?.name, - instanceID = go?.GetInstanceID() - }) - .ToList(), - assetGUIDs = UnityEditor.Selection.assetGUIDs - }; - - return new SuccessResponse("Retrieved current selection details.", selectionInfo); - } - catch (Exception e) - { - return new ErrorResponse($"Error getting selection: {e.Message}"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Resources/Editor/Selection.cs.meta b/Assets/MCPForUnity/Editor/Resources/Editor/Selection.cs.meta deleted file mode 100644 index ea9b555..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Editor/Selection.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: c7ea869623e094599a70be086ab4fc0e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Resources/Editor/Selection.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Resources/Editor/Windows.cs b/Assets/MCPForUnity/Editor/Resources/Editor/Windows.cs deleted file mode 100644 index b69a9d3..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Editor/Windows.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Resources.Editor -{ - /// - /// Provides list of all open editor windows. - /// - [McpForUnityResource("get_windows")] - public static class Windows - { - public static object HandleCommand(JObject @params) - { - try - { - EditorWindow[] allWindows = UnityEngine.Resources.FindObjectsOfTypeAll(); - var openWindows = new List(); - - foreach (EditorWindow window in allWindows) - { - if (window == null) - continue; - - try - { - openWindows.Add(new - { - title = window.titleContent.text, - typeName = window.GetType().FullName, - isFocused = EditorWindow.focusedWindow == window, - position = new - { - x = window.position.x, - y = window.position.y, - width = window.position.width, - height = window.position.height - }, - instanceID = window.GetInstanceID() - }); - } - catch (Exception ex) - { - McpLog.Warn($"Could not get info for window {window.GetType().Name}: {ex.Message}"); - } - } - - return new SuccessResponse("Retrieved list of open editor windows.", openWindows); - } - catch (Exception e) - { - return new ErrorResponse($"Error getting editor windows: {e.Message}"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Resources/Editor/Windows.cs.meta b/Assets/MCPForUnity/Editor/Resources/Editor/Windows.cs.meta deleted file mode 100644 index fdd8845..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Editor/Windows.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 58a341e64bea440b29deaf859aaea552 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Resources/Editor/Windows.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs b/Assets/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs deleted file mode 100644 index f86cd5f..0000000 --- a/Assets/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; - -namespace MCPForUnity.Editor.Resources -{ - /// - /// Marks a class as an MCP resource handler for auto-discovery. - /// The class must have a public static HandleCommand(JObject) method. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class McpForUnityResourceAttribute : Attribute - { - /// - /// The resource name used to route requests to this resource. - /// If not specified, defaults to the PascalCase class name converted to snake_case. - /// - public string ResourceName { get; } - - /// - /// Human-readable description of what this resource provides. - /// - public string Description { get; set; } - - /// - /// Create an MCP resource attribute with auto-generated resource name. - /// The resource name will be derived from the class name (PascalCase → snake_case). - /// Example: ManageAsset → manage_asset - /// - public McpForUnityResourceAttribute() - { - ResourceName = null; // Will be auto-generated - } - - /// - /// Create an MCP resource attribute with explicit resource name. - /// - /// The resource name (e.g., "manage_asset") - public McpForUnityResourceAttribute(string resourceName) - { - ResourceName = resourceName; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs.meta b/Assets/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs.meta deleted file mode 100644 index 1209739..0000000 --- a/Assets/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4c2d60f570f3d4bd2a6a2c1293094be3 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Resources/McpForUnityResourceAttribute.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Resources/MenuItems.meta b/Assets/MCPForUnity/Editor/Resources/MenuItems.meta deleted file mode 100644 index df20ed6..0000000 --- a/Assets/MCPForUnity/Editor/Resources/MenuItems.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: bca79cd3ef8ed466f9e50e2dc7850e46 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs b/Assets/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs deleted file mode 100644 index f6a8428..0000000 --- a/Assets/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; - -namespace MCPForUnity.Editor.Resources.MenuItems -{ - /// - /// Provides a simple read-only resource that returns Unity menu items. - /// - [McpForUnityResource("get_menu_items")] - public static class GetMenuItems - { - private static List _cached; - - [InitializeOnLoadMethod] - private static void BuildCache() => Refresh(); - - public static object HandleCommand(JObject @params) - { - bool forceRefresh = @params?["refresh"]?.ToObject() ?? false; - string search = @params?["search"]?.ToString(); - - var items = GetMenuItemsInternal(forceRefresh); - - if (!string.IsNullOrEmpty(search)) - { - items = items - .Where(item => item.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) - .ToList(); - } - - string message = $"Retrieved {items.Count} menu items"; - return new SuccessResponse(message, items); - } - - internal static List GetMenuItemsInternal(bool forceRefresh) - { - if (forceRefresh || _cached == null) - { - Refresh(); - } - - return (_cached ?? new List()).ToList(); - } - - private static void Refresh() - { - try - { - var methods = TypeCache.GetMethodsWithAttribute(); - _cached = methods - .SelectMany(m => m - .GetCustomAttributes(typeof(MenuItem), false) - .OfType() - .Select(attr => attr.menuItem)) - .Where(s => !string.IsNullOrEmpty(s)) - .Distinct(StringComparer.Ordinal) - .OrderBy(s => s, StringComparer.Ordinal) - .ToList(); - } - catch (Exception ex) - { - McpLog.Error($"[GetMenuItems] Failed to scan menu items: {ex}"); - _cached ??= new List(); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs.meta b/Assets/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs.meta deleted file mode 100644 index 9b5655d..0000000 --- a/Assets/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 04eeea61eb5c24033a88013845d25f23 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Resources/MenuItems/GetMenuItems.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Resources/Project.meta b/Assets/MCPForUnity/Editor/Resources/Project.meta deleted file mode 100644 index 1adf044..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Project.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 538489f13d7914c4eba9a67e29001b43 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Resources/Project/Layers.cs b/Assets/MCPForUnity/Editor/Resources/Project/Layers.cs deleted file mode 100644 index 9e9ef7d..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Project/Layers.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEngine; - -namespace MCPForUnity.Editor.Resources.Project -{ - /// - /// Provides dictionary of layer indices to layer names. - /// - [McpForUnityResource("get_layers")] - public static class Layers - { - private const int TotalLayerCount = 32; - - public static object HandleCommand(JObject @params) - { - try - { - var layers = new Dictionary(); - for (int i = 0; i < TotalLayerCount; i++) - { - string layerName = LayerMask.LayerToName(i); - if (!string.IsNullOrEmpty(layerName)) - { - layers.Add(i, layerName); - } - } - - return new SuccessResponse("Retrieved current named layers.", layers); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to retrieve layers: {e.Message}"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Resources/Project/Layers.cs.meta b/Assets/MCPForUnity/Editor/Resources/Project/Layers.cs.meta deleted file mode 100644 index cacffbb..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Project/Layers.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 959ee428299454ac19a636275208ca00 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Resources/Project/Layers.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs b/Assets/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs deleted file mode 100644 index 6e6d12f..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.IO; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Resources.Project -{ - /// - /// Provides static project configuration information. - /// - [McpForUnityResource("get_project_info")] - public static class ProjectInfo - { - public static object HandleCommand(JObject @params) - { - try - { - string assetsPath = Application.dataPath.Replace('\\', '/'); - string projectRoot = Directory.GetParent(assetsPath)?.FullName.Replace('\\', '/'); - string projectName = Path.GetFileName(projectRoot); - - var info = new - { - projectRoot = projectRoot ?? "", - projectName = projectName ?? "", - unityVersion = Application.unityVersion, - platform = EditorUserBuildSettings.activeBuildTarget.ToString(), - assetsPath = assetsPath - }; - - return new SuccessResponse("Retrieved project info.", info); - } - catch (Exception e) - { - return new ErrorResponse($"Error getting project info: {e.Message}"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs.meta b/Assets/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs.meta deleted file mode 100644 index a98914c..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 81b03415fcf93466e9ed667d19b58d43 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Resources/Project/ProjectInfo.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Resources/Project/Tags.cs b/Assets/MCPForUnity/Editor/Resources/Project/Tags.cs deleted file mode 100644 index 756f00d..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Project/Tags.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditorInternal; - -namespace MCPForUnity.Editor.Resources.Project -{ - /// - /// Provides list of all tags in the project. - /// - [McpForUnityResource("get_tags")] - public static class Tags - { - public static object HandleCommand(JObject @params) - { - try - { - string[] tags = InternalEditorUtility.tags; - return new SuccessResponse("Retrieved current tags.", tags); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to retrieve tags: {e.Message}"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Resources/Project/Tags.cs.meta b/Assets/MCPForUnity/Editor/Resources/Project/Tags.cs.meta deleted file mode 100644 index f412694..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Project/Tags.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 2179ac5d98f264d1681e7d5c0d0ed341 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Resources/Project/Tags.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Resources/Scene.meta b/Assets/MCPForUnity/Editor/Resources/Scene.meta deleted file mode 100644 index 20d08db..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Scene.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 563f6050485b445449a1db200bfba51c -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Resources/Scene/GameObjectResource.cs b/Assets/MCPForUnity/Editor/Resources/Scene/GameObjectResource.cs deleted file mode 100644 index 2588349..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Scene/GameObjectResource.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Resources.Scene -{ - /// - /// Resource handler for reading GameObject data. - /// Provides read-only access to GameObject information without component serialization. - /// - /// URI: unity://scene/gameobject/{instanceID} - /// - [McpForUnityResource("get_gameobject")] - public static class GameObjectResource - { - public static object HandleCommand(JObject @params) - { - if (@params == null) - { - return new ErrorResponse("Parameters cannot be null."); - } - - // Get instance ID from params - int? instanceID = null; - - var idToken = @params["instanceID"] ?? @params["instance_id"] ?? @params["id"]; - if (idToken != null) - { - instanceID = ParamCoercion.CoerceInt(idToken, -1); - if (instanceID == -1) - { - instanceID = null; - } - } - - if (!instanceID.HasValue) - { - return new ErrorResponse("'instanceID' parameter is required."); - } - - try - { - var go = EditorUtility.InstanceIDToObject(instanceID.Value) as GameObject; - if (go == null) - { - return new ErrorResponse($"GameObject with instance ID {instanceID} not found."); - } - - return new - { - success = true, - data = SerializeGameObject(go) - }; - } - catch (Exception e) - { - McpLog.Error($"[GameObjectResource] Error getting GameObject: {e}"); - return new ErrorResponse($"Error getting GameObject: {e.Message}"); - } - } - - /// - /// Serializes a GameObject without component details. - /// For component data, use GetComponents or GetComponent resources. - /// - public static object SerializeGameObject(GameObject go) - { - if (go == null) - return null; - - var transform = go.transform; - - // Get component type names (not full serialization) - var componentTypes = go.GetComponents() - .Where(c => c != null) - .Select(c => c.GetType().Name) - .ToList(); - - // Get children instance IDs (not full serialization) - var childrenIds = new List(); - foreach (Transform child in transform) - { - childrenIds.Add(child.gameObject.GetInstanceID()); - } - - return new - { - instanceID = go.GetInstanceID(), - name = go.name, - tag = go.tag, - layer = go.layer, - layerName = LayerMask.LayerToName(go.layer), - active = go.activeSelf, - activeInHierarchy = go.activeInHierarchy, - isStatic = go.isStatic, - transform = new - { - position = SerializeVector3(transform.position), - localPosition = SerializeVector3(transform.localPosition), - rotation = SerializeVector3(transform.eulerAngles), - localRotation = SerializeVector3(transform.localEulerAngles), - scale = SerializeVector3(transform.localScale), - lossyScale = SerializeVector3(transform.lossyScale) - }, - parent = transform.parent != null ? transform.parent.gameObject.GetInstanceID() : (int?)null, - children = childrenIds, - componentTypes = componentTypes, - path = GameObjectLookup.GetGameObjectPath(go) - }; - } - - private static object SerializeVector3(Vector3 v) - { - return new { x = v.x, y = v.y, z = v.z }; - } - } - - /// - /// Resource handler for reading all components on a GameObject. - /// - /// URI: unity://scene/gameobject/{instanceID}/components - /// - [McpForUnityResource("get_gameobject_components")] - public static class GameObjectComponentsResource - { - public static object HandleCommand(JObject @params) - { - if (@params == null) - { - return new ErrorResponse("Parameters cannot be null."); - } - - var idToken = @params["instanceID"] ?? @params["instance_id"] ?? @params["id"]; - int instanceID = ParamCoercion.CoerceInt(idToken, -1); - if (instanceID == -1) - { - return new ErrorResponse("'instanceID' parameter is required."); - } - - // Pagination parameters - int pageSize = ParamCoercion.CoerceInt(@params["pageSize"] ?? @params["page_size"], 25); - int cursor = ParamCoercion.CoerceInt(@params["cursor"], 0); - bool includeProperties = ParamCoercion.CoerceBool(@params["includeProperties"] ?? @params["include_properties"], true); - - pageSize = Mathf.Clamp(pageSize, 1, 100); - - try - { - var go = EditorUtility.InstanceIDToObject(instanceID) as GameObject; - if (go == null) - { - return new ErrorResponse($"GameObject with instance ID {instanceID} not found."); - } - - var allComponents = go.GetComponents().Where(c => c != null).ToList(); - int total = allComponents.Count; - - var pagedComponents = allComponents.Skip(cursor).Take(pageSize).ToList(); - - var componentData = new List(); - foreach (var component in pagedComponents) - { - if (includeProperties) - { - componentData.Add(GameObjectSerializer.GetComponentData(component)); - } - else - { - componentData.Add(new - { - typeName = component.GetType().FullName, - instanceID = component.GetInstanceID() - }); - } - } - - int? nextCursor = cursor + pagedComponents.Count < total ? cursor + pagedComponents.Count : (int?)null; - - return new - { - success = true, - data = new - { - gameObjectID = instanceID, - gameObjectName = go.name, - components = componentData, - cursor = cursor, - pageSize = pageSize, - nextCursor = nextCursor, - totalCount = total, - hasMore = nextCursor.HasValue, - includeProperties = includeProperties - } - }; - } - catch (Exception e) - { - McpLog.Error($"[GameObjectComponentsResource] Error getting components: {e}"); - return new ErrorResponse($"Error getting components: {e.Message}"); - } - } - } - - /// - /// Resource handler for reading a single component on a GameObject. - /// - /// URI: unity://scene/gameobject/{instanceID}/component/{componentName} - /// - [McpForUnityResource("get_gameobject_component")] - public static class GameObjectComponentResource - { - public static object HandleCommand(JObject @params) - { - if (@params == null) - { - return new ErrorResponse("Parameters cannot be null."); - } - - var idToken = @params["instanceID"] ?? @params["instance_id"] ?? @params["id"]; - int instanceID = ParamCoercion.CoerceInt(idToken, -1); - if (instanceID == -1) - { - return new ErrorResponse("'instanceID' parameter is required."); - } - - string componentName = ParamCoercion.CoerceString(@params["componentName"] ?? @params["component_name"] ?? @params["component"], null); - if (string.IsNullOrEmpty(componentName)) - { - return new ErrorResponse("'componentName' parameter is required."); - } - - try - { - var go = EditorUtility.InstanceIDToObject(instanceID) as GameObject; - if (go == null) - { - return new ErrorResponse($"GameObject with instance ID {instanceID} not found."); - } - - // Find the component by type name - Component targetComponent = null; - foreach (var component in go.GetComponents()) - { - if (component == null) continue; - - var typeName = component.GetType().Name; - var fullTypeName = component.GetType().FullName; - - if (string.Equals(typeName, componentName, StringComparison.OrdinalIgnoreCase) || - string.Equals(fullTypeName, componentName, StringComparison.OrdinalIgnoreCase)) - { - targetComponent = component; - break; - } - } - - if (targetComponent == null) - { - return new ErrorResponse($"Component '{componentName}' not found on GameObject '{go.name}'."); - } - - return new - { - success = true, - data = new - { - gameObjectID = instanceID, - gameObjectName = go.name, - component = GameObjectSerializer.GetComponentData(targetComponent) - } - }; - } - catch (Exception e) - { - McpLog.Error($"[GameObjectComponentResource] Error getting component: {e}"); - return new ErrorResponse($"Error getting component: {e.Message}"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Resources/Scene/GameObjectResource.cs.meta b/Assets/MCPForUnity/Editor/Resources/Scene/GameObjectResource.cs.meta deleted file mode 100644 index e545e44..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Scene/GameObjectResource.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 5ee79050d9f6d42798a0757cc7672517 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Resources/Scene/GameObjectResource.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Resources/Tests.meta b/Assets/MCPForUnity/Editor/Resources/Tests.meta deleted file mode 100644 index 0aa0bf0..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Tests.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 412726d2e774048939b0d2bd4f11a503 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Resources/Tests/GetTests.cs b/Assets/MCPForUnity/Editor/Resources/Tests/GetTests.cs deleted file mode 100644 index 4fe48c7..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Tests/GetTests.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; -using Newtonsoft.Json.Linq; -using UnityEditor.TestTools.TestRunner.Api; - -namespace MCPForUnity.Editor.Resources.Tests -{ - /// - /// Provides access to Unity tests from the Test Framework with pagination and filtering support. - /// This is a read-only resource that can be queried by MCP clients. - /// - /// Parameters: - /// - mode (optional): Filter by "EditMode" or "PlayMode" - /// - filter (optional): Filter test names by pattern (case-insensitive contains) - /// - page_size (optional): Number of tests per page (default: 50, max: 200) - /// - cursor (optional): 0-based cursor for pagination - /// - page_number (optional): 1-based page number (converted to cursor) - /// - [McpForUnityResource("get_tests")] - public static class GetTests - { - private const int DEFAULT_PAGE_SIZE = 50; - private const int MAX_PAGE_SIZE = 200; - - public static async Task HandleCommand(JObject @params) - { - // Parse mode filter - TestMode? modeFilter = null; - string modeStr = @params?["mode"]?.ToString(); - if (!string.IsNullOrEmpty(modeStr)) - { - if (!ModeParser.TryParse(modeStr, out modeFilter, out var parseError)) - { - return new ErrorResponse(parseError); - } - } - - // Parse name filter - string nameFilter = @params?["filter"]?.ToString(); - - McpLog.Info($"[GetTests] Retrieving tests (mode={modeFilter?.ToString() ?? "all"}, filter={nameFilter ?? "none"})"); - - IReadOnlyList> allTests; - try - { - allTests = await MCPServiceLocator.Tests.GetTestsAsync(modeFilter).ConfigureAwait(true); - } - catch (Exception ex) - { - McpLog.Error($"[GetTests] Error retrieving tests: {ex.Message}\n{ex.StackTrace}"); - return new ErrorResponse("Failed to retrieve tests"); - } - - // Apply name filter if provided and convert to List for pagination - List> filteredTests; - if (!string.IsNullOrEmpty(nameFilter)) - { - filteredTests = allTests - .Where(t => - (t.ContainsKey("name") && t["name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0) || - (t.ContainsKey("full_name") && t["full_name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0) - ) - .ToList(); - } - else - { - filteredTests = allTests.ToList(); - } - - // Clamp page_size before parsing pagination to ensure cursor is computed correctly - int requestedPageSize = ParamCoercion.CoerceInt( - @params?["page_size"] ?? @params?["pageSize"], - DEFAULT_PAGE_SIZE - ); - int clampedPageSize = System.Math.Min(requestedPageSize, MAX_PAGE_SIZE); - if (clampedPageSize <= 0) clampedPageSize = DEFAULT_PAGE_SIZE; - - // Create modified params with clamped page_size for cursor calculation - var paginationParams = new JObject(@params); - paginationParams["page_size"] = clampedPageSize; - - // Parse pagination with clamped page size - var pagination = PaginationRequest.FromParams(paginationParams, DEFAULT_PAGE_SIZE); - - // Create paginated response - var response = PaginationResponse>.Create(filteredTests, pagination); - - string message = !string.IsNullOrEmpty(nameFilter) - ? $"Retrieved {response.Items.Count} of {response.TotalCount} tests matching '{nameFilter}' (cursor {response.Cursor})" - : $"Retrieved {response.Items.Count} of {response.TotalCount} tests (cursor {response.Cursor})"; - - return new SuccessResponse(message, response); - } - } - - /// - /// DEPRECATED: Use get_tests with mode parameter instead. - /// Provides access to Unity tests for a specific mode (EditMode or PlayMode). - /// This is a read-only resource that can be queried by MCP clients. - /// - /// Parameters: - /// - mode (required): "EditMode" or "PlayMode" - /// - filter (optional): Filter test names by pattern (case-insensitive contains) - /// - page_size (optional): Number of tests per page (default: 50, max: 200) - /// - cursor (optional): 0-based cursor for pagination - /// - [McpForUnityResource("get_tests_for_mode")] - public static class GetTestsForMode - { - private const int DEFAULT_PAGE_SIZE = 50; - private const int MAX_PAGE_SIZE = 200; - - public static async Task HandleCommand(JObject @params) - { - string modeStr = @params?["mode"]?.ToString(); - if (string.IsNullOrEmpty(modeStr)) - { - return new ErrorResponse("'mode' parameter is required"); - } - - if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) - { - return new ErrorResponse(parseError); - } - - // Parse name filter - string nameFilter = @params?["filter"]?.ToString(); - - McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value} (filter={nameFilter ?? "none"})"); - - IReadOnlyList> allTests; - try - { - allTests = await MCPServiceLocator.Tests.GetTestsAsync(parsedMode).ConfigureAwait(true); - } - catch (Exception ex) - { - McpLog.Error($"[GetTestsForMode] Error retrieving tests: {ex.Message}\n{ex.StackTrace}"); - return new ErrorResponse("Failed to retrieve tests"); - } - - // Apply name filter if provided and convert to List for pagination - List> filteredTests; - if (!string.IsNullOrEmpty(nameFilter)) - { - filteredTests = allTests - .Where(t => - (t.ContainsKey("name") && t["name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0) || - (t.ContainsKey("full_name") && t["full_name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0) - ) - .ToList(); - } - else - { - filteredTests = allTests.ToList(); - } - - // Clamp page_size before parsing pagination to ensure cursor is computed correctly - int requestedPageSize = ParamCoercion.CoerceInt( - @params?["page_size"] ?? @params?["pageSize"], - DEFAULT_PAGE_SIZE - ); - int clampedPageSize = System.Math.Min(requestedPageSize, MAX_PAGE_SIZE); - if (clampedPageSize <= 0) clampedPageSize = DEFAULT_PAGE_SIZE; - - // Create modified params with clamped page_size for cursor calculation - var paginationParams = new JObject(@params); - paginationParams["page_size"] = clampedPageSize; - - // Parse pagination with clamped page size - var pagination = PaginationRequest.FromParams(paginationParams, DEFAULT_PAGE_SIZE); - - // Create paginated response - var response = PaginationResponse>.Create(filteredTests, pagination); - - string message = nameFilter != null - ? $"Retrieved {response.Items.Count} of {response.TotalCount} {parsedMode.Value} tests matching '{nameFilter}'" - : $"Retrieved {response.Items.Count} of {response.TotalCount} {parsedMode.Value} tests"; - - return new SuccessResponse(message, response); - } - } - - internal static class ModeParser - { - internal static bool TryParse(string modeStr, out TestMode? mode, out string error) - { - error = null; - mode = null; - - if (string.IsNullOrWhiteSpace(modeStr)) - { - error = "'mode' parameter cannot be empty"; - return false; - } - - if (modeStr.Equals("EditMode", StringComparison.OrdinalIgnoreCase)) - { - mode = TestMode.EditMode; - return true; - } - - if (modeStr.Equals("PlayMode", StringComparison.OrdinalIgnoreCase)) - { - mode = TestMode.PlayMode; - return true; - } - - error = $"Unknown test mode: '{modeStr}'. Use 'EditMode' or 'PlayMode'"; - return false; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Resources/Tests/GetTests.cs.meta b/Assets/MCPForUnity/Editor/Resources/Tests/GetTests.cs.meta deleted file mode 100644 index 5342397..0000000 --- a/Assets/MCPForUnity/Editor/Resources/Tests/GetTests.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 84183aaed077e4f25968269c952db2d7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Resources/Tests/GetTests.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services.meta b/Assets/MCPForUnity/Editor/Services.meta deleted file mode 100644 index e800dea..0000000 --- a/Assets/MCPForUnity/Editor/Services.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 2ab6b1cc527214416b21e07b96164f24 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Services/BridgeControlService.cs b/Assets/MCPForUnity/Editor/Services/BridgeControlService.cs deleted file mode 100644 index 04583e7..0000000 --- a/Assets/MCPForUnity/Editor/Services/BridgeControlService.cs +++ /dev/null @@ -1,157 +0,0 @@ - -using System; -using System.Threading.Tasks; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services.Transport; -using MCPForUnity.Editor.Services.Transport.Transports; -using UnityEditor; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Bridges the editor UI to the active transport (HTTP with WebSocket push, or stdio). - /// - public class BridgeControlService : IBridgeControlService - { - private readonly TransportManager _transportManager; - private TransportMode _preferredMode = TransportMode.Http; - - public BridgeControlService() - { - _transportManager = MCPServiceLocator.TransportManager; - } - - private TransportMode ResolvePreferredMode() - { - bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; - _preferredMode = useHttp ? TransportMode.Http : TransportMode.Stdio; - return _preferredMode; - } - - private static BridgeVerificationResult BuildVerificationResult(TransportState state, TransportMode mode, bool pingSucceeded, string messageOverride = null, bool? handshakeOverride = null) - { - bool handshakeValid = handshakeOverride ?? (mode == TransportMode.Stdio ? state.IsConnected : true); - string transportLabel = string.IsNullOrWhiteSpace(state.TransportName) - ? mode.ToString().ToLowerInvariant() - : state.TransportName; - string detailSuffix = string.IsNullOrWhiteSpace(state.Details) ? string.Empty : $" [{state.Details}]"; - string message = messageOverride - ?? state.Error - ?? (state.IsConnected ? $"Transport '{transportLabel}' connected{detailSuffix}" : $"Transport '{transportLabel}' disconnected{detailSuffix}"); - - return new BridgeVerificationResult - { - Success = pingSucceeded && handshakeValid, - HandshakeValid = handshakeValid, - PingSucceeded = pingSucceeded, - Message = message - }; - } - - public bool IsRunning - { - get - { - var mode = ResolvePreferredMode(); - return _transportManager.IsRunning(mode); - } - } - - public int CurrentPort - { - get - { - var mode = ResolvePreferredMode(); - var state = _transportManager.GetState(mode); - if (state.Port.HasValue) - { - return state.Port.Value; - } - - // Legacy fallback while the stdio bridge is still in play - return StdioBridgeHost.GetCurrentPort(); - } - } - - public bool IsAutoConnectMode => StdioBridgeHost.IsAutoConnectMode(); - public TransportMode? ActiveMode => ResolvePreferredMode(); - - public async Task StartAsync() - { - var mode = ResolvePreferredMode(); - try - { - // Treat transports as mutually exclusive for user-driven session starts: - // stop the *other* transport first to avoid duplicated sessions (e.g. stdio lingering when switching to HTTP). - var otherMode = mode == TransportMode.Http ? TransportMode.Stdio : TransportMode.Http; - try - { - await _transportManager.StopAsync(otherMode); - } - catch (Exception ex) - { - McpLog.Warn($"Error stopping other transport ({otherMode}) before start: {ex.Message}"); - } - - // Legacy safety: stdio may have been started outside TransportManager state. - if (otherMode == TransportMode.Stdio) - { - try { StdioBridgeHost.Stop(); } catch { } - } - - bool started = await _transportManager.StartAsync(mode); - if (!started) - { - McpLog.Warn($"Failed to start MCP transport: {mode}"); - } - return started; - } - catch (Exception ex) - { - McpLog.Error($"Error starting MCP transport {mode}: {ex.Message}"); - return false; - } - } - - public async Task StopAsync() - { - try - { - var mode = ResolvePreferredMode(); - await _transportManager.StopAsync(mode); - } - catch (Exception ex) - { - McpLog.Warn($"Error stopping MCP transport: {ex.Message}"); - } - } - - public async Task VerifyAsync() - { - var mode = ResolvePreferredMode(); - bool pingSucceeded = await _transportManager.VerifyAsync(mode); - var state = _transportManager.GetState(mode); - return BuildVerificationResult(state, mode, pingSucceeded); - } - - public BridgeVerificationResult Verify(int port) - { - var mode = ResolvePreferredMode(); - bool pingSucceeded = _transportManager.VerifyAsync(mode).GetAwaiter().GetResult(); - var state = _transportManager.GetState(mode); - - if (mode == TransportMode.Stdio) - { - bool handshakeValid = state.IsConnected && port == CurrentPort; - string message = handshakeValid - ? $"STDIO transport listening on port {CurrentPort}" - : $"STDIO transport port mismatch (expected {CurrentPort}, got {port})"; - return BuildVerificationResult(state, mode, pingSucceeded && handshakeValid, message, handshakeValid); - } - - return BuildVerificationResult(state, mode, pingSucceeded); - } - - } -} diff --git a/Assets/MCPForUnity/Editor/Services/BridgeControlService.cs.meta b/Assets/MCPForUnity/Editor/Services/BridgeControlService.cs.meta deleted file mode 100644 index 402dbfd..0000000 --- a/Assets/MCPForUnity/Editor/Services/BridgeControlService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: ed4f9f69d84a945248dafc0f0b5a62dd -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/BridgeControlService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/ClientConfigurationService.cs b/Assets/MCPForUnity/Editor/Services/ClientConfigurationService.cs deleted file mode 100644 index 65f0e1d..0000000 --- a/Assets/MCPForUnity/Editor/Services/ClientConfigurationService.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MCPForUnity.Editor.Clients; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Implementation of client configuration service - /// - public class ClientConfigurationService : IClientConfigurationService - { - private readonly List configurators; - - public ClientConfigurationService() - { - configurators = McpClientRegistry.All.ToList(); - } - - public IReadOnlyList GetAllClients() => configurators; - - public void ConfigureClient(IMcpClientConfigurator configurator) - { - // When using a local server path, clean stale build artifacts first. - // This prevents old deleted .py files from being picked up by Python's auto-discovery. - if (AssetPathUtility.IsLocalServerPath()) - { - AssetPathUtility.CleanLocalServerBuildArtifacts(); - } - - configurator.Configure(); - } - - public ClientConfigurationSummary ConfigureAllDetectedClients() - { - // When using a local server path, clean stale build artifacts once before configuring all clients. - if (AssetPathUtility.IsLocalServerPath()) - { - AssetPathUtility.CleanLocalServerBuildArtifacts(); - } - - var summary = new ClientConfigurationSummary(); - foreach (var configurator in configurators) - { - try - { - // Always re-run configuration so core fields stay current - configurator.CheckStatus(attemptAutoRewrite: false); - configurator.Configure(); - summary.SuccessCount++; - summary.Messages.Add($"✓ {configurator.DisplayName}: Configured successfully"); - } - catch (Exception ex) - { - summary.FailureCount++; - summary.Messages.Add($"⚠ {configurator.DisplayName}: {ex.Message}"); - } - } - - return summary; - } - - public bool CheckClientStatus(IMcpClientConfigurator configurator, bool attemptAutoRewrite = true) - { - var previous = configurator.Status; - var current = configurator.CheckStatus(attemptAutoRewrite); - return current != previous; - } - - } -} diff --git a/Assets/MCPForUnity/Editor/Services/ClientConfigurationService.cs.meta b/Assets/MCPForUnity/Editor/Services/ClientConfigurationService.cs.meta deleted file mode 100644 index f046337..0000000 --- a/Assets/MCPForUnity/Editor/Services/ClientConfigurationService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 76cad34d10fd24aaa95c4583c1f88fdf -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/ClientConfigurationService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/EditorConfigurationCache.cs b/Assets/MCPForUnity/Editor/Services/EditorConfigurationCache.cs deleted file mode 100644 index 40dedd5..0000000 --- a/Assets/MCPForUnity/Editor/Services/EditorConfigurationCache.cs +++ /dev/null @@ -1,345 +0,0 @@ -using System; -using MCPForUnity.Editor.Constants; -using UnityEditor; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Centralized cache for frequently-read EditorPrefs values. - /// Reduces scattered EditorPrefs.Get* calls and provides change notification. - /// - /// Usage: - /// var config = EditorConfigurationCache.Instance; - /// if (config.UseHttpTransport) { ... } - /// config.OnConfigurationChanged += (key) => { /* refresh UI */ }; - /// - public class EditorConfigurationCache - { - private static EditorConfigurationCache _instance; - private static readonly object _lock = new object(); - - /// - /// Singleton instance. Thread-safe lazy initialization. - /// - public static EditorConfigurationCache Instance - { - get - { - if (_instance == null) - { - lock (_lock) - { - if (_instance == null) - { - _instance = new EditorConfigurationCache(); - } - } - } - return _instance; - } - } - - /// - /// Event fired when any cached configuration value changes. - /// The string parameter is the EditorPrefKeys constant name that changed. - /// - public event Action OnConfigurationChanged; - - // Cached values - most frequently read - private bool _useHttpTransport; - private bool _debugLogs; - private bool _useBetaServer; - private bool _devModeForceServerRefresh; - private string _uvxPathOverride; - private string _gitUrlOverride; - private string _httpBaseUrl; - private string _httpRemoteBaseUrl; - private string _claudeCliPathOverride; - private string _httpTransportScope; - private int _unitySocketPort; - - /// - /// Whether to use HTTP transport (true) or Stdio transport (false). - /// Default: true - /// - public bool UseHttpTransport => _useHttpTransport; - - /// - /// Whether debug logging is enabled. - /// Default: false - /// - public bool DebugLogs => _debugLogs; - - /// - /// Whether to use the beta server channel. - /// Default: true - /// - public bool UseBetaServer => _useBetaServer; - - /// - /// Whether to force server refresh in dev mode (--no-cache --refresh). - /// Default: false - /// - public bool DevModeForceServerRefresh => _devModeForceServerRefresh; - - /// - /// Custom path override for uvx executable. - /// Default: empty string (auto-detect) - /// - public string UvxPathOverride => _uvxPathOverride; - - /// - /// Custom Git URL override for server installation. - /// Default: empty string (use default) - /// - public string GitUrlOverride => _gitUrlOverride; - - /// - /// HTTP base URL for the local MCP server. - /// Default: empty string - /// - public string HttpBaseUrl => _httpBaseUrl; - - /// - /// HTTP base URL for the remote-hosted MCP server. - /// Default: empty string - /// - public string HttpRemoteBaseUrl => _httpRemoteBaseUrl; - - /// - /// Custom path override for Claude CLI executable. - /// Default: empty string (auto-detect) - /// - public string ClaudeCliPathOverride => _claudeCliPathOverride; - - /// - /// HTTP transport scope: "local" or "remote". - /// Default: empty string - /// - public string HttpTransportScope => _httpTransportScope; - - /// - /// Unity socket port for Stdio transport. - /// Default: 0 (auto-assign) - /// - public int UnitySocketPort => _unitySocketPort; - - private EditorConfigurationCache() - { - Refresh(); - } - - /// - /// Refresh all cached values from EditorPrefs. - /// Call this after bulk EditorPrefs changes or domain reload. - /// - public void Refresh() - { - _useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); - _debugLogs = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false); - _useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true); - _devModeForceServerRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); - _uvxPathOverride = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty); - _gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, string.Empty); - _httpBaseUrl = EditorPrefs.GetString(EditorPrefKeys.HttpBaseUrl, string.Empty); - _httpRemoteBaseUrl = EditorPrefs.GetString(EditorPrefKeys.HttpRemoteBaseUrl, string.Empty); - _claudeCliPathOverride = EditorPrefs.GetString(EditorPrefKeys.ClaudeCliPathOverride, string.Empty); - _httpTransportScope = EditorPrefs.GetString(EditorPrefKeys.HttpTransportScope, string.Empty); - _unitySocketPort = EditorPrefs.GetInt(EditorPrefKeys.UnitySocketPort, 0); - } - - /// - /// Set UseHttpTransport and update cache + EditorPrefs atomically. - /// - public void SetUseHttpTransport(bool value) - { - if (_useHttpTransport != value) - { - _useHttpTransport = value; - EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, value); - OnConfigurationChanged?.Invoke(nameof(UseHttpTransport)); - } - } - - /// - /// Set DebugLogs and update cache + EditorPrefs atomically. - /// - public void SetDebugLogs(bool value) - { - if (_debugLogs != value) - { - _debugLogs = value; - EditorPrefs.SetBool(EditorPrefKeys.DebugLogs, value); - OnConfigurationChanged?.Invoke(nameof(DebugLogs)); - } - } - - /// - /// Set UseBetaServer and update cache + EditorPrefs atomically. - /// - public void SetUseBetaServer(bool value) - { - if (_useBetaServer != value) - { - _useBetaServer = value; - EditorPrefs.SetBool(EditorPrefKeys.UseBetaServer, value); - OnConfigurationChanged?.Invoke(nameof(UseBetaServer)); - } - } - - /// - /// Set DevModeForceServerRefresh and update cache + EditorPrefs atomically. - /// - public void SetDevModeForceServerRefresh(bool value) - { - if (_devModeForceServerRefresh != value) - { - _devModeForceServerRefresh = value; - EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, value); - OnConfigurationChanged?.Invoke(nameof(DevModeForceServerRefresh)); - } - } - - /// - /// Set UvxPathOverride and update cache + EditorPrefs atomically. - /// - public void SetUvxPathOverride(string value) - { - value = value ?? string.Empty; - if (_uvxPathOverride != value) - { - _uvxPathOverride = value; - EditorPrefs.SetString(EditorPrefKeys.UvxPathOverride, value); - OnConfigurationChanged?.Invoke(nameof(UvxPathOverride)); - } - } - - /// - /// Set GitUrlOverride and update cache + EditorPrefs atomically. - /// - public void SetGitUrlOverride(string value) - { - value = value ?? string.Empty; - if (_gitUrlOverride != value) - { - _gitUrlOverride = value; - EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, value); - OnConfigurationChanged?.Invoke(nameof(GitUrlOverride)); - } - } - - /// - /// Set HttpBaseUrl and update cache + EditorPrefs atomically. - /// - public void SetHttpBaseUrl(string value) - { - value = value ?? string.Empty; - if (_httpBaseUrl != value) - { - _httpBaseUrl = value; - EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, value); - OnConfigurationChanged?.Invoke(nameof(HttpBaseUrl)); - } - } - - /// - /// Set HttpRemoteBaseUrl and update cache + EditorPrefs atomically. - /// - public void SetHttpRemoteBaseUrl(string value) - { - value = value ?? string.Empty; - if (_httpRemoteBaseUrl != value) - { - _httpRemoteBaseUrl = value; - EditorPrefs.SetString(EditorPrefKeys.HttpRemoteBaseUrl, value); - OnConfigurationChanged?.Invoke(nameof(HttpRemoteBaseUrl)); - } - } - - /// - /// Set ClaudeCliPathOverride and update cache + EditorPrefs atomically. - /// - public void SetClaudeCliPathOverride(string value) - { - value = value ?? string.Empty; - if (_claudeCliPathOverride != value) - { - _claudeCliPathOverride = value; - EditorPrefs.SetString(EditorPrefKeys.ClaudeCliPathOverride, value); - OnConfigurationChanged?.Invoke(nameof(ClaudeCliPathOverride)); - } - } - - /// - /// Set HttpTransportScope and update cache + EditorPrefs atomically. - /// - public void SetHttpTransportScope(string value) - { - value = value ?? string.Empty; - if (_httpTransportScope != value) - { - _httpTransportScope = value; - EditorPrefs.SetString(EditorPrefKeys.HttpTransportScope, value); - OnConfigurationChanged?.Invoke(nameof(HttpTransportScope)); - } - } - - /// - /// Set UnitySocketPort and update cache + EditorPrefs atomically. - /// - public void SetUnitySocketPort(int value) - { - if (_unitySocketPort != value) - { - _unitySocketPort = value; - EditorPrefs.SetInt(EditorPrefKeys.UnitySocketPort, value); - OnConfigurationChanged?.Invoke(nameof(UnitySocketPort)); - } - } - - /// - /// Force refresh of a single cached value from EditorPrefs. - /// Useful when external code modifies EditorPrefs directly. - /// - public void InvalidateKey(string keyName) - { - switch (keyName) - { - case nameof(UseHttpTransport): - _useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); - break; - case nameof(DebugLogs): - _debugLogs = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false); - break; - case nameof(UseBetaServer): - _useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true); - break; - case nameof(DevModeForceServerRefresh): - _devModeForceServerRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); - break; - case nameof(UvxPathOverride): - _uvxPathOverride = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty); - break; - case nameof(GitUrlOverride): - _gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, string.Empty); - break; - case nameof(HttpBaseUrl): - _httpBaseUrl = EditorPrefs.GetString(EditorPrefKeys.HttpBaseUrl, string.Empty); - break; - case nameof(HttpRemoteBaseUrl): - _httpRemoteBaseUrl = EditorPrefs.GetString(EditorPrefKeys.HttpRemoteBaseUrl, string.Empty); - break; - case nameof(ClaudeCliPathOverride): - _claudeCliPathOverride = EditorPrefs.GetString(EditorPrefKeys.ClaudeCliPathOverride, string.Empty); - break; - case nameof(HttpTransportScope): - _httpTransportScope = EditorPrefs.GetString(EditorPrefKeys.HttpTransportScope, string.Empty); - break; - case nameof(UnitySocketPort): - _unitySocketPort = EditorPrefs.GetInt(EditorPrefKeys.UnitySocketPort, 0); - break; - } - OnConfigurationChanged?.Invoke(keyName); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/EditorConfigurationCache.cs.meta b/Assets/MCPForUnity/Editor/Services/EditorConfigurationCache.cs.meta deleted file mode 100644 index d25631e..0000000 --- a/Assets/MCPForUnity/Editor/Services/EditorConfigurationCache.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b4a183ac9b63c408886bce40ae58f462 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/EditorConfigurationCache.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/EditorPrefsWindowService.cs b/Assets/MCPForUnity/Editor/Services/EditorPrefsWindowService.cs deleted file mode 100644 index 2511b03..0000000 --- a/Assets/MCPForUnity/Editor/Services/EditorPrefsWindowService.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using MCPForUnity.Editor.Windows; -using UnityEditor; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Service for managing the EditorPrefs window - /// Follows the Class-level Singleton pattern - /// - public class EditorPrefsWindowService - { - private static EditorPrefsWindowService _instance; - - /// - /// Get the singleton instance - /// - public static EditorPrefsWindowService Instance - { - get - { - if (_instance == null) - { - throw new Exception("EditorPrefsWindowService not initialized"); - } - return _instance; - } - } - - /// - /// Initialize the service - /// - public static void Initialize() - { - if (_instance == null) - { - _instance = new EditorPrefsWindowService(); - } - } - - private EditorPrefsWindowService() - { - // Private constructor for singleton - } - - /// - /// Show the EditorPrefs window - /// - public void ShowWindow() - { - EditorPrefsWindow.ShowWindow(); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/EditorPrefsWindowService.cs.meta b/Assets/MCPForUnity/Editor/Services/EditorPrefsWindowService.cs.meta deleted file mode 100644 index 7e49399..0000000 --- a/Assets/MCPForUnity/Editor/Services/EditorPrefsWindowService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 2a1c6e4725a484c0abf10f6eaa1d8d5d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/EditorPrefsWindowService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/EditorStateCache.cs b/Assets/MCPForUnity/Editor/Services/EditorStateCache.cs deleted file mode 100644 index 24fec0f..0000000 --- a/Assets/MCPForUnity/Editor/Services/EditorStateCache.cs +++ /dev/null @@ -1,546 +0,0 @@ -using System; -using System.Reflection; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditorInternal; -using UnityEditor.SceneManagement; -using UnityEngine; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Maintains a cached readiness snapshot (v2) so status reads remain fast even when Unity is busy. - /// Updated on the main thread via Editor callbacks and periodic update ticks. - /// - [InitializeOnLoad] - internal static class EditorStateCache - { - private static readonly object LockObj = new(); - private static long _sequence; - private static long _observedUnixMs; - - private static bool _lastIsCompiling; - private static long? _lastCompileStartedUnixMs; - private static long? _lastCompileFinishedUnixMs; - - private static bool _domainReloadPending; - private static long? _domainReloadBeforeUnixMs; - private static long? _domainReloadAfterUnixMs; - - private static double _lastUpdateTimeSinceStartup; - private const double MinUpdateIntervalSeconds = 1.0; // Reduced frequency: 1s instead of 0.25s - - // State tracking to detect when snapshot actually changes (checked BEFORE building) - private static string _lastTrackedScenePath; - private static string _lastTrackedSceneName; - private static bool _lastTrackedIsFocused; - private static bool _lastTrackedIsPlaying; - private static bool _lastTrackedIsPaused; - private static bool _lastTrackedIsUpdating; - private static bool _lastTrackedTestsRunning; - private static string _lastTrackedActivityPhase; - - private static JObject _cached; - - private sealed class EditorStateSnapshot - { - [JsonProperty("schema_version")] - public string SchemaVersion { get; set; } - - [JsonProperty("observed_at_unix_ms")] - public long ObservedAtUnixMs { get; set; } - - [JsonProperty("sequence")] - public long Sequence { get; set; } - - [JsonProperty("unity")] - public EditorStateUnity Unity { get; set; } - - [JsonProperty("editor")] - public EditorStateEditor Editor { get; set; } - - [JsonProperty("activity")] - public EditorStateActivity Activity { get; set; } - - [JsonProperty("compilation")] - public EditorStateCompilation Compilation { get; set; } - - [JsonProperty("assets")] - public EditorStateAssets Assets { get; set; } - - [JsonProperty("tests")] - public EditorStateTests Tests { get; set; } - - [JsonProperty("transport")] - public EditorStateTransport Transport { get; set; } - } - - private sealed class EditorStateUnity - { - [JsonProperty("instance_id")] - public string InstanceId { get; set; } - - [JsonProperty("unity_version")] - public string UnityVersion { get; set; } - - [JsonProperty("project_id")] - public string ProjectId { get; set; } - - [JsonProperty("platform")] - public string Platform { get; set; } - - [JsonProperty("is_batch_mode")] - public bool? IsBatchMode { get; set; } - } - - private sealed class EditorStateEditor - { - [JsonProperty("is_focused")] - public bool? IsFocused { get; set; } - - [JsonProperty("play_mode")] - public EditorStatePlayMode PlayMode { get; set; } - - [JsonProperty("active_scene")] - public EditorStateActiveScene ActiveScene { get; set; } - } - - private sealed class EditorStatePlayMode - { - [JsonProperty("is_playing")] - public bool? IsPlaying { get; set; } - - [JsonProperty("is_paused")] - public bool? IsPaused { get; set; } - - [JsonProperty("is_changing")] - public bool? IsChanging { get; set; } - } - - private sealed class EditorStateActiveScene - { - [JsonProperty("path")] - public string Path { get; set; } - - [JsonProperty("guid")] - public string Guid { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - } - - private sealed class EditorStateActivity - { - [JsonProperty("phase")] - public string Phase { get; set; } - - [JsonProperty("since_unix_ms")] - public long SinceUnixMs { get; set; } - - [JsonProperty("reasons")] - public string[] Reasons { get; set; } - } - - private sealed class EditorStateCompilation - { - [JsonProperty("is_compiling")] - public bool? IsCompiling { get; set; } - - [JsonProperty("is_domain_reload_pending")] - public bool? IsDomainReloadPending { get; set; } - - [JsonProperty("last_compile_started_unix_ms")] - public long? LastCompileStartedUnixMs { get; set; } - - [JsonProperty("last_compile_finished_unix_ms")] - public long? LastCompileFinishedUnixMs { get; set; } - - [JsonProperty("last_domain_reload_before_unix_ms")] - public long? LastDomainReloadBeforeUnixMs { get; set; } - - [JsonProperty("last_domain_reload_after_unix_ms")] - public long? LastDomainReloadAfterUnixMs { get; set; } - } - - private sealed class EditorStateAssets - { - [JsonProperty("is_updating")] - public bool? IsUpdating { get; set; } - - [JsonProperty("external_changes_dirty")] - public bool? ExternalChangesDirty { get; set; } - - [JsonProperty("external_changes_last_seen_unix_ms")] - public long? ExternalChangesLastSeenUnixMs { get; set; } - - [JsonProperty("external_changes_dirty_since_unix_ms")] - public long? ExternalChangesDirtySinceUnixMs { get; set; } - - [JsonProperty("external_changes_last_cleared_unix_ms")] - public long? ExternalChangesLastClearedUnixMs { get; set; } - - [JsonProperty("refresh")] - public EditorStateRefresh Refresh { get; set; } - } - - private sealed class EditorStateRefresh - { - [JsonProperty("is_refresh_in_progress")] - public bool? IsRefreshInProgress { get; set; } - - [JsonProperty("last_refresh_requested_unix_ms")] - public long? LastRefreshRequestedUnixMs { get; set; } - - [JsonProperty("last_refresh_finished_unix_ms")] - public long? LastRefreshFinishedUnixMs { get; set; } - } - - private sealed class EditorStateTests - { - [JsonProperty("is_running")] - public bool? IsRunning { get; set; } - - [JsonProperty("mode")] - public string Mode { get; set; } - - [JsonProperty("current_job_id")] - public string CurrentJobId { get; set; } - - [JsonProperty("started_unix_ms")] - public long? StartedUnixMs { get; set; } - - [JsonProperty("started_by")] - public string StartedBy { get; set; } - - [JsonProperty("last_run")] - public EditorStateLastRun LastRun { get; set; } - } - - private sealed class EditorStateLastRun - { - [JsonProperty("finished_unix_ms")] - public long? FinishedUnixMs { get; set; } - - [JsonProperty("result")] - public string Result { get; set; } - - [JsonProperty("counts")] - public object Counts { get; set; } - } - - private sealed class EditorStateTransport - { - [JsonProperty("unity_bridge_connected")] - public bool? UnityBridgeConnected { get; set; } - - [JsonProperty("last_message_unix_ms")] - public long? LastMessageUnixMs { get; set; } - } - - static EditorStateCache() - { - try - { - _sequence = 0; - _observedUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - _cached = BuildSnapshot("init"); - - EditorApplication.update += OnUpdate; - EditorApplication.playModeStateChanged += _ => ForceUpdate("playmode"); - - AssemblyReloadEvents.beforeAssemblyReload += () => - { - _domainReloadPending = true; - _domainReloadBeforeUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - ForceUpdate("before_domain_reload"); - }; - AssemblyReloadEvents.afterAssemblyReload += () => - { - _domainReloadPending = false; - _domainReloadAfterUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - ForceUpdate("after_domain_reload"); - }; - } - catch (Exception ex) - { - McpLog.Error($"[EditorStateCache] Failed to initialise: {ex.Message}\n{ex.StackTrace}"); - } - } - - private static void OnUpdate() - { - // Throttle to reduce overhead while keeping the snapshot fresh enough for polling clients. - double now = EditorApplication.timeSinceStartup; - // Use GetActualIsCompiling() to avoid Play mode false positives (issue #582) - bool isCompiling = GetActualIsCompiling(); - - // Check for compilation edge transitions (always update on these) - bool compilationEdge = isCompiling != _lastIsCompiling; - - if (!compilationEdge && now - _lastUpdateTimeSinceStartup < MinUpdateIntervalSeconds) - { - return; - } - - // Fast state-change detection BEFORE building snapshot. - // This avoids the expensive BuildSnapshot() call entirely when nothing changed. - // These checks are much cheaper than building a full JSON snapshot. - var scene = EditorSceneManager.GetActiveScene(); - string scenePath = string.IsNullOrEmpty(scene.path) ? null : scene.path; - string sceneName = scene.name ?? string.Empty; - bool isFocused = InternalEditorUtility.isApplicationActive; - bool isPlaying = EditorApplication.isPlaying; - bool isPaused = EditorApplication.isPaused; - bool isUpdating = EditorApplication.isUpdating; - bool testsRunning = TestRunStatus.IsRunning; - - var activityPhase = "idle"; - if (testsRunning) - { - activityPhase = "running_tests"; - } - else if (isCompiling) - { - activityPhase = "compiling"; - } - else if (_domainReloadPending) - { - activityPhase = "domain_reload"; - } - else if (isUpdating) - { - activityPhase = "asset_import"; - } - else if (EditorApplication.isPlayingOrWillChangePlaymode) - { - activityPhase = "playmode_transition"; - } - - bool hasChanges = compilationEdge - || _lastTrackedScenePath != scenePath - || _lastTrackedSceneName != sceneName - || _lastTrackedIsFocused != isFocused - || _lastTrackedIsPlaying != isPlaying - || _lastTrackedIsPaused != isPaused - || _lastTrackedIsUpdating != isUpdating - || _lastTrackedTestsRunning != testsRunning - || _lastTrackedActivityPhase != activityPhase; - - if (!hasChanges) - { - // No state change - skip the expensive BuildSnapshot entirely. - // This is the key optimization that prevents the 28ms GC spikes. - return; - } - - // Update tracked state - _lastTrackedScenePath = scenePath; - _lastTrackedSceneName = sceneName; - _lastTrackedIsFocused = isFocused; - _lastTrackedIsPlaying = isPlaying; - _lastTrackedIsPaused = isPaused; - _lastTrackedIsUpdating = isUpdating; - _lastTrackedTestsRunning = testsRunning; - _lastTrackedActivityPhase = activityPhase; - - _lastUpdateTimeSinceStartup = now; - ForceUpdate("tick"); - } - - private static void ForceUpdate(string reason) - { - lock (LockObj) - { - _cached = BuildSnapshot(reason); - } - } - - private static JObject BuildSnapshot(string reason) - { - _sequence++; - _observedUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - - bool isCompiling = GetActualIsCompiling(); - if (isCompiling && !_lastIsCompiling) - { - _lastCompileStartedUnixMs = _observedUnixMs; - } - else if (!isCompiling && _lastIsCompiling) - { - _lastCompileFinishedUnixMs = _observedUnixMs; - } - _lastIsCompiling = isCompiling; - - var scene = EditorSceneManager.GetActiveScene(); - string scenePath = string.IsNullOrEmpty(scene.path) ? null : scene.path; - string sceneGuid = !string.IsNullOrEmpty(scenePath) ? AssetDatabase.AssetPathToGUID(scenePath) : null; - - bool testsRunning = TestRunStatus.IsRunning; - var testsMode = TestRunStatus.Mode?.ToString(); - string currentJobId = TestJobManager.CurrentJobId; - bool isFocused = InternalEditorUtility.isApplicationActive; - - var activityPhase = "idle"; - if (testsRunning) - { - activityPhase = "running_tests"; - } - else if (isCompiling) - { - activityPhase = "compiling"; - } - else if (_domainReloadPending) - { - activityPhase = "domain_reload"; - } - else if (EditorApplication.isUpdating) - { - activityPhase = "asset_import"; - } - else if (EditorApplication.isPlayingOrWillChangePlaymode) - { - activityPhase = "playmode_transition"; - } - - var snapshot = new EditorStateSnapshot - { - SchemaVersion = "unity-mcp/editor_state@2", - ObservedAtUnixMs = _observedUnixMs, - Sequence = _sequence, - Unity = new EditorStateUnity - { - InstanceId = null, - UnityVersion = Application.unityVersion, - ProjectId = null, - Platform = Application.platform.ToString(), - IsBatchMode = Application.isBatchMode - }, - Editor = new EditorStateEditor - { - IsFocused = isFocused, - PlayMode = new EditorStatePlayMode - { - IsPlaying = EditorApplication.isPlaying, - IsPaused = EditorApplication.isPaused, - IsChanging = EditorApplication.isPlayingOrWillChangePlaymode - }, - ActiveScene = new EditorStateActiveScene - { - Path = scenePath, - Guid = sceneGuid, - Name = scene.name ?? string.Empty - } - }, - Activity = new EditorStateActivity - { - Phase = activityPhase, - SinceUnixMs = _observedUnixMs, - Reasons = new[] { reason } - }, - Compilation = new EditorStateCompilation - { - IsCompiling = isCompiling, - IsDomainReloadPending = _domainReloadPending, - LastCompileStartedUnixMs = _lastCompileStartedUnixMs, - LastCompileFinishedUnixMs = _lastCompileFinishedUnixMs, - LastDomainReloadBeforeUnixMs = _domainReloadBeforeUnixMs, - LastDomainReloadAfterUnixMs = _domainReloadAfterUnixMs - }, - Assets = new EditorStateAssets - { - IsUpdating = EditorApplication.isUpdating, - ExternalChangesDirty = false, - ExternalChangesLastSeenUnixMs = null, - ExternalChangesDirtySinceUnixMs = null, - ExternalChangesLastClearedUnixMs = null, - Refresh = new EditorStateRefresh - { - IsRefreshInProgress = false, - LastRefreshRequestedUnixMs = null, - LastRefreshFinishedUnixMs = null - } - }, - Tests = new EditorStateTests - { - IsRunning = testsRunning, - Mode = testsMode, - CurrentJobId = string.IsNullOrEmpty(currentJobId) ? null : currentJobId, - StartedUnixMs = TestRunStatus.StartedUnixMs, - StartedBy = "unknown", - LastRun = TestRunStatus.FinishedUnixMs.HasValue - ? new EditorStateLastRun - { - FinishedUnixMs = TestRunStatus.FinishedUnixMs, - Result = "unknown", - Counts = null - } - : null - }, - Transport = new EditorStateTransport - { - UnityBridgeConnected = null, - LastMessageUnixMs = null - } - }; - - return JObject.FromObject(snapshot); - } - - public static JObject GetSnapshot() - { - lock (LockObj) - { - // Defensive: if something went wrong early, rebuild once. - if (_cached == null) - { - _cached = BuildSnapshot("rebuild"); - } - - // Always return a fresh clone to prevent mutation bugs. - // The main GC optimization comes from state-change detection (OnUpdate) - // which prevents unnecessary _cached rebuilds, not from caching the clone. - return (JObject)_cached.DeepClone(); - } - } - - /// - /// Returns the actual compilation state, working around a known Unity quirk where - /// EditorApplication.isCompiling can return false positives in Play mode. - /// See: https://github.com/CoplayDev/unity-mcp/issues/549 - /// - private static bool GetActualIsCompiling() - { - // If EditorApplication.isCompiling is false, Unity is definitely not compiling - if (!EditorApplication.isCompiling) - { - return false; - } - - // In Play mode, EditorApplication.isCompiling can have false positives. - // Double-check with CompilationPipeline.isCompiling via reflection. - if (EditorApplication.isPlaying) - { - try - { - Type pipeline = Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor"); - var prop = pipeline?.GetProperty("isCompiling", BindingFlags.Public | BindingFlags.Static); - if (prop != null) - { - return (bool)prop.GetValue(null); - } - } - catch - { - // If reflection fails, fall back to EditorApplication.isCompiling - } - } - - // Outside Play mode or if reflection failed, trust EditorApplication.isCompiling - return true; - } - } -} - - diff --git a/Assets/MCPForUnity/Editor/Services/EditorStateCache.cs.meta b/Assets/MCPForUnity/Editor/Services/EditorStateCache.cs.meta deleted file mode 100644 index 69478b5..0000000 --- a/Assets/MCPForUnity/Editor/Services/EditorStateCache.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: aa7909967ce3c48c493181c978782a54 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/EditorStateCache.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs b/Assets/MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs deleted file mode 100644 index b0ffd3e..0000000 --- a/Assets/MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.Threading.Tasks; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services.Transport; -using MCPForUnity.Editor.Windows; -using UnityEditor; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Ensures HTTP transports resume after domain reloads similar to the legacy stdio bridge. - /// - [InitializeOnLoad] - internal static class HttpBridgeReloadHandler - { - static HttpBridgeReloadHandler() - { - AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; - AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload; - } - - private static void OnBeforeAssemblyReload() - { - try - { - var transport = MCPServiceLocator.TransportManager; - bool shouldResume = transport.IsRunning(TransportMode.Http); - - if (shouldResume) - { - EditorPrefs.SetBool(EditorPrefKeys.ResumeHttpAfterReload, true); - } - else - { - EditorPrefs.DeleteKey(EditorPrefKeys.ResumeHttpAfterReload); - } - - if (shouldResume) - { - var stopTask = transport.StopAsync(TransportMode.Http); - stopTask.ContinueWith(t => - { - if (t.IsFaulted && t.Exception != null) - { - McpLog.Warn($"Error stopping MCP bridge before reload: {t.Exception.GetBaseException().Message}"); - } - }, TaskScheduler.Default); - } - } - catch (Exception ex) - { - McpLog.Warn($"Failed to evaluate HTTP bridge reload state: {ex.Message}"); - } - } - - private static void OnAfterAssemblyReload() - { - bool resume = false; - try - { - // Only resume HTTP if it is still the selected transport. - bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; - resume = useHttp && EditorPrefs.GetBool(EditorPrefKeys.ResumeHttpAfterReload, false); - if (resume) - { - EditorPrefs.DeleteKey(EditorPrefKeys.ResumeHttpAfterReload); - } - } - catch (Exception ex) - { - McpLog.Warn($"Failed to read HTTP bridge reload flag: {ex.Message}"); - resume = false; - } - - if (!resume) - { - return; - } - - // If the editor is not compiling, attempt an immediate restart without relying on editor focus. - bool isCompiling = EditorApplication.isCompiling; - try - { - var pipeline = Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor"); - var prop = pipeline?.GetProperty("isCompiling", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); - if (prop != null) isCompiling |= (bool)prop.GetValue(null); - } - catch { } - - if (!isCompiling) - { - try - { - var startTask = MCPServiceLocator.TransportManager.StartAsync(TransportMode.Http); - startTask.ContinueWith(t => - { - if (t.IsFaulted) - { - var baseEx = t.Exception?.GetBaseException(); - McpLog.Warn($"Failed to resume HTTP MCP bridge after domain reload: {baseEx?.Message}"); - return; - } - bool started = t.Result; - if (!started) - { - McpLog.Warn("Failed to resume HTTP MCP bridge after domain reload"); - } - else - { - MCPForUnityEditorWindow.RequestHealthVerification(); - } - }, TaskScheduler.Default); - return; - } - catch (Exception ex) - { - McpLog.Error($"Error resuming HTTP MCP bridge: {ex.Message}"); - return; - } - } - - // Fallback when compiling: schedule on the editor loop - EditorApplication.delayCall += async () => - { - try - { - bool started = await MCPServiceLocator.TransportManager.StartAsync(TransportMode.Http); - if (!started) - { - McpLog.Warn("Failed to resume HTTP MCP bridge after domain reload"); - } - else - { - MCPForUnityEditorWindow.RequestHealthVerification(); - } - } - catch (Exception ex) - { - McpLog.Error($"Error resuming HTTP MCP bridge: {ex.Message}"); - } - }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs.meta b/Assets/MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs.meta deleted file mode 100644 index 083c372..0000000 --- a/Assets/MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4c0cf970a7b494a659be151dc0124296 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/IBridgeControlService.cs b/Assets/MCPForUnity/Editor/Services/IBridgeControlService.cs deleted file mode 100644 index 7cc593e..0000000 --- a/Assets/MCPForUnity/Editor/Services/IBridgeControlService.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Threading.Tasks; -using MCPForUnity.Editor.Services.Transport; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Service for controlling the MCP for Unity Bridge connection - /// - public interface IBridgeControlService - { - /// - /// Gets whether the bridge is currently running - /// - bool IsRunning { get; } - - /// - /// Gets the current port the bridge is listening on - /// - int CurrentPort { get; } - - /// - /// Gets whether the bridge is in auto-connect mode - /// - bool IsAutoConnectMode { get; } - - /// - /// Gets the currently active transport mode, if any - /// - TransportMode? ActiveMode { get; } - - /// - /// Starts the MCP for Unity Bridge asynchronously - /// - /// True if the bridge started successfully - Task StartAsync(); - - /// - /// Stops the MCP for Unity Bridge asynchronously - /// - Task StopAsync(); - - /// - /// Verifies the bridge connection by sending a ping and waiting for a pong response - /// - /// The port to verify - /// Verification result with detailed status - BridgeVerificationResult Verify(int port); - - /// - /// Verifies the connection asynchronously (works for both HTTP and stdio transports) - /// - /// Verification result with detailed status - Task VerifyAsync(); - - } - - /// - /// Result of a bridge verification attempt - /// - public class BridgeVerificationResult - { - /// - /// Whether the verification was successful - /// - public bool Success { get; set; } - - /// - /// Human-readable message about the verification result - /// - public string Message { get; set; } - - /// - /// Whether the handshake was valid (FRAMING=1 protocol) - /// - public bool HandshakeValid { get; set; } - - /// - /// Whether the ping/pong exchange succeeded - /// - public bool PingSucceeded { get; set; } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/IBridgeControlService.cs.meta b/Assets/MCPForUnity/Editor/Services/IBridgeControlService.cs.meta deleted file mode 100644 index a517fc2..0000000 --- a/Assets/MCPForUnity/Editor/Services/IBridgeControlService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 6b5d9f677f6f54fc59e6fe921b260c61 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/IBridgeControlService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/IClientConfigurationService.cs b/Assets/MCPForUnity/Editor/Services/IClientConfigurationService.cs deleted file mode 100644 index 6172e8f..0000000 --- a/Assets/MCPForUnity/Editor/Services/IClientConfigurationService.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.Generic; -using MCPForUnity.Editor.Clients; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Service for configuring MCP clients - /// - public interface IClientConfigurationService - { - /// - /// Configures a specific MCP client - /// - /// The client to configure - void ConfigureClient(IMcpClientConfigurator configurator); - - /// - /// Configures all detected/installed MCP clients (skips clients where CLI/tools not found) - /// - /// Summary of configuration results - ClientConfigurationSummary ConfigureAllDetectedClients(); - - /// - /// Checks the configuration status of a client - /// - /// The client to check - /// If true, attempts to auto-fix mismatched paths - /// True if status changed, false otherwise - bool CheckClientStatus(IMcpClientConfigurator configurator, bool attemptAutoRewrite = true); - - /// Gets the registry of discovered configurators. - IReadOnlyList GetAllClients(); - } - - /// - /// Summary of configuration results for multiple clients - /// - public class ClientConfigurationSummary - { - /// - /// Number of clients successfully configured - /// - public int SuccessCount { get; set; } - - /// - /// Number of clients that failed to configure - /// - public int FailureCount { get; set; } - - /// - /// Number of clients skipped (already configured or tool not found) - /// - public int SkippedCount { get; set; } - - /// - /// Detailed messages for each client - /// - public System.Collections.Generic.List Messages { get; set; } = new(); - - /// - /// Gets a human-readable summary message - /// - public string GetSummaryMessage() - { - return $"✓ {SuccessCount} configured, ⚠ {FailureCount} failed, ➜ {SkippedCount} skipped"; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/IClientConfigurationService.cs.meta b/Assets/MCPForUnity/Editor/Services/IClientConfigurationService.cs.meta deleted file mode 100644 index 093fbe9..0000000 --- a/Assets/MCPForUnity/Editor/Services/IClientConfigurationService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: aae139cfae7ac4044ac52e2658005ea1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/IClientConfigurationService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/IPackageDeploymentService.cs b/Assets/MCPForUnity/Editor/Services/IPackageDeploymentService.cs deleted file mode 100644 index 743834c..0000000 --- a/Assets/MCPForUnity/Editor/Services/IPackageDeploymentService.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace MCPForUnity.Editor.Services -{ - public interface IPackageDeploymentService - { - string GetStoredSourcePath(); - void SetStoredSourcePath(string path); - void ClearStoredSourcePath(); - - string GetTargetPath(); - string GetTargetDisplayPath(); - - string GetLastBackupPath(); - bool HasBackup(); - - PackageDeploymentResult DeployFromStoredSource(); - PackageDeploymentResult RestoreLastBackup(); - } - - public class PackageDeploymentResult - { - public bool Success { get; set; } - public string Message { get; set; } - public string SourcePath { get; set; } - public string TargetPath { get; set; } - public string BackupPath { get; set; } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/IPackageDeploymentService.cs.meta b/Assets/MCPForUnity/Editor/Services/IPackageDeploymentService.cs.meta deleted file mode 100644 index 5c9f251..0000000 --- a/Assets/MCPForUnity/Editor/Services/IPackageDeploymentService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 9c7a6f1ce6cd4a8c8a3b5d58d4b760a2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/IPackageDeploymentService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/IPackageUpdateService.cs b/Assets/MCPForUnity/Editor/Services/IPackageUpdateService.cs deleted file mode 100644 index 9d4d2e4..0000000 --- a/Assets/MCPForUnity/Editor/Services/IPackageUpdateService.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace MCPForUnity.Editor.Services -{ - /// - /// Service for checking package updates and version information - /// - public interface IPackageUpdateService - { - /// - /// Checks if a newer version of the package is available - /// - /// The current package version - /// Update check result containing availability and latest version info - UpdateCheckResult CheckForUpdate(string currentVersion); - - /// - /// Compares two version strings to determine if the first is newer than the second - /// - /// First version string - /// Second version string - /// True if version1 is newer than version2 - bool IsNewerVersion(string version1, string version2); - - /// - /// Determines if the package was installed via Git or Asset Store - /// - /// True if installed via Git, false if Asset Store or unknown - bool IsGitInstallation(); - - /// - /// Clears the cached update check data, forcing a fresh check on next request - /// - void ClearCache(); - } - - /// - /// Result of an update check operation - /// - public class UpdateCheckResult - { - /// - /// Whether an update is available - /// - public bool UpdateAvailable { get; set; } - - /// - /// The latest version available (null if check failed or no update) - /// - public string LatestVersion { get; set; } - - /// - /// Whether the check was successful (false if network error, etc.) - /// - public bool CheckSucceeded { get; set; } - - /// - /// Optional message about the check result - /// - public string Message { get; set; } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/IPackageUpdateService.cs.meta b/Assets/MCPForUnity/Editor/Services/IPackageUpdateService.cs.meta deleted file mode 100644 index 2cb86c2..0000000 --- a/Assets/MCPForUnity/Editor/Services/IPackageUpdateService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: e94ae28f193184e4fb5068f62f4f00c6 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/IPackageUpdateService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/IPathResolverService.cs b/Assets/MCPForUnity/Editor/Services/IPathResolverService.cs deleted file mode 100644 index d57581d..0000000 --- a/Assets/MCPForUnity/Editor/Services/IPathResolverService.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace MCPForUnity.Editor.Services -{ - /// - /// Service for resolving paths to required tools and supporting user overrides - /// - public interface IPathResolverService - { - /// - /// Gets the uvx package manager path (respects override if set) - /// - /// Path to the uvx executable, or null if not found - string GetUvxPath(); - - /// - /// Gets the Claude CLI path (respects override if set) - /// - /// Path to the claude executable, or null if not found - string GetClaudeCliPath(); - - /// - /// Checks if Python is detected on the system - /// - /// True if Python is found - bool IsPythonDetected(); - - /// - /// Checks if Claude CLI is detected on the system - /// - /// True if Claude CLI is found - bool IsClaudeCliDetected(); - - /// - /// Sets an override for the uvx path - /// - /// Path to override with - void SetUvxPathOverride(string path); - - /// - /// Sets an override for the Claude CLI path - /// - /// Path to override with - void SetClaudeCliPathOverride(string path); - - /// - /// Clears the uvx path override - /// - void ClearUvxPathOverride(); - - /// - /// Clears the Claude CLI path override - /// - void ClearClaudeCliPathOverride(); - - /// - /// Gets whether a uvx path override is active - /// - bool HasUvxPathOverride { get; } - - /// - /// Gets whether a Claude CLI path override is active - /// - bool HasClaudeCliPathOverride { get; } - - /// - /// Gets whether the uvx path used a fallback from override to system path - /// - bool HasUvxPathFallback { get; } - - /// - /// Validates the provided uv executable by running "--version" and parsing the output. - /// - /// Absolute or relative path to the uv/uvx executable. - /// Parsed version string if successful. - /// True when the executable runs and returns a uv version string. - bool TryValidateUvxExecutable(string uvPath, out string version); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/IPathResolverService.cs.meta b/Assets/MCPForUnity/Editor/Services/IPathResolverService.cs.meta deleted file mode 100644 index 399c724..0000000 --- a/Assets/MCPForUnity/Editor/Services/IPathResolverService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 1e8d388be507345aeb0eaf27fbd3c022 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/IPathResolverService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/IPlatformService.cs b/Assets/MCPForUnity/Editor/Services/IPlatformService.cs deleted file mode 100644 index ec686b2..0000000 --- a/Assets/MCPForUnity/Editor/Services/IPlatformService.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MCPForUnity.Editor.Services -{ - /// - /// Service for platform detection and platform-specific environment access - /// - public interface IPlatformService - { - /// - /// Checks if the current platform is Windows - /// - /// True if running on Windows - bool IsWindows(); - - /// - /// Gets the SystemRoot environment variable (Windows-specific) - /// - /// SystemRoot path, or null if not available - string GetSystemRoot(); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/IPlatformService.cs.meta b/Assets/MCPForUnity/Editor/Services/IPlatformService.cs.meta deleted file mode 100644 index f0ac451..0000000 --- a/Assets/MCPForUnity/Editor/Services/IPlatformService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 1d90ff7f9a1e84c9bbbbedee2f7eda2a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/IPlatformService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs b/Assets/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs deleted file mode 100644 index 6595fc8..0000000 --- a/Assets/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Metadata for a discovered resource - /// - public class ResourceMetadata - { - public string Name { get; set; } - public string Description { get; set; } - public string ClassName { get; set; } - public string Namespace { get; set; } - public string AssemblyName { get; set; } - public bool IsBuiltIn { get; set; } - } - - /// - /// Service for discovering MCP resources via reflection - /// - public interface IResourceDiscoveryService - { - /// - /// Discovers all resources marked with [McpForUnityResource] - /// - List DiscoverAllResources(); - - /// - /// Gets metadata for a specific resource - /// - ResourceMetadata GetResourceMetadata(string resourceName); - - /// - /// Returns only the resources currently enabled - /// - List GetEnabledResources(); - - /// - /// Checks whether a resource is currently enabled - /// - bool IsResourceEnabled(string resourceName); - - /// - /// Updates the enabled state for a resource - /// - void SetResourceEnabled(string resourceName, bool enabled); - - /// - /// Invalidates the resource discovery cache - /// - void InvalidateCache(); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs.meta b/Assets/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs.meta deleted file mode 100644 index 9c0fd0e..0000000 --- a/Assets/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 7afb4739669224c74b4b4d706e6bbb49 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/IResourceDiscoveryService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/IServerManagementService.cs b/Assets/MCPForUnity/Editor/Services/IServerManagementService.cs deleted file mode 100644 index 299fad5..0000000 --- a/Assets/MCPForUnity/Editor/Services/IServerManagementService.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace MCPForUnity.Editor.Services -{ - /// - /// Interface for server management operations - /// - public interface IServerManagementService - { - /// - /// Clear the local uvx cache for the MCP server package - /// - /// True if successful, false otherwise - bool ClearUvxCache(); - - /// - /// Start the local HTTP server in a new terminal window. - /// Stops any existing server on the port and clears the uvx cache first. - /// - /// True if server was started successfully, false otherwise - bool StartLocalHttpServer(); - - /// - /// Stop the local HTTP server by finding the process listening on the configured port - /// - bool StopLocalHttpServer(); - - /// - /// Stop the Unity-managed local HTTP server if a handshake/pidfile exists, - /// even if the current transport selection has changed. - /// - bool StopManagedLocalHttpServer(); - - /// - /// Best-effort detection: returns true if a local MCP HTTP server appears to be running - /// on the configured local URL/port (used to drive UI state even if the session is not active). - /// - bool IsLocalHttpServerRunning(); - - /// - /// Fast reachability check: returns true if a local TCP listener is accepting connections - /// for the configured local URL/port (used for UI state without process inspection). - /// - bool IsLocalHttpServerReachable(); - - /// - /// Attempts to get the command that will be executed when starting the local HTTP server - /// - /// The command that will be executed when available - /// Reason why a command could not be produced - /// True if a command is available, false otherwise - bool TryGetLocalHttpServerCommand(out string command, out string error); - - /// - /// Check if the configured HTTP URL is a local address - /// - /// True if URL is local (localhost, 127.0.0.1, etc.) - bool IsLocalUrl(); - - /// - /// Check if the local HTTP server can be started - /// - /// True if HTTP transport is enabled and URL is local - bool CanStartLocalServer(); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/IServerManagementService.cs.meta b/Assets/MCPForUnity/Editor/Services/IServerManagementService.cs.meta deleted file mode 100644 index 7510a86..0000000 --- a/Assets/MCPForUnity/Editor/Services/IServerManagementService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: d41bfc9780b774affa6afbffd081eb79 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/IServerManagementService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/ITestRunnerService.cs b/Assets/MCPForUnity/Editor/Services/ITestRunnerService.cs deleted file mode 100644 index c24d9e4..0000000 --- a/Assets/MCPForUnity/Editor/Services/ITestRunnerService.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using UnityEditor.TestTools.TestRunner.Api; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Options for filtering which tests to run. - /// All properties are optional - null or empty arrays are ignored. - /// - public class TestFilterOptions - { - /// - /// Full names of specific tests to run (e.g., "MyNamespace.MyTests.TestMethod"). - /// - public string[] TestNames { get; set; } - - /// - /// Same as TestNames, except it allows for Regex. - /// - public string[] GroupNames { get; set; } - - /// - /// NUnit category names to filter by (tests marked with [Category] attribute). - /// - public string[] CategoryNames { get; set; } - - /// - /// Assembly names to filter tests by. - /// - public string[] AssemblyNames { get; set; } - } - - /// - /// Provides access to Unity Test Runner data and execution. - /// - public interface ITestRunnerService - { - /// - /// Retrieve the list of tests for the requested mode(s). - /// When is null, tests for both EditMode and PlayMode are returned. - /// - Task>> GetTestsAsync(TestMode? mode); - - /// - /// Execute tests for the supplied mode with optional filtering. - /// - /// The test mode (EditMode or PlayMode). - /// Optional filter options to run specific tests. Pass null to run all tests. - Task RunTestsAsync(TestMode mode, TestFilterOptions filterOptions = null); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/ITestRunnerService.cs.meta b/Assets/MCPForUnity/Editor/Services/ITestRunnerService.cs.meta deleted file mode 100644 index 18e0e90..0000000 --- a/Assets/MCPForUnity/Editor/Services/ITestRunnerService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: d23bf32361ff444beaf3510818c94bae -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/ITestRunnerService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/IToolDiscoveryService.cs b/Assets/MCPForUnity/Editor/Services/IToolDiscoveryService.cs deleted file mode 100644 index ee2c616..0000000 --- a/Assets/MCPForUnity/Editor/Services/IToolDiscoveryService.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Collections.Generic; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Metadata for a discovered tool - /// - public class ToolMetadata - { - public string Name { get; set; } - public string Description { get; set; } - public bool StructuredOutput { get; set; } - public List Parameters { get; set; } - public string ClassName { get; set; } - public string Namespace { get; set; } - public string AssemblyName { get; set; } - public bool AutoRegister { get; set; } = true; - public bool RequiresPolling { get; set; } = false; - public string PollAction { get; set; } = "status"; - public bool IsBuiltIn { get; set; } - } - - /// - /// Metadata for a tool parameter - /// - public class ParameterMetadata - { - public string Name { get; set; } - public string Description { get; set; } - public string Type { get; set; } // "string", "int", "bool", "float", etc. - public bool Required { get; set; } - public string DefaultValue { get; set; } - } - - /// - /// Service for discovering MCP tools via reflection - /// - public interface IToolDiscoveryService - { - /// - /// Discovers all tools marked with [McpForUnityTool] - /// - List DiscoverAllTools(); - - /// - /// Gets metadata for a specific tool - /// - ToolMetadata GetToolMetadata(string toolName); - - /// - /// Returns only the tools currently enabled for registration - /// - List GetEnabledTools(); - - /// - /// Checks whether a tool is currently enabled for registration - /// - bool IsToolEnabled(string toolName); - - /// - /// Updates the enabled state for a tool - /// - void SetToolEnabled(string toolName, bool enabled); - - /// - /// Invalidates the tool discovery cache - /// - void InvalidateCache(); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/IToolDiscoveryService.cs.meta b/Assets/MCPForUnity/Editor/Services/IToolDiscoveryService.cs.meta deleted file mode 100644 index 663f9f0..0000000 --- a/Assets/MCPForUnity/Editor/Services/IToolDiscoveryService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 497592a93fd994b2cb9803e7c8636ff7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/IToolDiscoveryService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/MCPServiceLocator.cs b/Assets/MCPForUnity/Editor/Services/MCPServiceLocator.cs deleted file mode 100644 index c8ceb4d..0000000 --- a/Assets/MCPForUnity/Editor/Services/MCPServiceLocator.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services.Transport; -using MCPForUnity.Editor.Services.Transport.Transports; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Service locator for accessing MCP services without dependency injection - /// - public static class MCPServiceLocator - { - private static IBridgeControlService _bridgeService; - private static IClientConfigurationService _clientService; - private static IPathResolverService _pathService; - private static ITestRunnerService _testRunnerService; - private static IPackageUpdateService _packageUpdateService; - private static IPlatformService _platformService; - private static IToolDiscoveryService _toolDiscoveryService; - private static IResourceDiscoveryService _resourceDiscoveryService; - private static IServerManagementService _serverManagementService; - private static TransportManager _transportManager; - private static IPackageDeploymentService _packageDeploymentService; - - public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService(); - public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService(); - public static IPathResolverService Paths => _pathService ??= new PathResolverService(); - public static ITestRunnerService Tests => _testRunnerService ??= new TestRunnerService(); - public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService(); - public static IPlatformService Platform => _platformService ??= new PlatformService(); - public static IToolDiscoveryService ToolDiscovery => _toolDiscoveryService ??= new ToolDiscoveryService(); - public static IResourceDiscoveryService ResourceDiscovery => _resourceDiscoveryService ??= new ResourceDiscoveryService(); - public static IServerManagementService Server => _serverManagementService ??= new ServerManagementService(); - public static TransportManager TransportManager => _transportManager ??= new TransportManager(); - public static IPackageDeploymentService Deployment => _packageDeploymentService ??= new PackageDeploymentService(); - - /// - /// Registers a custom implementation for a service (useful for testing) - /// - /// The service interface type - /// The implementation to register - public static void Register(T implementation) where T : class - { - if (implementation is IBridgeControlService b) - _bridgeService = b; - else if (implementation is IClientConfigurationService c) - _clientService = c; - else if (implementation is IPathResolverService p) - _pathService = p; - else if (implementation is ITestRunnerService t) - _testRunnerService = t; - else if (implementation is IPackageUpdateService pu) - _packageUpdateService = pu; - else if (implementation is IPlatformService ps) - _platformService = ps; - else if (implementation is IToolDiscoveryService td) - _toolDiscoveryService = td; - else if (implementation is IResourceDiscoveryService rd) - _resourceDiscoveryService = rd; - else if (implementation is IServerManagementService sm) - _serverManagementService = sm; - else if (implementation is IPackageDeploymentService pd) - _packageDeploymentService = pd; - else if (implementation is TransportManager tm) - _transportManager = tm; - } - - /// - /// Resets all services to their default implementations (useful for testing) - /// - public static void Reset() - { - (_bridgeService as IDisposable)?.Dispose(); - (_clientService as IDisposable)?.Dispose(); - (_pathService as IDisposable)?.Dispose(); - (_testRunnerService as IDisposable)?.Dispose(); - (_packageUpdateService as IDisposable)?.Dispose(); - (_platformService as IDisposable)?.Dispose(); - (_toolDiscoveryService as IDisposable)?.Dispose(); - (_resourceDiscoveryService as IDisposable)?.Dispose(); - (_serverManagementService as IDisposable)?.Dispose(); - (_transportManager as IDisposable)?.Dispose(); - (_packageDeploymentService as IDisposable)?.Dispose(); - - _bridgeService = null; - _clientService = null; - _pathService = null; - _testRunnerService = null; - _packageUpdateService = null; - _platformService = null; - _toolDiscoveryService = null; - _resourceDiscoveryService = null; - _serverManagementService = null; - _transportManager = null; - _packageDeploymentService = null; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/MCPServiceLocator.cs.meta b/Assets/MCPForUnity/Editor/Services/MCPServiceLocator.cs.meta deleted file mode 100644 index 0e784e8..0000000 --- a/Assets/MCPForUnity/Editor/Services/MCPServiceLocator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 276d6a9f9a1714ead91573945de78992 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/MCPServiceLocator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs b/Assets/MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs deleted file mode 100644 index 9b4bd61..0000000 --- a/Assets/MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Threading.Tasks; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services.Transport; -using UnityEditor; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Best-effort cleanup when the Unity Editor is quitting. - /// - Stops active transports so clients don't see a "hung" session longer than necessary. - /// - If HTTP Local is selected, attempts to stop the local HTTP server (guarded by PID heuristics). - /// - [InitializeOnLoad] - internal static class McpEditorShutdownCleanup - { - static McpEditorShutdownCleanup() - { - // Guard against duplicate subscriptions across domain reloads. - try { EditorApplication.quitting -= OnEditorQuitting; } catch { } - EditorApplication.quitting += OnEditorQuitting; - } - - private static void OnEditorQuitting() - { - // 1) Stop transports (best-effort, bounded wait). - try - { - var transport = MCPServiceLocator.TransportManager; - - Task stopHttp = transport.StopAsync(TransportMode.Http); - Task stopStdio = transport.StopAsync(TransportMode.Stdio); - - try { Task.WaitAll(new[] { stopHttp, stopStdio }, 750); } catch { } - } - catch (Exception ex) - { - // Avoid hard failures on quit. - McpLog.Warn($"Shutdown cleanup: failed to stop transports: {ex.Message}"); - } - - // 2) Stop local HTTP server if it was Unity-managed (best-effort). - try - { - bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; - string scope = string.Empty; - try { scope = EditorPrefs.GetString(EditorPrefKeys.HttpTransportScope, string.Empty); } catch { } - - bool stopped = false; - bool httpLocalSelected = - useHttp && - (string.Equals(scope, "local", StringComparison.OrdinalIgnoreCase) - || (string.IsNullOrEmpty(scope) && MCPServiceLocator.Server.IsLocalUrl())); - - if (httpLocalSelected) - { - // StopLocalHttpServer is already guarded to only terminate processes that look like mcp-for-unity. - // If it refuses to stop (e.g. URL was edited away from local), fall back to the Unity-managed stop. - stopped = MCPServiceLocator.Server.StopLocalHttpServer(); - } - - // Always attempt to stop a Unity-managed server if one exists. - // This covers cases where the user switched transports (e.g. to stdio) or StopLocalHttpServer refused. - if (!stopped) - { - MCPServiceLocator.Server.StopManagedLocalHttpServer(); - } - } - catch (Exception ex) - { - McpLog.Warn($"Shutdown cleanup: failed to stop local HTTP server: {ex.Message}"); - } - } - } -} - diff --git a/Assets/MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs.meta b/Assets/MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs.meta deleted file mode 100644 index f053545..0000000 --- a/Assets/MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4150c04e0907c45d7b332260911a0567 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/PackageDeploymentService.cs b/Assets/MCPForUnity/Editor/Services/PackageDeploymentService.cs deleted file mode 100644 index 7c0e27c..0000000 --- a/Assets/MCPForUnity/Editor/Services/PackageDeploymentService.cs +++ /dev/null @@ -1,304 +0,0 @@ -using System; -using System.IO; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using UnityEditor; -using UnityEngine; -using PackageInfo = UnityEditor.PackageManager.PackageInfo; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Handles copying a local MCPForUnity folder into the current project's package location with backup/restore support. - /// - public class PackageDeploymentService : IPackageDeploymentService - { - private const string BackupRootFolderName = "MCPForUnityDeployBackups"; - - public string GetStoredSourcePath() - { - return EditorPrefs.GetString(EditorPrefKeys.PackageDeploySourcePath, string.Empty); - } - - public void SetStoredSourcePath(string path) - { - ValidateSource(path); - EditorPrefs.SetString(EditorPrefKeys.PackageDeploySourcePath, Path.GetFullPath(path)); - } - - public void ClearStoredSourcePath() - { - EditorPrefs.DeleteKey(EditorPrefKeys.PackageDeploySourcePath); - } - - public string GetTargetPath() - { - // Prefer Package Manager resolved path for the installed package - var packageInfo = PackageInfo.FindForAssembly(typeof(PackageDeploymentService).Assembly); - if (packageInfo != null) - { - if (!string.IsNullOrEmpty(packageInfo.resolvedPath) && Directory.Exists(packageInfo.resolvedPath)) - { - return packageInfo.resolvedPath; - } - - if (!string.IsNullOrEmpty(packageInfo.assetPath)) - { - string absoluteFromAsset = MakeAbsolute(packageInfo.assetPath); - if (Directory.Exists(absoluteFromAsset)) - { - return absoluteFromAsset; - } - } - } - - // Fallback to computed package root - string packageRoot = AssetPathUtility.GetMcpPackageRootPath(); - if (!string.IsNullOrEmpty(packageRoot)) - { - string absolutePath = MakeAbsolute(packageRoot); - if (Directory.Exists(absolutePath)) - { - return absolutePath; - } - } - - return null; - } - - public string GetTargetDisplayPath() - { - string target = GetTargetPath(); - if (string.IsNullOrEmpty(target)) - return "Not found (check Packages/manifest.json)"; - // Use forward slashes to avoid backslash escape sequence issues in UI text - return target.Replace('\\', '/'); - } - - public string GetLastBackupPath() - { - return EditorPrefs.GetString(EditorPrefKeys.PackageDeployLastBackupPath, string.Empty); - } - - public bool HasBackup() - { - string path = GetLastBackupPath(); - return !string.IsNullOrEmpty(path) && Directory.Exists(path); - } - - public PackageDeploymentResult DeployFromStoredSource() - { - string sourcePath = GetStoredSourcePath(); - if (string.IsNullOrEmpty(sourcePath)) - { - return Fail("Select a MCPForUnity folder first."); - } - - string validationError = ValidateSource(sourcePath, throwOnError: false); - if (!string.IsNullOrEmpty(validationError)) - { - return Fail(validationError); - } - - string targetPath = GetTargetPath(); - if (string.IsNullOrEmpty(targetPath)) - { - return Fail("Could not locate the installed MCP package. Check Packages/manifest.json."); - } - - if (PathsEqual(sourcePath, targetPath)) - { - return Fail("Source and target are the same. Choose a different MCPForUnity folder."); - } - - try - { - EditorUtility.DisplayProgressBar("Deploy MCP for Unity", "Creating backup...", 0.25f); - string backupPath = CreateBackup(targetPath); - - EditorUtility.DisplayProgressBar("Deploy MCP for Unity", "Replacing package contents...", 0.7f); - CopyCoreFolders(sourcePath, targetPath); - - EditorPrefs.SetString(EditorPrefKeys.PackageDeployLastBackupPath, backupPath); - EditorPrefs.SetString(EditorPrefKeys.PackageDeployLastTargetPath, targetPath); - EditorPrefs.SetString(EditorPrefKeys.PackageDeployLastSourcePath, sourcePath); - - AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); - return Success("Deployment completed.", sourcePath, targetPath, backupPath); - } - catch (Exception ex) - { - McpLog.Error($"Deployment failed: {ex.Message}"); - return Fail($"Deployment failed: {ex.Message}"); - } - finally - { - EditorUtility.ClearProgressBar(); - } - } - - public PackageDeploymentResult RestoreLastBackup() - { - string backupPath = GetLastBackupPath(); - string targetPath = EditorPrefs.GetString(EditorPrefKeys.PackageDeployLastTargetPath, string.Empty); - - if (string.IsNullOrEmpty(backupPath) || !Directory.Exists(backupPath)) - { - return Fail("No backup available to restore."); - } - - if (string.IsNullOrEmpty(targetPath) || !Directory.Exists(targetPath)) - { - targetPath = GetTargetPath(); - } - - if (string.IsNullOrEmpty(targetPath) || !Directory.Exists(targetPath)) - { - return Fail("Could not locate target package path."); - } - - try - { - EditorUtility.DisplayProgressBar("Restore MCP for Unity", "Restoring backup...", 0.5f); - ReplaceDirectory(backupPath, targetPath); - - AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); - return Success("Restore completed.", null, targetPath, backupPath); - } - catch (Exception ex) - { - McpLog.Error($"Restore failed: {ex.Message}"); - return Fail($"Restore failed: {ex.Message}"); - } - finally - { - EditorUtility.ClearProgressBar(); - } - } - - private void CopyCoreFolders(string sourceRoot, string targetRoot) - { - string sourceEditor = Path.Combine(sourceRoot, "Editor"); - string sourceRuntime = Path.Combine(sourceRoot, "Runtime"); - - ReplaceDirectory(sourceEditor, Path.Combine(targetRoot, "Editor")); - ReplaceDirectory(sourceRuntime, Path.Combine(targetRoot, "Runtime")); - } - - private static void ReplaceDirectory(string source, string destination) - { - if (Directory.Exists(destination)) - { - FileUtil.DeleteFileOrDirectory(destination); - } - - FileUtil.CopyFileOrDirectory(source, destination); - } - - private string CreateBackup(string targetPath) - { - string backupRoot = Path.Combine(GetProjectRoot(), "Library", BackupRootFolderName); - Directory.CreateDirectory(backupRoot); - - string stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - string backupPath = Path.Combine(backupRoot, $"backup_{stamp}"); - - if (Directory.Exists(backupPath)) - { - FileUtil.DeleteFileOrDirectory(backupPath); - } - - FileUtil.CopyFileOrDirectory(targetPath, backupPath); - return backupPath; - } - - private static string ValidateSource(string sourcePath, bool throwOnError = true) - { - if (string.IsNullOrEmpty(sourcePath)) - { - if (throwOnError) - { - throw new ArgumentException("Source path cannot be empty."); - } - - return "Source path is empty."; - } - - if (!Directory.Exists(sourcePath)) - { - if (throwOnError) - { - throw new ArgumentException("Selected folder does not exist."); - } - - return "Selected folder does not exist."; - } - - bool hasEditor = Directory.Exists(Path.Combine(sourcePath, "Editor")); - bool hasRuntime = Directory.Exists(Path.Combine(sourcePath, "Runtime")); - - if (!hasEditor || !hasRuntime) - { - string message = "Folder must contain Editor and Runtime subfolders."; - if (throwOnError) - { - throw new ArgumentException(message); - } - - return message; - } - - return null; - } - - private static string MakeAbsolute(string assetPath) - { - assetPath = assetPath.Replace('\\', '/'); - - if (assetPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) - { - return Path.GetFullPath(Path.Combine(Application.dataPath, "..", assetPath)); - } - - if (assetPath.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase)) - { - return Path.GetFullPath(Path.Combine(Application.dataPath, "..", assetPath)); - } - - return Path.GetFullPath(assetPath); - } - - private static string GetProjectRoot() - { - return Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - } - - private static bool PathsEqual(string a, string b) - { - string normA = Path.GetFullPath(a).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - string normB = Path.GetFullPath(b).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - return string.Equals(normA, normB, StringComparison.OrdinalIgnoreCase); - } - - private static PackageDeploymentResult Success(string message, string source, string target, string backup) - { - return new PackageDeploymentResult - { - Success = true, - Message = message, - SourcePath = source, - TargetPath = target, - BackupPath = backup - }; - } - - private static PackageDeploymentResult Fail(string message) - { - return new PackageDeploymentResult - { - Success = false, - Message = message - }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/PackageDeploymentService.cs.meta b/Assets/MCPForUnity/Editor/Services/PackageDeploymentService.cs.meta deleted file mode 100644 index 24a0982..0000000 --- a/Assets/MCPForUnity/Editor/Services/PackageDeploymentService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 0b1f45e4e5d24413a6f1c8c0d8c5f2f1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/PackageDeploymentService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/PackageUpdateService.cs b/Assets/MCPForUnity/Editor/Services/PackageUpdateService.cs deleted file mode 100644 index 81c0161..0000000 --- a/Assets/MCPForUnity/Editor/Services/PackageUpdateService.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.Net; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Service for checking package updates from GitHub or Asset Store metadata - /// - public class PackageUpdateService : IPackageUpdateService - { - private const string LastCheckDateKey = EditorPrefKeys.LastUpdateCheck; - private const string CachedVersionKey = EditorPrefKeys.LatestKnownVersion; - private const string LastAssetStoreCheckDateKey = EditorPrefKeys.LastAssetStoreUpdateCheck; - private const string CachedAssetStoreVersionKey = EditorPrefKeys.LatestKnownAssetStoreVersion; - private const string PackageJsonUrl = "https://raw.githubusercontent.com/CoplayDev/unity-mcp/main/MCPForUnity/package.json"; - private const string AssetStoreVersionUrl = "https://gqoqjkkptwfbkwyssmnj.supabase.co/storage/v1/object/public/coplay-images/assetstoreversion.json"; - - /// - public UpdateCheckResult CheckForUpdate(string currentVersion) - { - bool isGitInstallation = IsGitInstallation(); - string lastCheckDate = EditorPrefs.GetString(isGitInstallation ? LastCheckDateKey : LastAssetStoreCheckDateKey, ""); - string cachedLatestVersion = EditorPrefs.GetString(isGitInstallation ? CachedVersionKey : CachedAssetStoreVersionKey, ""); - - if (lastCheckDate == DateTime.Now.ToString("yyyy-MM-dd") && !string.IsNullOrEmpty(cachedLatestVersion)) - { - return new UpdateCheckResult - { - CheckSucceeded = true, - LatestVersion = cachedLatestVersion, - UpdateAvailable = IsNewerVersion(cachedLatestVersion, currentVersion), - Message = "Using cached version check" - }; - } - - string latestVersion = isGitInstallation - ? FetchLatestVersionFromGitHub() - : FetchLatestVersionFromAssetStoreJson(); - - if (!string.IsNullOrEmpty(latestVersion)) - { - // Cache the result - EditorPrefs.SetString(isGitInstallation ? LastCheckDateKey : LastAssetStoreCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd")); - EditorPrefs.SetString(isGitInstallation ? CachedVersionKey : CachedAssetStoreVersionKey, latestVersion); - - return new UpdateCheckResult - { - CheckSucceeded = true, - LatestVersion = latestVersion, - UpdateAvailable = IsNewerVersion(latestVersion, currentVersion), - Message = "Successfully checked for updates" - }; - } - - return new UpdateCheckResult - { - CheckSucceeded = false, - UpdateAvailable = false, - Message = isGitInstallation - ? "Failed to check for updates (network issue or offline)" - : "Failed to check for Asset Store updates (network issue or offline)" - }; - } - - /// - public bool IsNewerVersion(string version1, string version2) - { - try - { - // Remove any "v" prefix - version1 = version1.TrimStart('v', 'V'); - version2 = version2.TrimStart('v', 'V'); - - var version1Parts = version1.Split('.'); - var version2Parts = version2.Split('.'); - - for (int i = 0; i < Math.Min(version1Parts.Length, version2Parts.Length); i++) - { - if (int.TryParse(version1Parts[i], out int v1Num) && - int.TryParse(version2Parts[i], out int v2Num)) - { - if (v1Num > v2Num) return true; - if (v1Num < v2Num) return false; - } - } - return false; - } - catch - { - return false; - } - } - - /// - public virtual bool IsGitInstallation() - { - // Git packages are installed via Package Manager and have a package.json in Packages/ - // Asset Store packages are in Assets/ - string packageRoot = AssetPathUtility.GetMcpPackageRootPath(); - - if (string.IsNullOrEmpty(packageRoot)) - { - return false; - } - - // If the package is in Packages/ it's a PM install (likely Git) - // If it's in Assets/ it's an Asset Store install - return packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase); - } - - /// - public void ClearCache() - { - EditorPrefs.DeleteKey(LastCheckDateKey); - EditorPrefs.DeleteKey(CachedVersionKey); - EditorPrefs.DeleteKey(LastAssetStoreCheckDateKey); - EditorPrefs.DeleteKey(CachedAssetStoreVersionKey); - } - - /// - /// Fetches the latest version from GitHub's main branch package.json - /// - protected virtual string FetchLatestVersionFromGitHub() - { - try - { - // GitHub API endpoint (Option 1 - has rate limits): - // https://api.github.com/repos/CoplayDev/unity-mcp/releases/latest - // - // We use Option 2 (package.json directly) because: - // - No API rate limits (GitHub serves raw files freely) - // - Simpler - just parse JSON for version field - // - More reliable - doesn't require releases to be published - // - Direct source of truth from the main branch - - using (var client = new WebClient()) - { - client.Headers.Add("User-Agent", "Unity-MCPForUnity-UpdateChecker"); - string jsonContent = client.DownloadString(PackageJsonUrl); - - var packageJson = JObject.Parse(jsonContent); - string version = packageJson["version"]?.ToString(); - - return string.IsNullOrEmpty(version) ? null : version; - } - } - catch (Exception ex) - { - // Silent fail - don't interrupt the user if network is unavailable - McpLog.Info($"Update check failed (this is normal if offline): {ex.Message}"); - return null; - } - } - - /// - /// Fetches the latest Asset Store version from a hosted JSON file. - /// - protected virtual string FetchLatestVersionFromAssetStoreJson() - { - try - { - using (var client = new WebClient()) - { - client.Headers.Add("User-Agent", "Unity-MCPForUnity-AssetStoreUpdateChecker"); - string jsonContent = client.DownloadString(AssetStoreVersionUrl); - - var versionJson = JObject.Parse(jsonContent); - string version = versionJson["version"]?.ToString(); - - return string.IsNullOrEmpty(version) ? null : version; - } - } - catch (Exception ex) - { - // Silent fail - don't interrupt the user if network is unavailable - McpLog.Info($"Asset Store update check failed (this is normal if offline): {ex.Message}"); - return null; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/PackageUpdateService.cs.meta b/Assets/MCPForUnity/Editor/Services/PackageUpdateService.cs.meta deleted file mode 100644 index fd46cab..0000000 --- a/Assets/MCPForUnity/Editor/Services/PackageUpdateService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 7c3c2304b14e9485ca54182fad73b035 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/PackageUpdateService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/PathResolverService.cs b/Assets/MCPForUnity/Editor/Services/PathResolverService.cs deleted file mode 100644 index 4181cf4..0000000 --- a/Assets/MCPForUnity/Editor/Services/PathResolverService.cs +++ /dev/null @@ -1,358 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Implementation of path resolver service with override support - /// - public class PathResolverService : IPathResolverService - { - private bool _hasUvxPathFallback; - - public bool HasUvxPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, null)); - public bool HasClaudeCliPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(EditorPrefKeys.ClaudeCliPathOverride, null)); - public bool HasUvxPathFallback => _hasUvxPathFallback; - - public string GetUvxPath() - { - // Reset fallback flag at the start of each resolution - _hasUvxPathFallback = false; - - // Check override first - only validate if explicitly set - if (HasUvxPathOverride) - { - string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty); - // Validate the override - if invalid, fall back to system discovery - if (TryValidateUvxExecutable(overridePath, out string version)) - { - return overridePath; - } - // Override is set but invalid - fall back to system discovery - string fallbackPath = ResolveUvxFromSystem(); - if (!string.IsNullOrEmpty(fallbackPath)) - { - _hasUvxPathFallback = true; - return fallbackPath; - } - // Return null to indicate override is invalid and no system fallback found - return null; - } - - // No override set - try discovery (uvx first, then uv) - string discovered = ResolveUvxFromSystem(); - if (!string.IsNullOrEmpty(discovered)) - { - return discovered; - } - - // Fallback to bare command - return "uvx"; - } - - /// - /// Resolves uv/uvx from system by trying both commands. - /// Returns the full path if found, null otherwise. - /// - private static string ResolveUvxFromSystem() - { - try - { - // Try uvx first, then uv - string[] commandNames = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? new[] { "uvx.exe", "uv.exe" } - : new[] { "uvx", "uv" }; - - foreach (string commandName in commandNames) - { - foreach (string candidate in EnumerateCommandCandidates(commandName)) - { - if (!string.IsNullOrEmpty(candidate) && File.Exists(candidate)) - { - return candidate; - } - } - } - } - catch (Exception ex) - { - McpLog.Debug($"PathResolver error: {ex.Message}"); - } - - return null; - } - - - - public string GetClaudeCliPath() - { - // Check override first - only validate if explicitly set - if (HasClaudeCliPathOverride) - { - string overridePath = EditorPrefs.GetString(EditorPrefKeys.ClaudeCliPathOverride, string.Empty); - // Validate the override - if invalid, don't fall back to discovery - if (File.Exists(overridePath)) - { - return overridePath; - } - // Override is set but invalid - return null (no fallback) - return null; - } - - // No override - use platform-specific discovery - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - string[] candidates = new[] - { - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "claude", "claude.exe"), - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "claude", "claude.exe"), - "claude.exe" - }; - - foreach (var c in candidates) - { - if (File.Exists(c)) return c; - } - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - string[] candidates = new[] - { - "/opt/homebrew/bin/claude", - "/usr/local/bin/claude", - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude") - }; - - foreach (var c in candidates) - { - if (File.Exists(c)) return c; - } - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - string[] candidates = new[] - { - "/usr/bin/claude", - "/usr/local/bin/claude", - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude") - }; - - foreach (var c in candidates) - { - if (File.Exists(c)) return c; - } - } - - return null; - } - - public bool IsPythonDetected() - { - return ExecPath.TryRun( - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "python.exe" : "python3", - "--version", - null, - out _, - out _, - 2000); - } - - public bool IsClaudeCliDetected() - { - return !string.IsNullOrEmpty(GetClaudeCliPath()); - } - - public void SetUvxPathOverride(string path) - { - if (string.IsNullOrEmpty(path)) - { - ClearUvxPathOverride(); - return; - } - - if (!File.Exists(path)) - { - throw new ArgumentException("The selected uvx executable does not exist"); - } - - EditorPrefs.SetString(EditorPrefKeys.UvxPathOverride, path); - } - - public void SetClaudeCliPathOverride(string path) - { - if (string.IsNullOrEmpty(path)) - { - ClearClaudeCliPathOverride(); - return; - } - - if (!File.Exists(path)) - { - throw new ArgumentException("The selected Claude CLI executable does not exist"); - } - - EditorPrefs.SetString(EditorPrefKeys.ClaudeCliPathOverride, path); - } - - public void ClearUvxPathOverride() - { - EditorPrefs.DeleteKey(EditorPrefKeys.UvxPathOverride); - } - - public void ClearClaudeCliPathOverride() - { - EditorPrefs.DeleteKey(EditorPrefKeys.ClaudeCliPathOverride); - } - - /// - /// Validates the provided uv executable by running "--version" and parsing the output. - /// - /// Absolute or relative path to the uv/uvx executable. - /// Parsed version string if successful. - /// True when the executable runs and returns a uvx version string. - public bool TryValidateUvxExecutable(string uvxPath, out string version) - { - version = null; - - if (string.IsNullOrEmpty(uvxPath)) - return false; - - try - { - // Check if the path is just a command name (no directory separator) - bool isBareCommand = !uvxPath.Contains('/') && !uvxPath.Contains('\\'); - - if (isBareCommand) - { - // For bare commands like "uvx" or "uv", use EnumerateCommandCandidates to find full path first - string fullPath = FindUvxExecutableInPath(uvxPath); - if (string.IsNullOrEmpty(fullPath)) - return false; - uvxPath = fullPath; - } - - // Use ExecPath.TryRun which properly handles async output reading and timeouts - if (!ExecPath.TryRun(uvxPath, "--version", null, out string stdout, out string stderr, 5000)) - return false; - - // Check stdout first, then stderr (some tools output to stderr) - string versionOutput = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim(); - - // uv/uvx outputs "uv x.y.z" or "uvx x.y.z", extract version number - if (versionOutput.StartsWith("uvx ") || versionOutput.StartsWith("uv ")) - { - // Extract version: "uv 0.9.18 (hash date)" -> "0.9.18" - int spaceIndex = versionOutput.IndexOf(' '); - if (spaceIndex >= 0) - { - string afterCommand = versionOutput.Substring(spaceIndex + 1).Trim(); - // Version is up to the first space or parenthesis - int nextSpace = afterCommand.IndexOf(' '); - int parenIndex = afterCommand.IndexOf('('); - int endIndex = Math.Min( - nextSpace >= 0 ? nextSpace : int.MaxValue, - parenIndex >= 0 ? parenIndex : int.MaxValue - ); - version = endIndex < int.MaxValue ? afterCommand.Substring(0, endIndex).Trim() : afterCommand; - return true; - } - } - } - catch - { - // Ignore validation errors - } - - return false; - } - - private string FindUvxExecutableInPath(string commandName) - { - try - { - // Generic search for any command in PATH and common locations - foreach (string candidate in EnumerateCommandCandidates(commandName)) - { - if (!string.IsNullOrEmpty(candidate) && File.Exists(candidate)) - { - return candidate; - } - } - } - catch - { - // Ignore errors - } - - return null; - } - - /// - /// Enumerates candidate paths for a generic command name. - /// Searches PATH and common locations. - /// - private static IEnumerable EnumerateCommandCandidates(string commandName) - { - string exeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !commandName.EndsWith(".exe") - ? commandName + ".exe" - : commandName; - - // Search PATH first - string pathEnv = Environment.GetEnvironmentVariable("PATH"); - if (!string.IsNullOrEmpty(pathEnv)) - { - foreach (string rawDir in pathEnv.Split(Path.PathSeparator)) - { - if (string.IsNullOrWhiteSpace(rawDir)) continue; - string dir = rawDir.Trim(); - yield return Path.Combine(dir, exeName); - } - } - - // User-local binary directories - string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - if (!string.IsNullOrEmpty(home)) - { - yield return Path.Combine(home, ".local", "bin", exeName); - yield return Path.Combine(home, ".cargo", "bin", exeName); - } - - // System directories (platform-specific) - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - yield return "/opt/homebrew/bin/" + exeName; - yield return "/usr/local/bin/" + exeName; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - yield return "/usr/local/bin/" + exeName; - yield return "/usr/bin/" + exeName; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - - if (!string.IsNullOrEmpty(localAppData)) - { - yield return Path.Combine(localAppData, "Programs", "uv", exeName); - // WinGet creates shim files in this location - yield return Path.Combine(localAppData, "Microsoft", "WinGet", "Links", exeName); - } - - if (!string.IsNullOrEmpty(programFiles)) - { - yield return Path.Combine(programFiles, "uv", exeName); - } - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/PathResolverService.cs.meta b/Assets/MCPForUnity/Editor/Services/PathResolverService.cs.meta deleted file mode 100644 index 1a35323..0000000 --- a/Assets/MCPForUnity/Editor/Services/PathResolverService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 00a6188fd15a847fa8cc7cb7a4ce3dce -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/PathResolverService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/PlatformService.cs b/Assets/MCPForUnity/Editor/Services/PlatformService.cs deleted file mode 100644 index 6e66371..0000000 --- a/Assets/MCPForUnity/Editor/Services/PlatformService.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Default implementation of platform detection service - /// - public class PlatformService : IPlatformService - { - /// - /// Checks if the current platform is Windows - /// - /// True if running on Windows - public bool IsWindows() - { - return Environment.OSVersion.Platform == PlatformID.Win32NT; - } - - /// - /// Gets the SystemRoot environment variable (Windows-specific) - /// - /// SystemRoot path, or "C:\\Windows" as fallback on Windows, null on other platforms - public string GetSystemRoot() - { - if (!IsWindows()) - return null; - - return Environment.GetEnvironmentVariable("SystemRoot") ?? "C:\\Windows"; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/PlatformService.cs.meta b/Assets/MCPForUnity/Editor/Services/PlatformService.cs.meta deleted file mode 100644 index 3b5fa83..0000000 --- a/Assets/MCPForUnity/Editor/Services/PlatformService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 3b2d7f32a595c45dd8c01f141c69761c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/PlatformService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs b/Assets/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs deleted file mode 100644 index d964259..0000000 --- a/Assets/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Resources; -using UnityEditor; - -namespace MCPForUnity.Editor.Services -{ - public class ResourceDiscoveryService : IResourceDiscoveryService - { - private Dictionary _cachedResources; - - public List DiscoverAllResources() - { - if (_cachedResources != null) - { - return _cachedResources.Values.ToList(); - } - - _cachedResources = new Dictionary(); - - var resourceTypes = TypeCache.GetTypesWithAttribute(); - foreach (var type in resourceTypes) - { - McpForUnityResourceAttribute resourceAttr; - try - { - resourceAttr = type.GetCustomAttribute(); - } - catch (Exception ex) - { - McpLog.Warn($"Failed to read [McpForUnityResource] for {type.FullName}: {ex.Message}"); - continue; - } - - if (resourceAttr == null) - { - continue; - } - - var metadata = ExtractResourceMetadata(type, resourceAttr); - if (metadata != null) - { - if (_cachedResources.ContainsKey(metadata.Name)) - { - McpLog.Warn($"Duplicate resource name '{metadata.Name}' from {type.FullName}; overwriting previous registration."); - } - _cachedResources[metadata.Name] = metadata; - EnsurePreferenceInitialized(metadata); - } - } - - McpLog.Info($"Discovered {_cachedResources.Count} MCP resources via reflection", false); - return _cachedResources.Values.ToList(); - } - - public ResourceMetadata GetResourceMetadata(string resourceName) - { - if (string.IsNullOrEmpty(resourceName)) - { - return null; - } - - if (_cachedResources == null) - { - DiscoverAllResources(); - } - - return _cachedResources.TryGetValue(resourceName, out var metadata) ? metadata : null; - } - - public List GetEnabledResources() - { - return DiscoverAllResources() - .Where(r => IsResourceEnabled(r.Name)) - .ToList(); - } - - public bool IsResourceEnabled(string resourceName) - { - if (string.IsNullOrEmpty(resourceName)) - { - return false; - } - - string key = GetResourcePreferenceKey(resourceName); - if (EditorPrefs.HasKey(key)) - { - return EditorPrefs.GetBool(key, true); - } - - // Default: all resources enabled - return true; - } - - public void SetResourceEnabled(string resourceName, bool enabled) - { - if (string.IsNullOrEmpty(resourceName)) - { - return; - } - - string key = GetResourcePreferenceKey(resourceName); - EditorPrefs.SetBool(key, enabled); - } - - public void InvalidateCache() - { - _cachedResources = null; - } - - private ResourceMetadata ExtractResourceMetadata(Type type, McpForUnityResourceAttribute resourceAttr) - { - try - { - string resourceName = resourceAttr.ResourceName; - if (string.IsNullOrEmpty(resourceName)) - { - resourceName = StringCaseUtility.ToSnakeCase(type.Name); - } - - string description = resourceAttr.Description ?? $"Resource: {resourceName}"; - - var metadata = new ResourceMetadata - { - Name = resourceName, - Description = description, - ClassName = type.Name, - Namespace = type.Namespace ?? "", - AssemblyName = type.Assembly.GetName().Name - }; - - metadata.IsBuiltIn = StringCaseUtility.IsBuiltInMcpType( - type, metadata.AssemblyName, "MCPForUnity.Editor.Resources"); - - return metadata; - } - catch (Exception ex) - { - McpLog.Error($"Failed to extract metadata for resource {type.Name}: {ex.Message}"); - return null; - } - } - - private void EnsurePreferenceInitialized(ResourceMetadata metadata) - { - if (metadata == null || string.IsNullOrEmpty(metadata.Name)) - { - return; - } - - string key = GetResourcePreferenceKey(metadata.Name); - if (!EditorPrefs.HasKey(key)) - { - EditorPrefs.SetBool(key, true); - } - } - - private static string GetResourcePreferenceKey(string resourceName) - { - return EditorPrefKeys.ResourceEnabledPrefix + resourceName; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs.meta b/Assets/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs.meta deleted file mode 100644 index 664e411..0000000 --- a/Assets/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 66ce49d2cc47a4bd3aa85ac9f099b757 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/ResourceDiscoveryService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Server.meta b/Assets/MCPForUnity/Editor/Services/Server.meta deleted file mode 100644 index e1e1dd4..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 1bb072befc9fe4242a501f46dce3fea1 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Services/Server/IPidFileManager.cs b/Assets/MCPForUnity/Editor/Services/Server/IPidFileManager.cs deleted file mode 100644 index b9bd74b..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/IPidFileManager.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace MCPForUnity.Editor.Services.Server -{ - /// - /// Interface for managing PID files and handshake state for the local HTTP server. - /// Handles persistence of server process information across Unity domain reloads. - /// - public interface IPidFileManager - { - /// - /// Gets the directory where PID files are stored. - /// - /// Path to the PID file directory - string GetPidDirectory(); - - /// - /// Gets the path to the PID file for a specific port. - /// - /// The port number - /// Full path to the PID file - string GetPidFilePath(int port); - - /// - /// Attempts to read the PID from a PID file. - /// - /// Path to the PID file - /// Output: the process ID if found - /// True if a valid PID was read - bool TryReadPid(string pidFilePath, out int pid); - - /// - /// Attempts to extract the port number from a PID file path. - /// - /// Path to the PID file - /// Output: the port number - /// True if the port was extracted successfully - bool TryGetPortFromPidFilePath(string pidFilePath, out int port); - - /// - /// Deletes a PID file. - /// - /// Path to the PID file to delete - void DeletePidFile(string pidFilePath); - - /// - /// Stores the handshake information (PID file path and instance token) in EditorPrefs. - /// - /// Path to the PID file - /// Unique instance token for the server - void StoreHandshake(string pidFilePath, string instanceToken); - - /// - /// Attempts to retrieve stored handshake information from EditorPrefs. - /// - /// Output: stored PID file path - /// Output: stored instance token - /// True if valid handshake information was found - bool TryGetHandshake(out string pidFilePath, out string instanceToken); - - /// - /// Stores PID tracking information in EditorPrefs. - /// - /// The process ID - /// The port number - /// Optional hash of the command arguments - void StoreTracking(int pid, int port, string argsHash = null); - - /// - /// Attempts to retrieve a stored PID for the expected port. - /// Validates that the stored information is still valid (within 6-hour window). - /// - /// The expected port number - /// Output: the stored process ID - /// True if a valid stored PID was found - bool TryGetStoredPid(int expectedPort, out int pid); - - /// - /// Gets the stored args hash for the tracked server. - /// - /// The stored args hash, or empty string if not found - string GetStoredArgsHash(); - - /// - /// Clears all PID tracking information from EditorPrefs. - /// - void ClearTracking(); - - /// - /// Computes a short hash of the input string for fingerprinting. - /// - /// The input string - /// A short hash string (16 hex characters) - string ComputeShortHash(string input); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Server/IPidFileManager.cs.meta b/Assets/MCPForUnity/Editor/Services/Server/IPidFileManager.cs.meta deleted file mode 100644 index 2470b00..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/IPidFileManager.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: f4a4c5d093da74ce79fb29a0670a58a7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Server/IPidFileManager.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Server/IProcessDetector.cs b/Assets/MCPForUnity/Editor/Services/Server/IProcessDetector.cs deleted file mode 100644 index 886e29d..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/IProcessDetector.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; - -namespace MCPForUnity.Editor.Services.Server -{ - /// - /// Interface for platform-specific process inspection operations. - /// Provides methods to detect MCP server processes, query process command lines, - /// and find processes listening on specific ports. - /// - public interface IProcessDetector - { - /// - /// Determines if a process looks like an MCP server process based on its command line. - /// Checks for indicators like uvx, python, mcp-for-unity, uvicorn, etc. - /// - /// The process ID to check - /// True if the process appears to be an MCP server - bool LooksLikeMcpServerProcess(int pid); - - /// - /// Attempts to get the command line arguments for a Unix process. - /// - /// The process ID - /// Output: normalized (lowercase, whitespace removed) command line args - /// True if the command line was retrieved successfully - bool TryGetProcessCommandLine(int pid, out string argsLower); - - /// - /// Gets the process IDs of all processes listening on a specific TCP port. - /// - /// The port number to check - /// List of process IDs listening on the port - List GetListeningProcessIdsForPort(int port); - - /// - /// Gets the current Unity Editor process ID safely. - /// - /// The current process ID, or -1 if it cannot be determined - int GetCurrentProcessId(); - - /// - /// Checks if a process exists on Unix systems. - /// - /// The process ID to check - /// True if the process exists - bool ProcessExists(int pid); - - /// - /// Normalizes a string for matching by removing whitespace and converting to lowercase. - /// - /// The input string - /// Normalized string for matching - string NormalizeForMatch(string input); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Server/IProcessDetector.cs.meta b/Assets/MCPForUnity/Editor/Services/Server/IProcessDetector.cs.meta deleted file mode 100644 index cef8c50..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/IProcessDetector.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 25f32875fb87541b69ead19c08520836 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Server/IProcessDetector.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Server/IProcessTerminator.cs b/Assets/MCPForUnity/Editor/Services/Server/IProcessTerminator.cs deleted file mode 100644 index 0f6e9f8..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/IProcessTerminator.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MCPForUnity.Editor.Services.Server -{ - /// - /// Interface for platform-specific process termination. - /// Provides methods to terminate processes gracefully or forcefully. - /// - public interface IProcessTerminator - { - /// - /// Terminates a process using platform-appropriate methods. - /// On Unix: Tries SIGTERM first with grace period, then SIGKILL. - /// On Windows: Tries taskkill, then taskkill /F. - /// - /// The process ID to terminate - /// True if the process was terminated successfully - bool Terminate(int pid); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Server/IProcessTerminator.cs.meta b/Assets/MCPForUnity/Editor/Services/Server/IProcessTerminator.cs.meta deleted file mode 100644 index c802068..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/IProcessTerminator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 6a55c18e08b534afa85654410da8a463 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Server/IProcessTerminator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Server/IServerCommandBuilder.cs b/Assets/MCPForUnity/Editor/Services/Server/IServerCommandBuilder.cs deleted file mode 100644 index f32b1eb..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/IServerCommandBuilder.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace MCPForUnity.Editor.Services.Server -{ - /// - /// Interface for building uvx/server command strings. - /// Handles platform-specific command construction for starting the MCP HTTP server. - /// - public interface IServerCommandBuilder - { - /// - /// Attempts to build the command parts for starting the local HTTP server. - /// - /// Output: the executable file name (e.g., uvx path) - /// Output: the command arguments - /// Output: the full command string for display - /// Output: error message if the command cannot be built - /// True if the command was built successfully - bool TryBuildCommand(out string fileName, out string arguments, out string displayCommand, out string error); - - /// - /// Builds the uv path from the uvx path by replacing uvx with uv. - /// - /// Path to uvx executable - /// Path to uv executable - string BuildUvPathFromUvx(string uvxPath); - - /// - /// Gets the platform-specific PATH prepend string for finding uv/uvx. - /// - /// Paths to prepend to PATH environment variable - string GetPlatformSpecificPathPrepend(); - - /// - /// Quotes a string if it contains spaces. - /// - /// The input string - /// The string, wrapped in quotes if it contains spaces - string QuoteIfNeeded(string input); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Server/IServerCommandBuilder.cs.meta b/Assets/MCPForUnity/Editor/Services/Server/IServerCommandBuilder.cs.meta deleted file mode 100644 index a8c76f2..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/IServerCommandBuilder.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 12e80005e3f5b45239c48db981675ccf -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Server/IServerCommandBuilder.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Server/ITerminalLauncher.cs b/Assets/MCPForUnity/Editor/Services/Server/ITerminalLauncher.cs deleted file mode 100644 index 3a89684..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/ITerminalLauncher.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Diagnostics; - -namespace MCPForUnity.Editor.Services.Server -{ - /// - /// Interface for launching commands in platform-specific terminal windows. - /// Supports macOS Terminal, Windows cmd, and Linux terminal emulators. - /// - public interface ITerminalLauncher - { - /// - /// Creates a ProcessStartInfo for opening a terminal window with the given command. - /// Works cross-platform: macOS, Windows, and Linux. - /// - /// The command to execute in the terminal - /// A configured ProcessStartInfo for launching the terminal - ProcessStartInfo CreateTerminalProcessStartInfo(string command); - - /// - /// Gets the project root path for storing terminal scripts. - /// - /// Path to the project root directory - string GetProjectRootPath(); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Server/ITerminalLauncher.cs.meta b/Assets/MCPForUnity/Editor/Services/Server/ITerminalLauncher.cs.meta deleted file mode 100644 index f87d18a..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/ITerminalLauncher.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: a5990e868c0cd4999858ce1c1a2defed -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Server/ITerminalLauncher.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Server/PidFileManager.cs b/Assets/MCPForUnity/Editor/Services/Server/PidFileManager.cs deleted file mode 100644 index eca60ee..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/PidFileManager.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using MCPForUnity.Editor.Constants; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Services.Server -{ - /// - /// Manages PID files and handshake state for the local HTTP server. - /// Handles persistence of server process information across Unity domain reloads. - /// - public class PidFileManager : IPidFileManager - { - /// - public string GetPidDirectory() - { - return Path.Combine(GetProjectRootPath(), "Library", "MCPForUnity", "RunState"); - } - - /// - public string GetPidFilePath(int port) - { - string dir = GetPidDirectory(); - Directory.CreateDirectory(dir); - return Path.Combine(dir, $"mcp_http_{port}.pid"); - } - - /// - public bool TryReadPid(string pidFilePath, out int pid) - { - pid = 0; - try - { - if (string.IsNullOrEmpty(pidFilePath) || !File.Exists(pidFilePath)) - { - return false; - } - - string text = File.ReadAllText(pidFilePath).Trim(); - if (int.TryParse(text, out pid)) - { - return pid > 0; - } - - // Best-effort: tolerate accidental extra whitespace/newlines. - var firstLine = text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); - if (int.TryParse(firstLine, out pid)) - { - return pid > 0; - } - - pid = 0; - return false; - } - catch - { - pid = 0; - return false; - } - } - - /// - public bool TryGetPortFromPidFilePath(string pidFilePath, out int port) - { - port = 0; - if (string.IsNullOrEmpty(pidFilePath)) - { - return false; - } - - try - { - string fileName = Path.GetFileNameWithoutExtension(pidFilePath); - if (string.IsNullOrEmpty(fileName)) - { - return false; - } - - const string prefix = "mcp_http_"; - if (!fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - string portText = fileName.Substring(prefix.Length); - return int.TryParse(portText, out port) && port > 0; - } - catch - { - port = 0; - return false; - } - } - - /// - public void DeletePidFile(string pidFilePath) - { - try - { - if (!string.IsNullOrEmpty(pidFilePath) && File.Exists(pidFilePath)) - { - File.Delete(pidFilePath); - } - } - catch { } - } - - /// - public void StoreHandshake(string pidFilePath, string instanceToken) - { - try - { - if (!string.IsNullOrEmpty(pidFilePath)) - { - EditorPrefs.SetString(EditorPrefKeys.LastLocalHttpServerPidFilePath, pidFilePath); - } - } - catch { } - - try - { - if (!string.IsNullOrEmpty(instanceToken)) - { - EditorPrefs.SetString(EditorPrefKeys.LastLocalHttpServerInstanceToken, instanceToken); - } - } - catch { } - } - - /// - public bool TryGetHandshake(out string pidFilePath, out string instanceToken) - { - pidFilePath = null; - instanceToken = null; - try - { - pidFilePath = EditorPrefs.GetString(EditorPrefKeys.LastLocalHttpServerPidFilePath, string.Empty); - instanceToken = EditorPrefs.GetString(EditorPrefKeys.LastLocalHttpServerInstanceToken, string.Empty); - if (string.IsNullOrEmpty(pidFilePath) || string.IsNullOrEmpty(instanceToken)) - { - pidFilePath = null; - instanceToken = null; - return false; - } - return true; - } - catch - { - pidFilePath = null; - instanceToken = null; - return false; - } - } - - /// - public void StoreTracking(int pid, int port, string argsHash = null) - { - try { EditorPrefs.SetInt(EditorPrefKeys.LastLocalHttpServerPid, pid); } catch { } - try { EditorPrefs.SetInt(EditorPrefKeys.LastLocalHttpServerPort, port); } catch { } - try { EditorPrefs.SetString(EditorPrefKeys.LastLocalHttpServerStartedUtc, DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)); } catch { } - try - { - if (!string.IsNullOrEmpty(argsHash)) - { - EditorPrefs.SetString(EditorPrefKeys.LastLocalHttpServerPidArgsHash, argsHash); - } - else - { - EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPidArgsHash); - } - } - catch { } - } - - /// - public bool TryGetStoredPid(int expectedPort, out int pid) - { - pid = 0; - try - { - int storedPid = EditorPrefs.GetInt(EditorPrefKeys.LastLocalHttpServerPid, 0); - int storedPort = EditorPrefs.GetInt(EditorPrefKeys.LastLocalHttpServerPort, 0); - string storedUtc = EditorPrefs.GetString(EditorPrefKeys.LastLocalHttpServerStartedUtc, string.Empty); - - if (storedPid <= 0 || storedPort != expectedPort) - { - return false; - } - - // Only trust the stored PID for a short window to avoid PID reuse issues. - // (We still verify the PID is listening on the expected port before killing.) - if (!string.IsNullOrEmpty(storedUtc) - && DateTime.TryParse(storedUtc, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var startedAt)) - { - if ((DateTime.UtcNow - startedAt) > TimeSpan.FromHours(6)) - { - return false; - } - } - - pid = storedPid; - return true; - } - catch - { - return false; - } - } - - /// - public string GetStoredArgsHash() - { - try - { - return EditorPrefs.GetString(EditorPrefKeys.LastLocalHttpServerPidArgsHash, string.Empty); - } - catch - { - return string.Empty; - } - } - - /// - public void ClearTracking() - { - try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPid); } catch { } - try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPort); } catch { } - try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerStartedUtc); } catch { } - try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPidArgsHash); } catch { } - try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPidFilePath); } catch { } - try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerInstanceToken); } catch { } - } - - /// - public string ComputeShortHash(string input) - { - if (string.IsNullOrEmpty(input)) return string.Empty; - try - { - using var sha = SHA256.Create(); - byte[] bytes = Encoding.UTF8.GetBytes(input); - byte[] hash = sha.ComputeHash(bytes); - // 8 bytes => 16 hex chars is plenty as a stable fingerprint for our purposes. - var sb = new StringBuilder(16); - for (int i = 0; i < 8 && i < hash.Length; i++) - { - sb.Append(hash[i].ToString("x2")); - } - return sb.ToString(); - } - catch - { - return string.Empty; - } - } - - private static string GetProjectRootPath() - { - try - { - // Application.dataPath is "...//Assets" - return Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - } - catch - { - return Application.dataPath; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Server/PidFileManager.cs.meta b/Assets/MCPForUnity/Editor/Services/Server/PidFileManager.cs.meta deleted file mode 100644 index cd97e43..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/PidFileManager.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 57875f281fda94a4ea17cb74d4b13378 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Server/PidFileManager.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Server/ProcessDetector.cs b/Assets/MCPForUnity/Editor/Services/Server/ProcessDetector.cs deleted file mode 100644 index b553cb3..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/ProcessDetector.cs +++ /dev/null @@ -1,268 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using MCPForUnity.Editor.Helpers; -using UnityEngine; - -namespace MCPForUnity.Editor.Services.Server -{ - /// - /// Platform-specific process inspection for detecting MCP server processes. - /// - public class ProcessDetector : IProcessDetector - { - /// - public string NormalizeForMatch(string input) - { - if (string.IsNullOrEmpty(input)) return string.Empty; - var sb = new StringBuilder(input.Length); - foreach (char c in input) - { - if (char.IsWhiteSpace(c)) continue; - sb.Append(char.ToLowerInvariant(c)); - } - return sb.ToString(); - } - - /// - public int GetCurrentProcessId() - { - try { return System.Diagnostics.Process.GetCurrentProcess().Id; } - catch { return -1; } - } - - /// - public bool ProcessExists(int pid) - { - try - { - if (Application.platform == RuntimePlatform.WindowsEditor) - { - // On Windows, use tasklist to check if process exists - bool ok = ExecPath.TryRun("tasklist", $"/FI \"PID eq {pid}\"", Application.dataPath, out var stdout, out var stderr, 5000); - string combined = ((stdout ?? string.Empty) + "\n" + (stderr ?? string.Empty)).ToLowerInvariant(); - return ok && combined.Contains(pid.ToString()); - } - - // Unix: ps exits non-zero when PID is not found. - string psPath = "/bin/ps"; - if (!File.Exists(psPath)) psPath = "ps"; - ExecPath.TryRun(psPath, $"-p {pid} -o pid=", Application.dataPath, out var psStdout, out var psStderr, 2000); - string combined2 = ((psStdout ?? string.Empty) + "\n" + (psStderr ?? string.Empty)).Trim(); - return !string.IsNullOrEmpty(combined2) && combined2.Any(char.IsDigit); - } - catch - { - return true; // Assume it exists if we cannot verify. - } - } - - /// - public bool TryGetProcessCommandLine(int pid, out string argsLower) - { - argsLower = string.Empty; - try - { - if (Application.platform == RuntimePlatform.WindowsEditor) - { - // Windows: use wmic to get command line - ExecPath.TryRun("cmd.exe", $"/c wmic process where \"ProcessId={pid}\" get CommandLine /value", Application.dataPath, out var wmicOut, out var wmicErr, 5000); - string wmicCombined = ((wmicOut ?? string.Empty) + "\n" + (wmicErr ?? string.Empty)); - if (!string.IsNullOrEmpty(wmicCombined) && wmicCombined.ToLowerInvariant().Contains("commandline=")) - { - argsLower = NormalizeForMatch(wmicOut ?? string.Empty); - return true; - } - return false; - } - - // Unix: ps -p pid -ww -o args= - string psPath = "/bin/ps"; - if (!File.Exists(psPath)) psPath = "ps"; - - bool ok = ExecPath.TryRun(psPath, $"-p {pid} -ww -o args=", Application.dataPath, out var stdout, out var stderr, 5000); - if (!ok && string.IsNullOrWhiteSpace(stdout)) - { - return false; - } - string combined = ((stdout ?? string.Empty) + "\n" + (stderr ?? string.Empty)).Trim(); - if (string.IsNullOrEmpty(combined)) return false; - // Normalize for matching to tolerate ps wrapping/newlines. - argsLower = NormalizeForMatch(combined); - return true; - } - catch - { - return false; - } - } - - /// - public List GetListeningProcessIdsForPort(int port) - { - var results = new List(); - try - { - string stdout, stderr; - bool success; - - if (Application.platform == RuntimePlatform.WindowsEditor) - { - // Run netstat -ano directly (without findstr) and filter in C#. - // Using findstr in a pipe causes the entire command to return exit code 1 when no matches are found, - // which ExecPath.TryRun interprets as failure. Running netstat alone gives us exit code 0 on success. - success = ExecPath.TryRun("netstat.exe", "-ano", Application.dataPath, out stdout, out stderr); - - // Process stdout regardless of success flag - netstat might still produce valid output - if (!string.IsNullOrEmpty(stdout)) - { - string portSuffix = $":{port}"; - var lines = stdout.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - // Windows netstat format: Proto Local Address Foreign Address State PID - // Example: TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 12345 - if (line.Contains("LISTENING") && line.Contains(portSuffix)) - { - var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - // Verify the local address column actually ends with :{port} - // parts[0] = Proto (TCP), parts[1] = Local Address, parts[2] = Foreign Address, parts[3] = State, parts[4] = PID - if (parts.Length >= 5) - { - string localAddr = parts[1]; - if (localAddr.EndsWith(portSuffix) && int.TryParse(parts[parts.Length - 1], out int parsedPid)) - { - results.Add(parsedPid); - } - } - } - } - } - } - else - { - // lsof: only return LISTENers (avoids capturing random clients) - // Use /usr/sbin/lsof directly as it might not be in PATH for Unity - string lsofPath = "/usr/sbin/lsof"; - if (!File.Exists(lsofPath)) lsofPath = "lsof"; // Fallback - - // -nP: avoid DNS/service name lookups; faster and less error-prone - success = ExecPath.TryRun(lsofPath, $"-nP -iTCP:{port} -sTCP:LISTEN -t", Application.dataPath, out stdout, out stderr); - if (success && !string.IsNullOrWhiteSpace(stdout)) - { - var pidStrings = stdout.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var pidString in pidStrings) - { - if (int.TryParse(pidString.Trim(), out int parsedPid)) - { - results.Add(parsedPid); - } - } - } - } - } - catch (Exception ex) - { - McpLog.Warn($"Error checking port {port}: {ex.Message}"); - } - return results.Distinct().ToList(); - } - - /// - public bool LooksLikeMcpServerProcess(int pid) - { - try - { - // Windows best-effort: First check process name with tasklist, then try to get command line with wmic - if (Application.platform == RuntimePlatform.WindowsEditor) - { - // Step 1: Check if process name matches known server executables - ExecPath.TryRun("cmd.exe", $"/c tasklist /FI \"PID eq {pid}\"", Application.dataPath, out var tasklistOut, out var tasklistErr, 5000); - string tasklistCombined = ((tasklistOut ?? string.Empty) + "\n" + (tasklistErr ?? string.Empty)).ToLowerInvariant(); - - // Check for common process names - bool isPythonOrUv = tasklistCombined.Contains("python") || tasklistCombined.Contains("uvx") || tasklistCombined.Contains("uv.exe"); - if (!isPythonOrUv) - { - return false; - } - - // Step 2: Try to get command line with wmic for better validation - ExecPath.TryRun("cmd.exe", $"/c wmic process where \"ProcessId={pid}\" get CommandLine /value", Application.dataPath, out var wmicOut, out var wmicErr, 5000); - string wmicCombined = ((wmicOut ?? string.Empty) + "\n" + (wmicErr ?? string.Empty)).ToLowerInvariant(); - string wmicCompact = NormalizeForMatch(wmicOut ?? string.Empty); - - // If we can see the command line, validate it's our server - if (!string.IsNullOrEmpty(wmicCombined) && wmicCombined.Contains("commandline=")) - { - bool mentionsMcp = wmicCompact.Contains("mcp-for-unity") - || wmicCompact.Contains("mcp_for_unity") - || wmicCompact.Contains("mcpforunity") - || wmicCompact.Contains("mcpforunityserver"); - bool mentionsTransport = wmicCompact.Contains("--transporthttp") || (wmicCompact.Contains("--transport") && wmicCompact.Contains("http")); - bool mentionsUvicorn = wmicCombined.Contains("uvicorn"); - - if (mentionsMcp || mentionsTransport || mentionsUvicorn) - { - return true; - } - } - - // Fall back to just checking for python/uv processes if wmic didn't give us details - // This is less precise but necessary for cases where wmic access is restricted - return isPythonOrUv; - } - - // macOS/Linux: ps -p pid -ww -o comm= -o args= - // Use -ww to avoid truncating long command lines (important for reliably spotting 'mcp-for-unity'). - // Use an absolute ps path to avoid relying on PATH inside the Unity Editor process. - string psPath = "/bin/ps"; - if (!File.Exists(psPath)) psPath = "ps"; - // Important: ExecPath.TryRun returns false when exit code != 0, but ps output can still be useful. - // Always parse stdout/stderr regardless of exit code to avoid false negatives. - ExecPath.TryRun(psPath, $"-p {pid} -ww -o comm= -o args=", Application.dataPath, out var psOut, out var psErr, 5000); - string raw = ((psOut ?? string.Empty) + "\n" + (psErr ?? string.Empty)).Trim(); - string s = raw.ToLowerInvariant(); - string sCompact = NormalizeForMatch(raw); - if (!string.IsNullOrEmpty(s)) - { - bool mentionsMcp = sCompact.Contains("mcp-for-unity") - || sCompact.Contains("mcp_for_unity") - || sCompact.Contains("mcpforunity"); - - // If it explicitly mentions the server package/entrypoint, that is sufficient. - // Note: Check before Unity exclusion since "mcp-for-unity" contains "unity". - if (mentionsMcp) - { - return true; - } - - // Explicitly never kill Unity / Unity Hub processes - // Note: explicit !mentionsMcp is defensive; we already return early for mentionsMcp above. - if (s.Contains("unityhub") || s.Contains("unity hub") || (s.Contains("unity") && !mentionsMcp)) - { - return false; - } - - // Positive indicators - bool mentionsUvx = s.Contains("uvx") || s.Contains(" uvx "); - bool mentionsUv = s.Contains("uv ") || s.Contains("/uv"); - bool mentionsPython = s.Contains("python"); - bool mentionsUvicorn = s.Contains("uvicorn"); - bool mentionsTransport = sCompact.Contains("--transporthttp") || (sCompact.Contains("--transport") && sCompact.Contains("http")); - - // Accept if it looks like uv/uvx/python launching our server package/entrypoint - if ((mentionsUvx || mentionsUv || mentionsPython || mentionsUvicorn) && mentionsTransport) - { - return true; - } - } - } - catch { } - - return false; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Server/ProcessDetector.cs.meta b/Assets/MCPForUnity/Editor/Services/Server/ProcessDetector.cs.meta deleted file mode 100644 index 6af7311..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/ProcessDetector.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4df6fa24a35d74d1cb9b67e40e50b45d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Server/ProcessDetector.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Server/ProcessTerminator.cs b/Assets/MCPForUnity/Editor/Services/Server/ProcessTerminator.cs deleted file mode 100644 index 7e803b1..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/ProcessTerminator.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.IO; -using MCPForUnity.Editor.Helpers; -using UnityEngine; - -namespace MCPForUnity.Editor.Services.Server -{ - /// - /// Platform-specific process termination for stopping MCP server processes. - /// - public class ProcessTerminator : IProcessTerminator - { - private readonly IProcessDetector _processDetector; - - /// - /// Creates a new ProcessTerminator with the specified process detector. - /// - /// Process detector for checking process existence - public ProcessTerminator(IProcessDetector processDetector) - { - _processDetector = processDetector ?? throw new ArgumentNullException(nameof(processDetector)); - } - - /// - public bool Terminate(int pid) - { - // CRITICAL: Validate PID before any kill operation. - // On Unix, kill(-1) kills ALL processes the user can signal! - // On Unix, kill(0) signals all processes in the process group. - // PID 1 is init/launchd and must never be killed. - // Only positive PIDs > 1 are valid for targeted termination. - if (pid <= 1) - { - return false; - } - - // Never kill the current Unity process - int currentPid = _processDetector.GetCurrentProcessId(); - if (currentPid > 0 && pid == currentPid) - { - return false; - } - - try - { - string stdout, stderr; - if (Application.platform == RuntimePlatform.WindowsEditor) - { - // taskkill without /F first; fall back to /F if needed. - bool ok = ExecPath.TryRun("taskkill", $"/PID {pid} /T", Application.dataPath, out stdout, out stderr); - if (!ok) - { - ok = ExecPath.TryRun("taskkill", $"/F /PID {pid} /T", Application.dataPath, out stdout, out stderr); - } - return ok; - } - else - { - // Try a graceful termination first, then escalate if the process is still alive. - // Note: `kill -15` can succeed (exit 0) even if the process takes time to exit, - // so we verify and only escalate when needed. - string killPath = "/bin/kill"; - if (!File.Exists(killPath)) killPath = "kill"; - ExecPath.TryRun(killPath, $"-15 {pid}", Application.dataPath, out stdout, out stderr); - - // Wait briefly for graceful shutdown. - var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(8); - while (DateTime.UtcNow < deadline) - { - if (!_processDetector.ProcessExists(pid)) - { - return true; - } - System.Threading.Thread.Sleep(100); - } - - // Escalate. - ExecPath.TryRun(killPath, $"-9 {pid}", Application.dataPath, out stdout, out stderr); - return !_processDetector.ProcessExists(pid); - } - } - catch (Exception ex) - { - McpLog.Error($"Error killing process {pid}: {ex.Message}"); - return false; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Server/ProcessTerminator.cs.meta b/Assets/MCPForUnity/Editor/Services/Server/ProcessTerminator.cs.meta deleted file mode 100644 index 0adb4cd..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/ProcessTerminator.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 900df88b4d0844704af9cb47633d44a9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Server/ProcessTerminator.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs b/Assets/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs deleted file mode 100644 index 47b4675..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Services.Server -{ - /// - /// Builds uvx/server command strings for starting the MCP HTTP server. - /// Handles platform-specific command construction. - /// - public class ServerCommandBuilder : IServerCommandBuilder - { - /// - public bool TryBuildCommand(out string fileName, out string arguments, out string displayCommand, out string error) - { - fileName = null; - arguments = null; - displayCommand = null; - error = null; - - bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport; - if (!useHttpTransport) - { - error = "HTTP transport is disabled. Enable it in the MCP For Unity window first."; - return false; - } - - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); - if (!IsLocalUrl(httpUrl)) - { - error = $"The configured URL ({httpUrl}) is not a local address. Local server launch only works for localhost."; - return false; - } - - var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts(); - if (string.IsNullOrEmpty(uvxPath)) - { - error = "uv is not installed or found in PATH. Install it or set an override in Advanced Settings."; - return false; - } - - // Use central helper that checks both DevModeForceServerRefresh AND local path detection. - // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead - string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty; - bool projectScopedTools = EditorPrefs.GetBool( - EditorPrefKeys.ProjectScopedToolsLocalHttp, - true - ); - string scopedFlag = projectScopedTools ? " --project-scoped-tools" : string.Empty; - - // Use centralized helper for beta server / prerelease args - string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true); - - string args = string.IsNullOrEmpty(fromArgs) - ? $"{devFlags}{packageName} --transport http --http-url {httpUrl}{scopedFlag}" - : $"{devFlags}{fromArgs} {packageName} --transport http --http-url {httpUrl}{scopedFlag}"; - - fileName = uvxPath; - arguments = args; - displayCommand = $"{QuoteIfNeeded(uvxPath)} {args}"; - return true; - } - - /// - public string BuildUvPathFromUvx(string uvxPath) - { - if (string.IsNullOrWhiteSpace(uvxPath)) - { - return uvxPath; - } - - string directory = Path.GetDirectoryName(uvxPath); - string extension = Path.GetExtension(uvxPath); - string uvFileName = "uv" + extension; - - return string.IsNullOrEmpty(directory) - ? uvFileName - : Path.Combine(directory, uvFileName); - } - - /// - public string GetPlatformSpecificPathPrepend() - { - if (Application.platform == RuntimePlatform.OSXEditor) - { - return string.Join(Path.PathSeparator.ToString(), new[] - { - "/opt/homebrew/bin", - "/usr/local/bin", - "/usr/bin", - "/bin" - }); - } - - if (Application.platform == RuntimePlatform.LinuxEditor) - { - return string.Join(Path.PathSeparator.ToString(), new[] - { - "/usr/local/bin", - "/usr/bin", - "/bin" - }); - } - - if (Application.platform == RuntimePlatform.WindowsEditor) - { - string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - - return string.Join(Path.PathSeparator.ToString(), new[] - { - !string.IsNullOrEmpty(localAppData) ? Path.Combine(localAppData, "Programs", "uv") : null, - !string.IsNullOrEmpty(programFiles) ? Path.Combine(programFiles, "uv") : null - }.Where(p => !string.IsNullOrEmpty(p)).ToArray()); - } - - return null; - } - - /// - public string QuoteIfNeeded(string input) - { - if (string.IsNullOrEmpty(input)) return input; - return input.IndexOf(' ') >= 0 ? $"\"{input}\"" : input; - } - - /// - /// Check if a URL is local (localhost, 127.0.0.1, 0.0.0.0, ::1) - /// - private static bool IsLocalUrl(string url) - { - if (string.IsNullOrEmpty(url)) return false; - - try - { - var uri = new Uri(url); - string host = uri.Host.ToLower(); - return host == "localhost" || host == "127.0.0.1" || host == "0.0.0.0" || host == "::1"; - } - catch - { - return false; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs.meta b/Assets/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs.meta deleted file mode 100644 index 55b0cac..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: db917800a5c2948088ede8a5d230b56e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Server/TerminalLauncher.cs b/Assets/MCPForUnity/Editor/Services/Server/TerminalLauncher.cs deleted file mode 100644 index fd8bd5d..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/TerminalLauncher.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.IO; -using MCPForUnity.Editor.Helpers; -using UnityEngine; - -namespace MCPForUnity.Editor.Services.Server -{ - /// - /// Launches commands in platform-specific terminal windows. - /// Supports macOS Terminal, Windows cmd, and Linux terminal emulators. - /// - public class TerminalLauncher : ITerminalLauncher - { - /// - public string GetProjectRootPath() - { - try - { - // Application.dataPath is "...//Assets" - return Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - } - catch - { - return Application.dataPath; - } - } - - /// - public System.Diagnostics.ProcessStartInfo CreateTerminalProcessStartInfo(string command) - { - if (string.IsNullOrWhiteSpace(command)) - throw new ArgumentException("Command cannot be empty", nameof(command)); - - command = command.Replace("\r", "").Replace("\n", ""); - -#if UNITY_EDITOR_OSX - // macOS: Avoid AppleScript (automation permission prompts). Use a .command script and open it. - string scriptsDir = Path.Combine(GetProjectRootPath(), "Library", "MCPForUnity", "TerminalScripts"); - Directory.CreateDirectory(scriptsDir); - string scriptPath = Path.Combine(scriptsDir, "mcp-terminal.command"); - File.WriteAllText( - scriptPath, - "#!/bin/bash\n" + - "set -e\n" + - "clear\n" + - $"{command}\n"); - ExecPath.TryRun("/bin/chmod", $"+x \"{scriptPath}\"", Application.dataPath, out _, out _, 3000); - return new System.Diagnostics.ProcessStartInfo - { - FileName = "/usr/bin/open", - Arguments = $"-a Terminal \"{scriptPath}\"", - UseShellExecute = false, - CreateNoWindow = true - }; -#elif UNITY_EDITOR_WIN - // Windows: Avoid brittle nested-quote escaping by writing a .cmd script and starting it in a new window. - string scriptsDir = Path.Combine(GetProjectRootPath(), "Library", "MCPForUnity", "TerminalScripts"); - Directory.CreateDirectory(scriptsDir); - string scriptPath = Path.Combine(scriptsDir, "mcp-terminal.cmd"); - File.WriteAllText( - scriptPath, - "@echo off\r\n" + - "cls\r\n" + - command + "\r\n"); - return new System.Diagnostics.ProcessStartInfo - { - FileName = "cmd.exe", - Arguments = $"/c start \"MCP Server\" cmd.exe /k \"{scriptPath}\"", - UseShellExecute = false, - CreateNoWindow = true - }; -#else - // Linux: Try common terminal emulators - // We use bash -c to execute the command, so we must properly quote/escape for bash - // Escape single quotes for the inner bash string - string escapedCommandLinux = command.Replace("'", "'\\''"); - // Wrap the command in single quotes for bash -c - string script = $"'{escapedCommandLinux}; exec bash'"; - // Escape double quotes for the outer Process argument string - string escapedScriptForArg = script.Replace("\"", "\\\""); - string bashCmdArgs = $"bash -c \"{escapedScriptForArg}\""; - - string[] terminals = { "gnome-terminal", "xterm", "konsole", "xfce4-terminal" }; - string terminalCmd = null; - - foreach (var term in terminals) - { - try - { - var which = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = "which", - Arguments = term, - UseShellExecute = false, - RedirectStandardOutput = true, - CreateNoWindow = true - }); - which.WaitForExit(5000); // Wait for up to 5 seconds, the command is typically instantaneous - if (which.ExitCode == 0) - { - terminalCmd = term; - break; - } - } - catch { } - } - - if (terminalCmd == null) - { - terminalCmd = "xterm"; // Fallback - } - - // Different terminals have different argument formats - string args; - if (terminalCmd == "gnome-terminal") - { - args = $"-- {bashCmdArgs}"; - } - else if (terminalCmd == "konsole") - { - args = $"-e {bashCmdArgs}"; - } - else if (terminalCmd == "xfce4-terminal") - { - // xfce4-terminal expects -e "command string" or -e command arg - args = $"--hold -e \"{bashCmdArgs.Replace("\"", "\\\"")}\""; - } - else // xterm and others - { - args = $"-hold -e {bashCmdArgs}"; - } - - return new System.Diagnostics.ProcessStartInfo - { - FileName = terminalCmd, - Arguments = args, - UseShellExecute = false, - CreateNoWindow = true - }; -#endif - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Server/TerminalLauncher.cs.meta b/Assets/MCPForUnity/Editor/Services/Server/TerminalLauncher.cs.meta deleted file mode 100644 index 23d6743..0000000 --- a/Assets/MCPForUnity/Editor/Services/Server/TerminalLauncher.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: d9693a18d706548b3aae28ea87f1ed08 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Server/TerminalLauncher.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/ServerManagementService.cs b/Assets/MCPForUnity/Editor/Services/ServerManagementService.cs deleted file mode 100644 index 1df3384..0000000 --- a/Assets/MCPForUnity/Editor/Services/ServerManagementService.cs +++ /dev/null @@ -1,876 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Sockets; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services.Server; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Service for managing MCP server lifecycle - /// - public class ServerManagementService : IServerManagementService - { - private readonly IProcessDetector _processDetector; - private readonly IPidFileManager _pidFileManager; - private readonly IProcessTerminator _processTerminator; - private readonly IServerCommandBuilder _commandBuilder; - private readonly ITerminalLauncher _terminalLauncher; - - /// - /// Creates a new ServerManagementService with default dependencies. - /// - public ServerManagementService() : this(null, null, null, null, null) { } - - /// - /// Creates a new ServerManagementService with injected dependencies (for testing). - /// - /// Process detector implementation (null for default) - /// PID file manager implementation (null for default) - /// Process terminator implementation (null for default) - /// Server command builder implementation (null for default) - /// Terminal launcher implementation (null for default) - public ServerManagementService( - IProcessDetector processDetector, - IPidFileManager pidFileManager = null, - IProcessTerminator processTerminator = null, - IServerCommandBuilder commandBuilder = null, - ITerminalLauncher terminalLauncher = null) - { - _processDetector = processDetector ?? new ProcessDetector(); - _pidFileManager = pidFileManager ?? new PidFileManager(); - _processTerminator = processTerminator ?? new ProcessTerminator(_processDetector); - _commandBuilder = commandBuilder ?? new ServerCommandBuilder(); - _terminalLauncher = terminalLauncher ?? new TerminalLauncher(); - } - - private string QuoteIfNeeded(string s) - { - return _commandBuilder.QuoteIfNeeded(s); - } - - private string NormalizeForMatch(string s) - { - return _processDetector.NormalizeForMatch(s); - } - - private void ClearLocalServerPidTracking() - { - _pidFileManager.ClearTracking(); - } - - private void StoreLocalHttpServerHandshake(string pidFilePath, string instanceToken) - { - _pidFileManager.StoreHandshake(pidFilePath, instanceToken); - } - - private bool TryGetLocalHttpServerHandshake(out string pidFilePath, out string instanceToken) - { - return _pidFileManager.TryGetHandshake(out pidFilePath, out instanceToken); - } - - private string GetLocalHttpServerPidFilePath(int port) - { - return _pidFileManager.GetPidFilePath(port); - } - - private bool TryReadPidFromPidFile(string pidFilePath, out int pid) - { - return _pidFileManager.TryReadPid(pidFilePath, out pid); - } - - private bool TryProcessCommandLineContainsInstanceToken(int pid, string instanceToken, out bool containsToken) - { - containsToken = false; - if (pid <= 0 || string.IsNullOrEmpty(instanceToken)) - { - return false; - } - - try - { - string tokenNeedle = instanceToken.ToLowerInvariant(); - - if (Application.platform == RuntimePlatform.WindowsEditor) - { - // Query full command line so we can validate token (reduces PID reuse risk). - // Use CIM via PowerShell (wmic is deprecated). - string ps = $"(Get-CimInstance Win32_Process -Filter \\\"ProcessId={pid}\\\").CommandLine"; - bool ok = ExecPath.TryRun("powershell", $"-NoProfile -Command \"{ps}\"", Application.dataPath, out var stdout, out var stderr, 5000); - string combined = ((stdout ?? string.Empty) + "\n" + (stderr ?? string.Empty)).ToLowerInvariant(); - containsToken = combined.Contains(tokenNeedle); - return ok; - } - - if (TryGetUnixProcessArgs(pid, out var argsLowerNow)) - { - containsToken = argsLowerNow.Contains(NormalizeForMatch(tokenNeedle)); - return true; - } - } - catch { } - - return false; - } - - private string ComputeShortHash(string input) - { - return _pidFileManager.ComputeShortHash(input); - } - - private bool TryGetStoredLocalServerPid(int expectedPort, out int pid) - { - return _pidFileManager.TryGetStoredPid(expectedPort, out pid); - } - - private string GetStoredArgsHash() - { - return _pidFileManager.GetStoredArgsHash(); - } - - /// - /// Clear the local uvx cache for the MCP server package - /// - /// True if successful, false otherwise - public bool ClearUvxCache() - { - try - { - string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); - string uvCommand = BuildUvPathFromUvx(uvxPath); - - // Get the package name - string packageName = "mcp-for-unity"; - - // Run uvx cache clean command - string args = $"cache clean {packageName}"; - - bool success; - string stdout; - string stderr; - - success = ExecuteUvCommand(uvCommand, args, out stdout, out stderr); - - if (success) - { - McpLog.Info($"uv cache cleared successfully: {stdout}"); - return true; - } - string combinedOutput = string.Join( - Environment.NewLine, - new[] { stderr, stdout }.Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim())); - - string lockHint = (!string.IsNullOrEmpty(combinedOutput) && - combinedOutput.IndexOf("currently in-use", StringComparison.OrdinalIgnoreCase) >= 0) - ? "Another uv process may be holding the cache lock; wait a moment and try again or clear with '--force' from a terminal." - : string.Empty; - - if (string.IsNullOrEmpty(combinedOutput)) - { - combinedOutput = "Command failed with no output. Ensure uv is installed, on PATH, or set an override in Advanced Settings."; - } - - McpLog.Error( - $"Failed to clear uv cache using '{uvCommand} {args}'. " + - $"Details: {combinedOutput}{(string.IsNullOrEmpty(lockHint) ? string.Empty : " Hint: " + lockHint)}"); - return false; - } - catch (Exception ex) - { - McpLog.Error($"Error clearing uv cache: {ex.Message}"); - return false; - } - } - - private bool ExecuteUvCommand(string uvCommand, string args, out string stdout, out string stderr) - { - stdout = null; - stderr = null; - - string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); - string uvPath = BuildUvPathFromUvx(uvxPath); - - if (!string.Equals(uvCommand, uvPath, StringComparison.OrdinalIgnoreCase)) - { - return ExecPath.TryRun(uvCommand, args, Application.dataPath, out stdout, out stderr, 30000); - } - - string command = $"{uvPath} {args}"; - string extraPathPrepend = GetPlatformSpecificPathPrepend(); - - if (Application.platform == RuntimePlatform.WindowsEditor) - { - return ExecPath.TryRun("cmd.exe", $"/c {command}", Application.dataPath, out stdout, out stderr, 30000, extraPathPrepend); - } - - string shell = File.Exists("/bin/bash") ? "/bin/bash" : "/bin/sh"; - - if (!string.IsNullOrEmpty(shell) && File.Exists(shell)) - { - string escaped = command.Replace("\"", "\\\""); - return ExecPath.TryRun(shell, $"-lc \"{escaped}\"", Application.dataPath, out stdout, out stderr, 30000, extraPathPrepend); - } - - return ExecPath.TryRun(uvPath, args, Application.dataPath, out stdout, out stderr, 30000, extraPathPrepend); - } - - private string BuildUvPathFromUvx(string uvxPath) - { - return _commandBuilder.BuildUvPathFromUvx(uvxPath); - } - - private string GetPlatformSpecificPathPrepend() - { - return _commandBuilder.GetPlatformSpecificPathPrepend(); - } - - /// - /// Start the local HTTP server in a separate terminal window. - /// Stops any existing server on the port and clears the uvx cache first. - /// - public bool StartLocalHttpServer() - { - /// Clean stale Python build artifacts when using a local dev server path - AssetPathUtility.CleanLocalServerBuildArtifacts(); - - if (!TryGetLocalHttpServerCommandParts(out _, out _, out var displayCommand, out var error)) - { - EditorUtility.DisplayDialog( - "Cannot Start HTTP Server", - error ?? "The server command could not be constructed with the current settings.", - "OK"); - return false; - } - - // First, try to stop any existing server (quietly; we'll only warn if the port remains occupied). - StopLocalHttpServerInternal(quiet: true); - - // If the port is still occupied, don't start and explain why (avoid confusing "refusing to stop" warnings). - try - { - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); - if (Uri.TryCreate(httpUrl, UriKind.Absolute, out var uri) && uri.Port > 0) - { - var remaining = GetListeningProcessIdsForPort(uri.Port); - if (remaining.Count > 0) - { - EditorUtility.DisplayDialog( - "Port In Use", - $"Cannot start the local HTTP server because port {uri.Port} is already in use by PID(s): " + - $"{string.Join(", ", remaining)}\n\n" + - "MCP For Unity will not terminate unrelated processes. Stop the owning process manually or change the HTTP URL.", - "OK"); - return false; - } - } - } - catch { } - - // Note: Dev mode cache-busting is handled by `uvx --no-cache --refresh` in the generated command. - - // Create a per-launch token + pidfile path so Stop can be deterministic without relying on port/PID heuristics. - string baseUrlForPid = HttpEndpointUtility.GetLocalBaseUrl(); - Uri.TryCreate(baseUrlForPid, UriKind.Absolute, out var uriForPid); - int portForPid = uriForPid?.Port ?? 0; - string instanceToken = Guid.NewGuid().ToString("N"); - string pidFilePath = portForPid > 0 ? GetLocalHttpServerPidFilePath(portForPid) : null; - - string launchCommand = displayCommand; - if (!string.IsNullOrEmpty(pidFilePath)) - { - launchCommand = $"{displayCommand} --pidfile {QuoteIfNeeded(pidFilePath)} --unity-instance-token {instanceToken}"; - } - - if (EditorUtility.DisplayDialog( - "Start Local HTTP Server", - $"This will start the MCP server in HTTP mode in a new terminal window:\n\n{launchCommand}\n\n" + - "Continue?", - "Start Server", - "Cancel")) - { - try - { - // Clear any stale handshake state from prior launches. - ClearLocalServerPidTracking(); - - // Best-effort: delete stale pidfile if it exists. - try - { - if (!string.IsNullOrEmpty(pidFilePath) && File.Exists(pidFilePath)) - { - DeletePidFile(pidFilePath); - } - } - catch { } - - // Launch the server in a new terminal window (keeps user-visible logs). - var startInfo = CreateTerminalProcessStartInfo(launchCommand); - System.Diagnostics.Process.Start(startInfo); - if (!string.IsNullOrEmpty(pidFilePath)) - { - StoreLocalHttpServerHandshake(pidFilePath, instanceToken); - } - McpLog.Info($"Started local HTTP server in terminal: {launchCommand}"); - return true; - } - catch (Exception ex) - { - McpLog.Error($"Failed to start server: {ex.Message}"); - EditorUtility.DisplayDialog( - "Error", - $"Failed to start server: {ex.Message}", - "OK"); - return false; - } - } - - return false; - } - - /// - /// Stop the local HTTP server by finding the process listening on the configured port - /// - public bool StopLocalHttpServer() - { - return StopLocalHttpServerInternal(quiet: false); - } - - public bool StopManagedLocalHttpServer() - { - if (!TryGetLocalHttpServerHandshake(out var pidFilePath, out _)) - { - return false; - } - - int port = 0; - if (!TryGetPortFromPidFilePath(pidFilePath, out port) || port <= 0) - { - string baseUrl = HttpEndpointUtility.GetLocalBaseUrl(); - if (IsLocalUrl(baseUrl) - && Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri) - && uri.Port > 0) - { - port = uri.Port; - } - } - - if (port <= 0) - { - return false; - } - - return StopLocalHttpServerInternal(quiet: true, portOverride: port, allowNonLocalUrl: true); - } - - public bool IsLocalHttpServerRunning() - { - try - { - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); - if (!IsLocalUrl(httpUrl)) - { - return false; - } - - if (!Uri.TryCreate(httpUrl, UriKind.Absolute, out var uri) || uri.Port <= 0) - { - return false; - } - - int port = uri.Port; - - // Handshake path: if we have a pidfile+token and the PID is still the listener, treat as running. - if (TryGetLocalHttpServerHandshake(out var pidFilePath, out var instanceToken) - && TryReadPidFromPidFile(pidFilePath, out var pidFromFile) - && pidFromFile > 0) - { - var pidsNow = GetListeningProcessIdsForPort(port); - if (pidsNow.Contains(pidFromFile)) - { - return true; - } - } - - var pids = GetListeningProcessIdsForPort(port); - if (pids.Count == 0) - { - return false; - } - - // Strong signal: stored PID is still the listener. - if (TryGetStoredLocalServerPid(port, out int storedPid) && storedPid > 0) - { - if (pids.Contains(storedPid)) - { - return true; - } - } - - // Best-effort: if anything listening looks like our server, treat as running. - foreach (var pid in pids) - { - if (pid <= 0) continue; - if (LooksLikeMcpServerProcess(pid)) - { - return true; - } - } - - return false; - } - catch - { - return false; - } - } - - public bool IsLocalHttpServerReachable() - { - try - { - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); - if (!IsLocalUrl(httpUrl)) - { - return false; - } - - if (!Uri.TryCreate(httpUrl, UriKind.Absolute, out var uri) || uri.Port <= 0) - { - return false; - } - - return TryConnectToLocalPort(uri.Host, uri.Port, timeoutMs: 50); - } - catch - { - return false; - } - } - - private static bool TryConnectToLocalPort(string host, int port, int timeoutMs) - { - try - { - if (string.IsNullOrEmpty(host)) - { - host = "127.0.0.1"; - } - - var hosts = new HashSet(StringComparer.OrdinalIgnoreCase) { host }; - if (host == "localhost" || host == "0.0.0.0") - { - hosts.Add("127.0.0.1"); - } - if (host == "::" || host == "0:0:0:0:0:0:0:0") - { - hosts.Add("::1"); - } - - foreach (var target in hosts) - { - try - { - using (var client = new TcpClient()) - { - var connectTask = client.ConnectAsync(target, port); - if (connectTask.Wait(timeoutMs) && client.Connected) - { - return true; - } - } - } - catch - { - // Ignore per-host failures. - } - } - } - catch - { - // Ignore probe failures and treat as unreachable. - } - - return false; - } - - private bool StopLocalHttpServerInternal(bool quiet, int? portOverride = null, bool allowNonLocalUrl = false) - { - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); - if (!allowNonLocalUrl && !IsLocalUrl(httpUrl)) - { - if (!quiet) - { - McpLog.Warn("Cannot stop server: URL is not local."); - } - return false; - } - - try - { - int port = 0; - if (portOverride.HasValue) - { - port = portOverride.Value; - } - else - { - var uri = new Uri(httpUrl); - port = uri.Port; - } - - if (port <= 0) - { - if (!quiet) - { - McpLog.Warn("Cannot stop server: Invalid port."); - } - return false; - } - - // Guardrails: - // - Never terminate the Unity Editor process. - // - Only terminate processes that look like the MCP server (uv/uvx/python running mcp-for-unity). - // This prevents accidental termination of unrelated services (including Unity itself). - int unityPid = GetCurrentProcessIdSafe(); - bool stoppedAny = false; - - // Preferred deterministic stop path: if we have a pidfile+token from a Unity-managed launch, - // validate and terminate exactly that PID. - if (TryGetLocalHttpServerHandshake(out var pidFilePath, out var instanceToken)) - { - // Prefer deterministic stop when Unity started the server (pidfile+token). - // If the pidfile isn't available yet (fast quit after start), we can optionally fall back - // to port-based heuristics when a port override was supplied (managed-stop path). - if (!TryReadPidFromPidFile(pidFilePath, out var pidFromFile) || pidFromFile <= 0) - { - if (!portOverride.HasValue) - { - if (!quiet) - { - McpLog.Warn( - $"Cannot stop local HTTP server on port {port}: pidfile not available yet at '{pidFilePath}'. " + - "If you just started the server, wait a moment and try again."); - } - return false; - } - - // Managed-stop fallback: proceed with port-based heuristics below. - // We intentionally do NOT clear handshake state here; it will be cleared if we successfully - // stop a server process and/or the port is freed. - } - else - { - // Never kill Unity/Hub. - if (unityPid > 0 && pidFromFile == unityPid) - { - if (!quiet) - { - McpLog.Warn($"Refusing to stop port {port}: pidfile PID {pidFromFile} is the Unity Editor process."); - } - } - else - { - var listeners = GetListeningProcessIdsForPort(port); - if (listeners.Count == 0) - { - // Nothing is listening anymore; clear stale handshake state. - try { DeletePidFile(pidFilePath); } catch { } - ClearLocalServerPidTracking(); - if (!quiet) - { - McpLog.Info($"No process found listening on port {port}"); - } - return false; - } - bool pidIsListener = listeners.Contains(pidFromFile); - bool tokenQueryOk = TryProcessCommandLineContainsInstanceToken(pidFromFile, instanceToken, out bool tokenMatches); - bool allowKill; - if (tokenQueryOk) - { - allowKill = tokenMatches; - } - else - { - // If token validation is unavailable (e.g. Windows CIM permission issues), - // fall back to a stricter heuristic: only allow stop if the PID still looks like our server. - allowKill = LooksLikeMcpServerProcess(pidFromFile); - } - - if (pidIsListener && allowKill) - { - if (TerminateProcess(pidFromFile)) - { - stoppedAny = true; - try { DeletePidFile(pidFilePath); } catch { } - ClearLocalServerPidTracking(); - if (!quiet) - { - McpLog.Info($"Stopped local HTTP server on port {port} (PID: {pidFromFile})"); - } - return true; - } - if (!quiet) - { - McpLog.Warn($"Failed to terminate local HTTP server on port {port} (PID: {pidFromFile})."); - } - return false; - } - if (!quiet) - { - McpLog.Warn( - $"Refusing to stop port {port}: pidfile PID {pidFromFile} failed validation " + - $"(listener={pidIsListener}, tokenMatch={tokenMatches}, tokenQueryOk={tokenQueryOk})."); - } - return false; - } - } - } - - var pids = GetListeningProcessIdsForPort(port); - if (pids.Count == 0) - { - if (stoppedAny) - { - // We stopped what Unity started; the port is now free. - if (!quiet) - { - McpLog.Info($"Stopped local HTTP server on port {port}"); - } - ClearLocalServerPidTracking(); - return true; - } - - if (!quiet) - { - McpLog.Info($"No process found listening on port {port}"); - } - ClearLocalServerPidTracking(); - return false; - } - - // Prefer killing the PID that we previously observed binding this port (if still valid). - if (TryGetStoredLocalServerPid(port, out int storedPid)) - { - if (pids.Contains(storedPid)) - { - string expectedHash = string.Empty; - expectedHash = GetStoredArgsHash(); - - // Prefer a fingerprint match (reduces PID reuse risk). If missing (older installs), - // fall back to a looser check to avoid leaving orphaned servers after domain reload. - if (TryGetUnixProcessArgs(storedPid, out var storedArgsLowerNow)) - { - // Never kill Unity/Hub. - // Note: "mcp-for-unity" includes "unity", so detect MCP indicators first. - bool storedMentionsMcp = storedArgsLowerNow.Contains("mcp-for-unity") - || storedArgsLowerNow.Contains("mcp_for_unity") - || storedArgsLowerNow.Contains("mcpforunity"); - if (storedArgsLowerNow.Contains("unityhub") - || storedArgsLowerNow.Contains("unity hub") - || (storedArgsLowerNow.Contains("unity") && !storedMentionsMcp)) - { - if (!quiet) - { - McpLog.Warn($"Refusing to stop port {port}: stored PID {storedPid} appears to be a Unity process."); - } - } - else - { - bool allowKill = false; - if (!string.IsNullOrEmpty(expectedHash)) - { - allowKill = string.Equals(expectedHash, ComputeShortHash(storedArgsLowerNow), StringComparison.OrdinalIgnoreCase); - } - else - { - // Older versions didn't store a fingerprint; accept common server indicators. - allowKill = storedArgsLowerNow.Contains("uvicorn") - || storedArgsLowerNow.Contains("fastmcp") - || storedArgsLowerNow.Contains("mcpforunity") - || storedArgsLowerNow.Contains("mcp-for-unity") - || storedArgsLowerNow.Contains("mcp_for_unity") - || storedArgsLowerNow.Contains("uvx") - || storedArgsLowerNow.Contains("python"); - } - - if (allowKill && TerminateProcess(storedPid)) - { - if (!quiet) - { - McpLog.Info($"Stopped local HTTP server on port {port} (PID: {storedPid})"); - } - stoppedAny = true; - ClearLocalServerPidTracking(); - // Refresh the PID list to avoid double-work. - pids = GetListeningProcessIdsForPort(port); - } - else if (!allowKill && !quiet) - { - McpLog.Warn($"Refusing to stop port {port}: stored PID {storedPid} did not match expected server fingerprint."); - } - } - } - } - else - { - // Stale PID (no longer listening). Clear. - ClearLocalServerPidTracking(); - } - } - - foreach (var pid in pids) - { - if (pid <= 0) continue; - if (unityPid > 0 && pid == unityPid) - { - if (!quiet) - { - McpLog.Warn($"Refusing to stop port {port}: owning PID appears to be the Unity Editor process (PID {pid})."); - } - continue; - } - - if (!LooksLikeMcpServerProcess(pid)) - { - if (!quiet) - { - McpLog.Warn($"Refusing to stop port {port}: owning PID {pid} does not look like mcp-for-unity."); - } - continue; - } - - if (TerminateProcess(pid)) - { - McpLog.Info($"Stopped local HTTP server on port {port} (PID: {pid})"); - stoppedAny = true; - } - else - { - if (!quiet) - { - McpLog.Warn($"Failed to stop process PID {pid} on port {port}"); - } - } - } - - if (stoppedAny) - { - ClearLocalServerPidTracking(); - } - return stoppedAny; - } - catch (Exception ex) - { - if (!quiet) - { - McpLog.Error($"Failed to stop server: {ex.Message}"); - } - return false; - } - } - - private bool TryGetUnixProcessArgs(int pid, out string argsLower) - { - return _processDetector.TryGetProcessCommandLine(pid, out argsLower); - } - - private bool TryGetPortFromPidFilePath(string pidFilePath, out int port) - { - return _pidFileManager.TryGetPortFromPidFilePath(pidFilePath, out port); - } - - private void DeletePidFile(string pidFilePath) - { - _pidFileManager.DeletePidFile(pidFilePath); - } - - private List GetListeningProcessIdsForPort(int port) - { - return _processDetector.GetListeningProcessIdsForPort(port); - } - - private int GetCurrentProcessIdSafe() - { - return _processDetector.GetCurrentProcessId(); - } - - private bool LooksLikeMcpServerProcess(int pid) - { - return _processDetector.LooksLikeMcpServerProcess(pid); - } - - private bool TerminateProcess(int pid) - { - return _processTerminator.Terminate(pid); - } - - /// - /// Attempts to build the command used for starting the local HTTP server - /// - public bool TryGetLocalHttpServerCommand(out string command, out string error) - { - command = null; - error = null; - if (!TryGetLocalHttpServerCommandParts(out var fileName, out var args, out var displayCommand, out error)) - { - return false; - } - - // Maintain existing behavior: return a single command string suitable for display/copy. - command = displayCommand; - return true; - } - - private bool TryGetLocalHttpServerCommandParts(out string fileName, out string arguments, out string displayCommand, out string error) - { - return _commandBuilder.TryBuildCommand(out fileName, out arguments, out displayCommand, out error); - } - - /// - /// Check if the configured HTTP URL is a local address - /// - public bool IsLocalUrl() - { - string httpUrl = HttpEndpointUtility.GetLocalBaseUrl(); - return IsLocalUrl(httpUrl); - } - - /// - /// Check if a URL is local (localhost, 127.0.0.1, 0.0.0.0) - /// - private static bool IsLocalUrl(string url) - { - if (string.IsNullOrEmpty(url)) return false; - - try - { - var uri = new Uri(url); - string host = uri.Host.ToLower(); - return host == "localhost" || host == "127.0.0.1" || host == "0.0.0.0" || host == "::1"; - } - catch - { - return false; - } - } - - /// - /// Check if the local HTTP server can be started - /// - public bool CanStartLocalServer() - { - bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport; - return useHttpTransport && IsLocalUrl(); - } - - private System.Diagnostics.ProcessStartInfo CreateTerminalProcessStartInfo(string command) - { - return _terminalLauncher.CreateTerminalProcessStartInfo(command); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/ServerManagementService.cs.meta b/Assets/MCPForUnity/Editor/Services/ServerManagementService.cs.meta deleted file mode 100644 index f5af4a1..0000000 --- a/Assets/MCPForUnity/Editor/Services/ServerManagementService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 8e60df35c5a76462d8aaa8078da86d75 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/ServerManagementService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs b/Assets/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs deleted file mode 100644 index a7c5f39..0000000 --- a/Assets/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services.Transport; -using MCPForUnity.Editor.Services.Transport.Transports; -using UnityEditor; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Ensures the legacy stdio bridge resumes after domain reloads, mirroring the HTTP handler. - /// - [InitializeOnLoad] - internal static class StdioBridgeReloadHandler - { - static StdioBridgeReloadHandler() - { - AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; - AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload; - } - - private static void OnBeforeAssemblyReload() - { - try - { - // Only persist resume intent when stdio is the active transport and the bridge is running. - bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; - // Check both TransportManager AND StdioBridgeHost directly, because CI starts via StdioBridgeHost - // bypassing TransportManager state. - bool tmRunning = MCPServiceLocator.TransportManager.IsRunning(TransportMode.Stdio); - bool hostRunning = StdioBridgeHost.IsRunning; - bool isRunning = tmRunning || hostRunning; - bool shouldResume = !useHttp && isRunning; - - if (shouldResume) - { - EditorPrefs.SetBool(EditorPrefKeys.ResumeStdioAfterReload, true); - - // Stop only the stdio bridge; leave HTTP untouched if it is running concurrently. - var stopTask = MCPServiceLocator.TransportManager.StopAsync(TransportMode.Stdio); - - // Wait for stop to complete (which deletes the status file) - try { stopTask.Wait(500); } catch { } - - // Write reloading status so clients don't think we vanished - StdioBridgeHost.WriteHeartbeat(true, "reloading"); - } - else - { - EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); - } - } - catch (Exception ex) - { - McpLog.Warn($"Failed to persist stdio reload flag: {ex.Message}"); - } - } - - private static void OnAfterAssemblyReload() - { - bool resume = false; - try - { - bool resumeFlag = EditorPrefs.GetBool(EditorPrefKeys.ResumeStdioAfterReload, false); - bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; - resume = resumeFlag && !useHttp; - - // If we're not going to resume, clear the flag immediately to avoid stuck "Resuming..." state - if (!resume) - { - EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); - } - } - catch (Exception ex) - { - McpLog.Warn($"Failed to read stdio reload flag: {ex.Message}"); - } - - if (!resume) - { - return; - } - - // Restart via TransportManager so state stays in sync; if it fails (port busy), rely on UI to retry. - TryStartBridgeImmediate(); - } - - private static void TryStartBridgeImmediate() - { - var startTask = MCPServiceLocator.TransportManager.StartAsync(TransportMode.Stdio); - startTask.ContinueWith(t => - { - // Clear the flag after attempting to start (success or failure). - // This prevents getting stuck in "Resuming..." state. - // We do this synchronously on the continuation thread - it's safe because - // EditorPrefs operations are thread-safe and any new reload will set the flag - // fresh in OnBeforeAssemblyReload before we get here. - try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); } catch { } - - if (t.IsFaulted) - { - var baseEx = t.Exception?.GetBaseException(); - McpLog.Warn($"Failed to resume stdio bridge after reload: {baseEx?.Message}"); - return; - } - if (!t.Result) - { - McpLog.Warn("Failed to resume stdio bridge after domain reload"); - return; - } - - MCPForUnity.Editor.Windows.MCPForUnityEditorWindow.RequestHealthVerification(); - }, System.Threading.Tasks.TaskScheduler.Default); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs.meta b/Assets/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs.meta deleted file mode 100644 index 5efc355..0000000 --- a/Assets/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 6e603c72a87974cf5b495cd683165fbf -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/TestJobManager.cs b/Assets/MCPForUnity/Editor/Services/TestJobManager.cs deleted file mode 100644 index bf2ffec..0000000 --- a/Assets/MCPForUnity/Editor/Services/TestJobManager.cs +++ /dev/null @@ -1,673 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json; -using UnityEditor; -using UnityEditorInternal; -using UnityEditor.TestTools.TestRunner.Api; - -namespace MCPForUnity.Editor.Services -{ - internal enum TestJobStatus - { - Running, - Succeeded, - Failed - } - - internal sealed class TestJobFailure - { - public string FullName { get; set; } - public string Message { get; set; } - } - - internal sealed class TestJob - { - public string JobId { get; set; } - public TestJobStatus Status { get; set; } - public string Mode { get; set; } - public long StartedUnixMs { get; set; } - public long? FinishedUnixMs { get; set; } - public long LastUpdateUnixMs { get; set; } - public int? TotalTests { get; set; } - public int CompletedTests { get; set; } - public string CurrentTestFullName { get; set; } - public long? CurrentTestStartedUnixMs { get; set; } - public string LastFinishedTestFullName { get; set; } - public long? LastFinishedUnixMs { get; set; } - public List FailuresSoFar { get; set; } - public string Error { get; set; } - public TestRunResult Result { get; set; } - } - - /// - /// Tracks async test jobs started via MCP tools. This is not intended to capture manual Test Runner UI runs. - /// - internal static class TestJobManager - { - // Keep this small to avoid ballooning payloads during polling. - private const int FailureCap = 25; - private const long StuckThresholdMs = 60_000; - private const long InitializationTimeoutMs = 15_000; // 15 seconds to call OnRunStarted, else fail - private const int MaxJobsToKeep = 10; - private const long MinPersistIntervalMs = 1000; // Throttle persistence to reduce overhead - - // SessionState survives domain reloads within the same Unity Editor session. - private const string SessionKeyJobs = "MCPForUnity.TestJobsV1"; - private const string SessionKeyCurrentJobId = "MCPForUnity.CurrentTestJobIdV1"; - - private static readonly object LockObj = new(); - private static readonly Dictionary Jobs = new(); - private static string _currentJobId; - private static long _lastPersistUnixMs; - - static TestJobManager() - { - // Restore after domain reloads (e.g., compilation while a job is running). - TryRestoreFromSessionState(); - } - - public static string CurrentJobId - { - get { lock (LockObj) return _currentJobId; } - } - - public static bool HasRunningJob - { - get - { - lock (LockObj) - { - return !string.IsNullOrEmpty(_currentJobId); - } - } - } - - /// - /// Force-clears any stuck or orphaned test job. Call this when tests get stuck due to - /// assembly reloads or other interruptions. - /// - /// True if a job was cleared, false if no running job exists. - public static bool ClearStuckJob() - { - bool cleared = false; - lock (LockObj) - { - if (string.IsNullOrEmpty(_currentJobId)) - { - return false; - } - - if (Jobs.TryGetValue(_currentJobId, out var job) && job.Status == TestJobStatus.Running) - { - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - job.Status = TestJobStatus.Failed; - job.Error = "Job cleared manually (stuck or orphaned)"; - job.FinishedUnixMs = now; - job.LastUpdateUnixMs = now; - McpLog.Warn($"[TestJobManager] Manually cleared stuck job {_currentJobId}"); - cleared = true; - } - - _currentJobId = null; - } - PersistToSessionState(force: true); - return cleared; - } - - private sealed class PersistedState - { - public string current_job_id { get; set; } - public List jobs { get; set; } - } - - private sealed class PersistedJob - { - public string job_id { get; set; } - public string status { get; set; } - public string mode { get; set; } - public long started_unix_ms { get; set; } - public long? finished_unix_ms { get; set; } - public long last_update_unix_ms { get; set; } - public int? total_tests { get; set; } - public int completed_tests { get; set; } - public string current_test_full_name { get; set; } - public long? current_test_started_unix_ms { get; set; } - public string last_finished_test_full_name { get; set; } - public long? last_finished_unix_ms { get; set; } - public List failures_so_far { get; set; } - public string error { get; set; } - } - - private static TestJobStatus ParseStatus(string status) - { - if (string.IsNullOrWhiteSpace(status)) - { - return TestJobStatus.Running; - } - - string s = status.Trim().ToLowerInvariant(); - return s switch - { - "succeeded" => TestJobStatus.Succeeded, - "failed" => TestJobStatus.Failed, - _ => TestJobStatus.Running - }; - } - - private static void TryRestoreFromSessionState() - { - try - { - string json = SessionState.GetString(SessionKeyJobs, string.Empty); - if (string.IsNullOrWhiteSpace(json)) - { - var legacy = SessionState.GetString(SessionKeyCurrentJobId, string.Empty); - _currentJobId = string.IsNullOrWhiteSpace(legacy) ? null : legacy; - return; - } - - var state = JsonConvert.DeserializeObject(json); - if (state?.jobs == null) - { - return; - } - - lock (LockObj) - { - Jobs.Clear(); - foreach (var pj in state.jobs) - { - if (pj == null || string.IsNullOrWhiteSpace(pj.job_id)) - { - continue; - } - - Jobs[pj.job_id] = new TestJob - { - JobId = pj.job_id, - Status = ParseStatus(pj.status), - Mode = pj.mode, - StartedUnixMs = pj.started_unix_ms, - FinishedUnixMs = pj.finished_unix_ms, - LastUpdateUnixMs = pj.last_update_unix_ms, - TotalTests = pj.total_tests, - CompletedTests = pj.completed_tests, - CurrentTestFullName = pj.current_test_full_name, - CurrentTestStartedUnixMs = pj.current_test_started_unix_ms, - LastFinishedTestFullName = pj.last_finished_test_full_name, - LastFinishedUnixMs = pj.last_finished_unix_ms, - FailuresSoFar = pj.failures_so_far ?? new List(), - Error = pj.error, - // Intentionally not persisted to avoid ballooning SessionState. - Result = null - }; - } - - _currentJobId = string.IsNullOrWhiteSpace(state.current_job_id) ? null : state.current_job_id; - if (!string.IsNullOrEmpty(_currentJobId) && !Jobs.ContainsKey(_currentJobId)) - { - _currentJobId = null; - } - - // Detect and clean up stale "running" jobs that were orphaned by domain reload. - // After a domain reload, TestRunStatus resets to not-running, but _currentJobId - // may still be set. If the job hasn't been updated recently, it's likely orphaned. - if (!string.IsNullOrEmpty(_currentJobId) && Jobs.TryGetValue(_currentJobId, out var currentJob)) - { - if (currentJob.Status == TestJobStatus.Running) - { - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - long staleCutoffMs = 5 * 60 * 1000; // 5 minutes - if (now - currentJob.LastUpdateUnixMs > staleCutoffMs) - { - McpLog.Warn($"[TestJobManager] Clearing stale job {_currentJobId} (last update {(now - currentJob.LastUpdateUnixMs) / 1000}s ago)"); - currentJob.Status = TestJobStatus.Failed; - currentJob.Error = "Job orphaned after domain reload"; - currentJob.FinishedUnixMs = now; - _currentJobId = null; - } - } - } - } - } - catch (Exception ex) - { - // Restoration is best-effort; never block editor load. - McpLog.Warn($"[TestJobManager] Failed to restore SessionState: {ex.Message}"); - } - } - - private static void PersistToSessionState(bool force = false) - { - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - - // Throttle non-critical updates to reduce overhead during large test runs - if (!force && (now - _lastPersistUnixMs) < MinPersistIntervalMs) - { - return; - } - - try - { - PersistedState snapshot; - lock (LockObj) - { - var jobs = Jobs.Values - .OrderByDescending(j => j.LastUpdateUnixMs) - .Take(MaxJobsToKeep) - .Select(j => new PersistedJob - { - job_id = j.JobId, - status = j.Status.ToString().ToLowerInvariant(), - mode = j.Mode, - started_unix_ms = j.StartedUnixMs, - finished_unix_ms = j.FinishedUnixMs, - last_update_unix_ms = j.LastUpdateUnixMs, - total_tests = j.TotalTests, - completed_tests = j.CompletedTests, - current_test_full_name = j.CurrentTestFullName, - current_test_started_unix_ms = j.CurrentTestStartedUnixMs, - last_finished_test_full_name = j.LastFinishedTestFullName, - last_finished_unix_ms = j.LastFinishedUnixMs, - failures_so_far = (j.FailuresSoFar ?? new List()).Take(FailureCap).ToList(), - error = j.Error - }) - .ToList(); - - snapshot = new PersistedState - { - current_job_id = _currentJobId, - jobs = jobs - }; - } - - SessionState.SetString(SessionKeyCurrentJobId, snapshot.current_job_id ?? string.Empty); - SessionState.SetString(SessionKeyJobs, JsonConvert.SerializeObject(snapshot)); - _lastPersistUnixMs = now; - } - catch (Exception ex) - { - McpLog.Warn($"[TestJobManager] Failed to persist SessionState: {ex.Message}"); - } - } - - public static string StartJob(TestMode mode, TestFilterOptions filterOptions = null) - { - string jobId = Guid.NewGuid().ToString("N"); - long started = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - string modeStr = mode.ToString(); - - var job = new TestJob - { - JobId = jobId, - Status = TestJobStatus.Running, - Mode = modeStr, - StartedUnixMs = started, - FinishedUnixMs = null, - LastUpdateUnixMs = started, - TotalTests = null, - CompletedTests = 0, - CurrentTestFullName = null, - CurrentTestStartedUnixMs = null, - LastFinishedTestFullName = null, - LastFinishedUnixMs = null, - FailuresSoFar = new List(), - Error = null, - Result = null - }; - - // Single lock scope for check-and-set to avoid TOCTOU race - lock (LockObj) - { - if (!string.IsNullOrEmpty(_currentJobId)) - { - throw new InvalidOperationException("A Unity test run is already in progress."); - } - Jobs[jobId] = job; - _currentJobId = jobId; - } - PersistToSessionState(force: true); - - // Kick the run (must be called on main thread; our command handlers already run there). - Task task = MCPServiceLocator.Tests.RunTestsAsync(mode, filterOptions); - - void FinalizeJob(Action finalize) - { - // Ensure state mutation happens on main thread to avoid Unity API surprises. - EditorApplication.delayCall += () => - { - try { finalize(); } - catch (Exception ex) { McpLog.Error($"[TestJobManager] Finalize failed: {ex.Message}\n{ex.StackTrace}"); } - }; - } - - task.ContinueWith(t => - { - // NOTE: We now finalize jobs deterministically from the TestRunnerService RunFinished callback. - // This continuation is retained as a safety net in case RunFinished is not delivered. - FinalizeJob(() => FinalizeFromTask(jobId, t)); - }, TaskScheduler.Default); - - return jobId; - } - - public static void FinalizeCurrentJobFromRunFinished(TestRunResult resultPayload) - { - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - lock (LockObj) - { - if (string.IsNullOrEmpty(_currentJobId) || !Jobs.TryGetValue(_currentJobId, out var job)) - { - return; - } - - job.LastUpdateUnixMs = now; - job.FinishedUnixMs = now; - job.Status = resultPayload != null && resultPayload.Failed > 0 - ? TestJobStatus.Failed - : TestJobStatus.Succeeded; - job.Error = null; - job.Result = resultPayload; - job.CurrentTestFullName = null; - _currentJobId = null; - } - PersistToSessionState(force: true); - } - - public static void OnRunStarted(int? totalTests) - { - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - lock (LockObj) - { - if (string.IsNullOrEmpty(_currentJobId) || !Jobs.TryGetValue(_currentJobId, out var job)) - { - return; - } - - job.LastUpdateUnixMs = now; - job.TotalTests = totalTests; - job.CompletedTests = 0; - job.CurrentTestFullName = null; - job.CurrentTestStartedUnixMs = null; - job.LastFinishedTestFullName = null; - job.LastFinishedUnixMs = null; - job.FailuresSoFar ??= new List(); - job.FailuresSoFar.Clear(); - } - PersistToSessionState(force: true); - } - - public static void OnTestStarted(string testFullName) - { - if (string.IsNullOrWhiteSpace(testFullName)) - { - return; - } - - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - lock (LockObj) - { - if (string.IsNullOrEmpty(_currentJobId) || !Jobs.TryGetValue(_currentJobId, out var job)) - { - return; - } - - job.LastUpdateUnixMs = now; - job.CurrentTestFullName = testFullName; - job.CurrentTestStartedUnixMs = now; - } - PersistToSessionState(); - } - - public static void OnLeafTestFinished(string testFullName, bool isFailure, string message) - { - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - lock (LockObj) - { - if (string.IsNullOrEmpty(_currentJobId) || !Jobs.TryGetValue(_currentJobId, out var job)) - { - return; - } - - job.LastUpdateUnixMs = now; - job.CompletedTests = Math.Max(0, job.CompletedTests + 1); - job.LastFinishedTestFullName = testFullName; - job.LastFinishedUnixMs = now; - - if (isFailure) - { - job.FailuresSoFar ??= new List(); - if (job.FailuresSoFar.Count < FailureCap) - { - job.FailuresSoFar.Add(new TestJobFailure - { - FullName = testFullName, - Message = string.IsNullOrWhiteSpace(message) ? "Test failed" : message - }); - } - } - } - PersistToSessionState(); - } - - public static void OnRunFinished() - { - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - lock (LockObj) - { - if (string.IsNullOrEmpty(_currentJobId) || !Jobs.TryGetValue(_currentJobId, out var job)) - { - return; - } - - job.LastUpdateUnixMs = now; - job.CurrentTestFullName = null; - } - PersistToSessionState(force: true); - } - - internal static TestJob GetJob(string jobId) - { - if (string.IsNullOrWhiteSpace(jobId)) - { - return null; - } - - TestJob jobToReturn = null; - bool shouldPersist = false; - lock (LockObj) - { - if (!Jobs.TryGetValue(jobId, out var job)) - { - return null; - } - - // Check if job is stuck in "running" state without having called OnRunStarted (TotalTests still null). - // This happens when tests fail to initialize (e.g., unsaved scene, compilation issues). - // After 15 seconds without initialization, auto-fail the job to prevent hanging. - if (job.Status == TestJobStatus.Running && job.TotalTests == null) - { - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - if (!EditorApplication.isCompiling && !EditorApplication.isUpdating && now - job.StartedUnixMs > InitializationTimeoutMs) - { - McpLog.Warn($"[TestJobManager] Job {jobId} failed to initialize within {InitializationTimeoutMs}ms, auto-failing"); - job.Status = TestJobStatus.Failed; - job.Error = "Test job failed to initialize (tests did not start within timeout)"; - job.FinishedUnixMs = now; - job.LastUpdateUnixMs = now; - if (_currentJobId == jobId) - { - _currentJobId = null; - } - shouldPersist = true; - } - } - - jobToReturn = job; - } - - if (shouldPersist) - { - PersistToSessionState(force: true); - } - return jobToReturn; - } - - internal static object ToSerializable(TestJob job, bool includeDetails, bool includeFailedTests) - { - if (job == null) - { - return null; - } - - object resultPayload = null; - if (job.Status == TestJobStatus.Succeeded && job.Result != null) - { - resultPayload = job.Result.ToSerializable(job.Mode, includeDetails, includeFailedTests); - } - - return new - { - job_id = job.JobId, - status = job.Status.ToString().ToLowerInvariant(), - mode = job.Mode, - started_unix_ms = job.StartedUnixMs, - finished_unix_ms = job.FinishedUnixMs, - last_update_unix_ms = job.LastUpdateUnixMs, - progress = new - { - completed = job.CompletedTests, - total = job.TotalTests, - current_test_full_name = job.CurrentTestFullName, - current_test_started_unix_ms = job.CurrentTestStartedUnixMs, - last_finished_test_full_name = job.LastFinishedTestFullName, - last_finished_unix_ms = job.LastFinishedUnixMs, - stuck_suspected = IsStuck(job), - editor_is_focused = InternalEditorUtility.isApplicationActive, - blocked_reason = GetBlockedReason(job), - failures_so_far = BuildFailuresPayload(job.FailuresSoFar), - failures_capped = (job.FailuresSoFar != null && job.FailuresSoFar.Count >= FailureCap) - }, - error = job.Error, - result = resultPayload - }; - } - - private static string GetBlockedReason(TestJob job) - { - if (job == null || job.Status != TestJobStatus.Running) - { - return null; - } - - if (!IsStuck(job)) - { - return null; - } - - // This matches the real-world symptom you observed: background Unity can get heavily throttled by OS/Editor. - if (!InternalEditorUtility.isApplicationActive) - { - return "editor_unfocused"; - } - - if (EditorApplication.isCompiling) - { - return "compiling"; - } - - if (EditorApplication.isUpdating) - { - return "asset_import"; - } - - return "unknown"; - } - - private static bool IsStuck(TestJob job) - { - if (job == null || job.Status != TestJobStatus.Running) - { - return false; - } - - if (string.IsNullOrWhiteSpace(job.CurrentTestFullName) || !job.CurrentTestStartedUnixMs.HasValue) - { - return false; - } - - long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - return (now - job.CurrentTestStartedUnixMs.Value) > StuckThresholdMs; - } - - private static object[] BuildFailuresPayload(List failures) - { - if (failures == null || failures.Count == 0) - { - return Array.Empty(); - } - - var list = new object[failures.Count]; - for (int i = 0; i < failures.Count; i++) - { - var f = failures[i]; - list[i] = new { full_name = f?.FullName, message = f?.Message }; - } - return list; - } - - private static void FinalizeFromTask(string jobId, Task task) - { - lock (LockObj) - { - if (!Jobs.TryGetValue(jobId, out var existing)) - { - if (_currentJobId == jobId) _currentJobId = null; - return; - } - - // If RunFinished already finalized the job, do nothing. - if (existing.Status != TestJobStatus.Running) - { - if (_currentJobId == jobId) _currentJobId = null; - return; - } - - existing.LastUpdateUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - existing.FinishedUnixMs = existing.LastUpdateUnixMs; - - if (task.IsFaulted) - { - existing.Status = TestJobStatus.Failed; - existing.Error = task.Exception?.GetBaseException()?.Message ?? "Unknown test job failure"; - existing.Result = null; - } - else if (task.IsCanceled) - { - existing.Status = TestJobStatus.Failed; - existing.Error = "Test job canceled"; - existing.Result = null; - } - else - { - var result = task.Result; - existing.Status = result != null && result.Failed > 0 - ? TestJobStatus.Failed - : TestJobStatus.Succeeded; - existing.Error = null; - existing.Result = result; - } - - if (_currentJobId == jobId) - { - _currentJobId = null; - } - } - PersistToSessionState(force: true); - } - } -} - diff --git a/Assets/MCPForUnity/Editor/Services/TestJobManager.cs.meta b/Assets/MCPForUnity/Editor/Services/TestJobManager.cs.meta deleted file mode 100644 index 1b08674..0000000 --- a/Assets/MCPForUnity/Editor/Services/TestJobManager.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 2d7a9b8c0e1f4a6b9c3d2e1f0a9b8c7d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/TestJobManager.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/TestRunStatus.cs b/Assets/MCPForUnity/Editor/Services/TestRunStatus.cs deleted file mode 100644 index da3ae6c..0000000 --- a/Assets/MCPForUnity/Editor/Services/TestRunStatus.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using UnityEditor.TestTools.TestRunner.Api; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Thread-safe, minimal shared status for Unity Test Runner execution. - /// Used by editor readiness snapshots so callers can avoid starting overlapping runs. - /// - internal static class TestRunStatus - { - private static readonly object LockObj = new(); - - private static bool _isRunning; - private static TestMode? _mode; - private static long? _startedUnixMs; - private static long? _finishedUnixMs; - - public static bool IsRunning - { - get { lock (LockObj) return _isRunning; } - } - - public static TestMode? Mode - { - get { lock (LockObj) return _mode; } - } - - public static long? StartedUnixMs - { - get { lock (LockObj) return _startedUnixMs; } - } - - public static long? FinishedUnixMs - { - get { lock (LockObj) return _finishedUnixMs; } - } - - public static void MarkStarted(TestMode mode) - { - lock (LockObj) - { - _isRunning = true; - _mode = mode; - _startedUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - _finishedUnixMs = null; - } - } - - public static void MarkFinished() - { - lock (LockObj) - { - _isRunning = false; - _finishedUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - _mode = null; - } - } - } -} - - diff --git a/Assets/MCPForUnity/Editor/Services/TestRunStatus.cs.meta b/Assets/MCPForUnity/Editor/Services/TestRunStatus.cs.meta deleted file mode 100644 index eae76f5..0000000 --- a/Assets/MCPForUnity/Editor/Services/TestRunStatus.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b3d140c288f6e4b6aa2b7e8181a09c1e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/TestRunStatus.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs b/Assets/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs deleted file mode 100644 index ddcfe87..0000000 --- a/Assets/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs +++ /dev/null @@ -1,150 +0,0 @@ -// TestRunnerNoThrottle.cs -// Sets Unity Editor to "No Throttling" mode during test runs. -// This helps tests that don't trigger compilation run smoothly in the background. -// Note: Tests that trigger mid-run compilation may still stall due to OS-level throttling. - -using System; -using System.Reflection; -using MCPForUnity.Editor.Helpers; -using UnityEditor; -using UnityEditor.TestTools.TestRunner.Api; -using UnityEngine; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Automatically sets the editor to "No Throttling" mode during test runs. - /// - /// This helps prevent background stalls for normal tests. However, tests that trigger - /// script compilation mid-run may still stall because: - /// - Internal Unity coroutine waits rely on editor ticks - /// - OS-level throttling affects the main thread when Unity is backgrounded - /// - No amount of internal nudging can overcome OS thread scheduling - /// - /// The MCP workflow is unaffected because socket messages provide external stimulus - /// that wakes Unity's main thread. - /// - [InitializeOnLoad] - public static class TestRunnerNoThrottle - { - private const string ApplicationIdleTimeKey = "ApplicationIdleTime"; - private const string InteractionModeKey = "InteractionMode"; - - // SessionState keys to persist across domain reload - private const string SessionKey_TestRunActive = "TestRunnerNoThrottle_TestRunActive"; - private const string SessionKey_PrevIdleTime = "TestRunnerNoThrottle_PrevIdleTime"; - private const string SessionKey_PrevInteractionMode = "TestRunnerNoThrottle_PrevInteractionMode"; - private const string SessionKey_SettingsCaptured = "TestRunnerNoThrottle_SettingsCaptured"; - - // Keep reference to avoid GC and set HideFlags to avoid serialization issues - private static TestRunnerApi _api; - - static TestRunnerNoThrottle() - { - try - { - _api = ScriptableObject.CreateInstance(); - _api.hideFlags = HideFlags.HideAndDontSave; - _api.RegisterCallbacks(new TestCallbacks()); - - // Check if recovering from domain reload during an active test run - if (IsTestRunActive()) - { - McpLog.Info("[TestRunnerNoThrottle] Recovered from domain reload - reapplying No Throttling."); - ApplyNoThrottling(); - } - } - catch (Exception e) - { - McpLog.Warn($"[TestRunnerNoThrottle] Failed to register callbacks: {e}"); - } - } - - #region State Persistence - - private static bool IsTestRunActive() => SessionState.GetBool(SessionKey_TestRunActive, false); - private static void SetTestRunActive(bool active) => SessionState.SetBool(SessionKey_TestRunActive, active); - private static bool AreSettingsCaptured() => SessionState.GetBool(SessionKey_SettingsCaptured, false); - private static void SetSettingsCaptured(bool captured) => SessionState.SetBool(SessionKey_SettingsCaptured, captured); - private static int GetPrevIdleTime() => SessionState.GetInt(SessionKey_PrevIdleTime, 4); - private static void SetPrevIdleTime(int value) => SessionState.SetInt(SessionKey_PrevIdleTime, value); - private static int GetPrevInteractionMode() => SessionState.GetInt(SessionKey_PrevInteractionMode, 0); - private static void SetPrevInteractionMode(int value) => SessionState.SetInt(SessionKey_PrevInteractionMode, value); - - #endregion - - /// - /// Apply no-throttling preemptively before tests start. - /// Call this before Execute() for PlayMode tests to ensure Unity isn't throttled - /// during the Play mode transition (before RunStarted fires). - /// - public static void ApplyNoThrottlingPreemptive() - { - SetTestRunActive(true); - ApplyNoThrottling(); - } - - private static void ApplyNoThrottling() - { - if (!AreSettingsCaptured()) - { - SetPrevIdleTime(EditorPrefs.GetInt(ApplicationIdleTimeKey, 4)); - SetPrevInteractionMode(EditorPrefs.GetInt(InteractionModeKey, 0)); - SetSettingsCaptured(true); - } - - // 0ms idle + InteractionMode=1 (No Throttling) - EditorPrefs.SetInt(ApplicationIdleTimeKey, 0); - EditorPrefs.SetInt(InteractionModeKey, 1); - - ForceEditorToApplyInteractionPrefs(); - McpLog.Info("[TestRunnerNoThrottle] Applied No Throttling for test run."); - } - - private static void RestoreThrottling() - { - if (!AreSettingsCaptured()) return; - - EditorPrefs.SetInt(ApplicationIdleTimeKey, GetPrevIdleTime()); - EditorPrefs.SetInt(InteractionModeKey, GetPrevInteractionMode()); - ForceEditorToApplyInteractionPrefs(); - - SetSettingsCaptured(false); - SetTestRunActive(false); - McpLog.Info("[TestRunnerNoThrottle] Restored Interaction Mode after test run."); - } - - private static void ForceEditorToApplyInteractionPrefs() - { - try - { - var method = typeof(EditorApplication).GetMethod( - "UpdateInteractionModeSettings", - BindingFlags.Static | BindingFlags.NonPublic - ); - method?.Invoke(null, null); - } - catch - { - // Ignore reflection errors - } - } - - private sealed class TestCallbacks : ICallbacks - { - public void RunStarted(ITestAdaptor testsToRun) - { - SetTestRunActive(true); - ApplyNoThrottling(); - } - - public void RunFinished(ITestResultAdaptor result) - { - RestoreThrottling(); - } - - public void TestStarted(ITestAdaptor test) { } - public void TestFinished(ITestResultAdaptor result) { } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs.meta b/Assets/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs.meta deleted file mode 100644 index 638c4bb..0000000 --- a/Assets/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 07a60b029782d464a9506fa520d2a8c8 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/TestRunnerNoThrottle.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/TestRunnerService.cs b/Assets/MCPForUnity/Editor/Services/TestRunnerService.cs deleted file mode 100644 index 374715b..0000000 --- a/Assets/MCPForUnity/Editor/Services/TestRunnerService.cs +++ /dev/null @@ -1,619 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MCPForUnity.Editor.Helpers; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEditor.TestTools.TestRunner.Api; -using UnityEngine; -using UnityEngine.SceneManagement; - -namespace MCPForUnity.Editor.Services -{ - /// - /// Concrete implementation of . - /// Coordinates Unity Test Runner operations and produces structured results. - /// - internal sealed class TestRunnerService : ITestRunnerService, ICallbacks, IDisposable - { - private static readonly TestMode[] AllModes = { TestMode.EditMode, TestMode.PlayMode }; - - private readonly TestRunnerApi _testRunnerApi; - private readonly SemaphoreSlim _operationLock = new SemaphoreSlim(1, 1); - private readonly List _leafResults = new List(); - private TaskCompletionSource _runCompletionSource; - - public TestRunnerService() - { - _testRunnerApi = ScriptableObject.CreateInstance(); - _testRunnerApi.RegisterCallbacks(this); - } - - public async Task>> GetTestsAsync(TestMode? mode) - { - await _operationLock.WaitAsync().ConfigureAwait(true); - try - { - var modes = mode.HasValue ? new[] { mode.Value } : AllModes; - - var results = new List>(); - var seen = new HashSet(StringComparer.Ordinal); - - foreach (var m in modes) - { - var root = await RetrieveTestRootAsync(m).ConfigureAwait(true); - if (root != null) - { - CollectFromNode(root, m, results, seen, new List()); - } - } - - return results; - } - finally - { - _operationLock.Release(); - } - } - - public async Task RunTestsAsync(TestMode mode, TestFilterOptions filterOptions = null) - { - await _operationLock.WaitAsync().ConfigureAwait(true); - Task runTask; - bool adjustedPlayModeOptions = false; - bool originalEnterPlayModeOptionsEnabled = false; - EnterPlayModeOptions originalEnterPlayModeOptions = EnterPlayModeOptions.None; - try - { - if (_runCompletionSource != null && !_runCompletionSource.Task.IsCompleted) - { - throw new InvalidOperationException("A Unity test run is already in progress."); - } - - if (EditorApplication.isPlaying || EditorApplication.isPlayingOrWillChangePlaymode) - { - throw new InvalidOperationException("Cannot start a test run while the Editor is in or entering Play Mode. Stop Play Mode and try again."); - } - - if (mode == TestMode.PlayMode) - { - // PlayMode runs transition the editor into play across multiple update ticks. Unity's - // built-in pipeline schedules SaveModifiedSceneTask early, but that task uses - // EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo which throws once play mode is - // active. To minimize that window we pre-save dirty scenes and disable domain reload (so the - // MCP bridge stays alive). We do NOT force runSynchronously here because that can freeze the - // editor in some projects. If the TestRunner still hits the save task after entering play, the - // run can fail; in that case, rerun from a clean Edit Mode state. - adjustedPlayModeOptions = EnsurePlayModeRunsWithoutDomainReload( - out originalEnterPlayModeOptionsEnabled, - out originalEnterPlayModeOptions); - } - - _leafResults.Clear(); - _runCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - // Mark running immediately so readiness snapshots reflect the busy state even before callbacks fire. - TestRunStatus.MarkStarted(mode); - - var filter = new Filter - { - testMode = mode, - testNames = filterOptions?.TestNames, - groupNames = filterOptions?.GroupNames, - categoryNames = filterOptions?.CategoryNames, - assemblyNames = filterOptions?.AssemblyNames - }; - var settings = new ExecutionSettings(filter); - - // Save dirty scenes for all test modes to prevent modal dialogs blocking MCP - // (Issue #525: EditMode tests were blocked by save dialog) - SaveDirtyScenesIfNeeded(); - - // Apply no-throttling preemptively for PlayMode tests. This ensures Unity - // isn't throttled during the Play mode transition (which requires multiple - // editor frames). Without this, unfocused Unity may never reach RunStarted - // where throttling would normally be disabled. - if (mode == TestMode.PlayMode) - { - TestRunnerNoThrottle.ApplyNoThrottlingPreemptive(); - } - - _testRunnerApi.Execute(settings); - - runTask = _runCompletionSource.Task; - } - catch - { - // Ensure the status is cleared if we failed to start the run. - TestRunStatus.MarkFinished(); - if (adjustedPlayModeOptions) - { - RestoreEnterPlayModeOptions(originalEnterPlayModeOptionsEnabled, originalEnterPlayModeOptions); - } - - _operationLock.Release(); - throw; - } - - try - { - return await runTask.ConfigureAwait(true); - } - finally - { - if (adjustedPlayModeOptions) - { - RestoreEnterPlayModeOptions(originalEnterPlayModeOptionsEnabled, originalEnterPlayModeOptions); - } - - _operationLock.Release(); - } - } - - public void Dispose() - { - try - { - _testRunnerApi?.UnregisterCallbacks(this); - } - catch - { - // Ignore cleanup errors - } - - if (_testRunnerApi != null) - { - ScriptableObject.DestroyImmediate(_testRunnerApi); - } - - _operationLock.Dispose(); - } - - #region TestRunnerApi callbacks - - public void RunStarted(ITestAdaptor testsToRun) - { - _leafResults.Clear(); - try - { - // Best-effort progress info for async polling (avoid heavy payloads). - int? total = null; - if (testsToRun != null) - { - total = CountLeafTests(testsToRun); - } - TestJobManager.OnRunStarted(total); - } - catch - { - TestJobManager.OnRunStarted(null); - } - } - - public void RunFinished(ITestResultAdaptor result) - { - // Always create payload and clean up job state, even if _runCompletionSource is null. - // This handles domain reload scenarios (e.g., PlayMode tests) where the TestRunnerService - // is recreated and _runCompletionSource is lost, but TestJobManager state persists via - // SessionState and the Test Runner still delivers the RunFinished callback. - var payload = TestRunResult.Create(result, _leafResults); - - // Clean up state regardless of _runCompletionSource - these methods safely handle - // the case where no MCP job exists (e.g., manual test runs via Unity UI). - TestRunStatus.MarkFinished(); - TestJobManager.OnRunFinished(); - TestJobManager.FinalizeCurrentJobFromRunFinished(payload); - - // Report result to awaiting caller if we have a completion source - if (_runCompletionSource != null) - { - _runCompletionSource.TrySetResult(payload); - _runCompletionSource = null; - } - } - - public void TestStarted(ITestAdaptor test) - { - try - { - // Prefer FullName for uniqueness; fall back to Name. - string fullName = test?.FullName; - if (string.IsNullOrWhiteSpace(fullName)) - { - fullName = test?.Name; - } - TestJobManager.OnTestStarted(fullName); - } - catch - { - // ignore - } - } - - public void TestFinished(ITestResultAdaptor result) - { - if (result == null) - { - return; - } - - if (!result.HasChildren) - { - _leafResults.Add(result); - try - { - string fullName = result.Test?.FullName; - if (string.IsNullOrWhiteSpace(fullName)) - { - fullName = result.Test?.Name; - } - - bool isFailure = false; - string message = null; - try - { - // NUnit outcomes are strings in the adaptor; keep it simple. - string outcome = result.ResultState; - if (!string.IsNullOrWhiteSpace(outcome)) - { - var o = outcome.Trim().ToLowerInvariant(); - isFailure = o.Contains("failed") || o.Contains("error"); - } - message = result.Message; - } - catch - { - // ignore adaptor quirks - } - - TestJobManager.OnLeafTestFinished(fullName, isFailure, message); - } - catch - { - // ignore - } - } - } - - #endregion - - private static int CountLeafTests(ITestAdaptor node) - { - if (node == null) - { - return 0; - } - - if (!node.HasChildren) - { - return 1; - } - - int total = 0; - try - { - foreach (var child in node.Children) - { - total += CountLeafTests(child); - } - } - catch - { - // If Unity changes the adaptor behavior, treat it as "unknown total". - return 0; - } - - return total; - } - - private static bool EnsurePlayModeRunsWithoutDomainReload( - out bool originalEnterPlayModeOptionsEnabled, - out EnterPlayModeOptions originalEnterPlayModeOptions) - { - originalEnterPlayModeOptionsEnabled = EditorSettings.enterPlayModeOptionsEnabled; - originalEnterPlayModeOptions = EditorSettings.enterPlayModeOptions; - - // When Play Mode triggers a domain reload, the MCP connection is torn down and the pending - // test run response never makes it back to the caller. To keep the bridge alive for this - // invocation, temporarily enable Enter Play Mode Options with domain reload disabled. - bool domainReloadDisabled = (originalEnterPlayModeOptions & EnterPlayModeOptions.DisableDomainReload) != 0; - bool needsChange = !originalEnterPlayModeOptionsEnabled || !domainReloadDisabled; - if (!needsChange) - { - return false; - } - - var desired = originalEnterPlayModeOptions | EnterPlayModeOptions.DisableDomainReload; - EditorSettings.enterPlayModeOptionsEnabled = true; - EditorSettings.enterPlayModeOptions = desired; - return true; - } - - private static void RestoreEnterPlayModeOptions(bool originalEnabled, EnterPlayModeOptions originalOptions) - { - EditorSettings.enterPlayModeOptions = originalOptions; - EditorSettings.enterPlayModeOptionsEnabled = originalEnabled; - } - - private static void SaveDirtyScenesIfNeeded() - { - int sceneCount = SceneManager.sceneCount; - for (int i = 0; i < sceneCount; i++) - { - var scene = SceneManager.GetSceneAt(i); - if (scene.isDirty) - { - if (string.IsNullOrEmpty(scene.path)) - { - McpLog.Warn($"[TestRunnerService] Skipping unsaved scene '{scene.name}': save it manually before running tests."); - continue; - } - try - { - EditorSceneManager.SaveScene(scene); - } - catch (Exception ex) - { - McpLog.Warn($"[TestRunnerService] Failed to save dirty scene '{scene.name}': {ex.Message}"); - } - } - } - } - - #region Test list helpers - - private async Task RetrieveTestRootAsync(TestMode mode) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - _testRunnerApi.RetrieveTestList(mode, root => - { - tcs.TrySetResult(root); - }); - - // Ensure the editor pumps at least one additional update in case the window is unfocused. - EditorApplication.QueuePlayerLoopUpdate(); - - var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromSeconds(30))).ConfigureAwait(true); - if (completed != tcs.Task) - { - McpLog.Warn($"[TestRunnerService] Timeout waiting for test retrieval callback for {mode}"); - return null; - } - - try - { - return await tcs.Task.ConfigureAwait(true); - } - catch (Exception ex) - { - McpLog.Error($"[TestRunnerService] Error retrieving tests for {mode}: {ex.Message}\n{ex.StackTrace}"); - return null; - } - } - - private static void CollectFromNode( - ITestAdaptor node, - TestMode mode, - List> output, - HashSet seen, - List path) - { - if (node == null) - { - return; - } - - bool hasName = !string.IsNullOrEmpty(node.Name); - if (hasName) - { - path.Add(node.Name); - } - - bool hasChildren = node.HasChildren && node.Children != null; - - if (!hasChildren) - { - string fullName = string.IsNullOrEmpty(node.FullName) ? node.Name ?? string.Empty : node.FullName; - string key = $"{mode}:{fullName}"; - - if (!string.IsNullOrEmpty(fullName) && seen.Add(key)) - { - string computedPath = path.Count > 0 ? string.Join("/", path) : fullName; - output.Add(new Dictionary - { - ["name"] = node.Name ?? fullName, - ["full_name"] = fullName, - ["path"] = computedPath, - ["mode"] = mode.ToString(), - }); - } - } - else if (node.Children != null) - { - foreach (var child in node.Children) - { - CollectFromNode(child, mode, output, seen, path); - } - } - - if (hasName && path.Count > 0) - { - path.RemoveAt(path.Count - 1); - } - } - - #endregion - } - - /// - /// Summary of a Unity test run. - /// - public sealed class TestRunResult - { - internal TestRunResult(TestRunSummary summary, IReadOnlyList results) - { - Summary = summary; - Results = results; - } - - public TestRunSummary Summary { get; } - public IReadOnlyList Results { get; } - - public int Total => Summary.Total; - public int Passed => Summary.Passed; - public int Failed => Summary.Failed; - public int Skipped => Summary.Skipped; - - public object ToSerializable(string mode, bool includeDetails = false, bool includeFailedTests = false) - { - // Determine which results to include - IEnumerable resultsToSerialize; - if (includeDetails) - { - // Include all test results - resultsToSerialize = Results.Select(r => r.ToSerializable()); - } - else if (includeFailedTests) - { - // Include only failed and skipped tests - resultsToSerialize = Results - .Where(r => !string.Equals(r.State, "Passed", StringComparison.OrdinalIgnoreCase)) - .Select(r => r.ToSerializable()); - } - else - { - // No individual test results - resultsToSerialize = null; - } - - return new - { - mode, - summary = Summary.ToSerializable(), - results = resultsToSerialize?.ToList(), - }; - } - - internal static TestRunResult Create(ITestResultAdaptor summary, IReadOnlyList tests) - { - var materializedTests = tests.Select(TestRunTestResult.FromAdaptor).ToList(); - - int passed = summary?.PassCount - ?? materializedTests.Count(t => string.Equals(t.State, "Passed", StringComparison.OrdinalIgnoreCase)); - int failed = summary?.FailCount - ?? materializedTests.Count(t => string.Equals(t.State, "Failed", StringComparison.OrdinalIgnoreCase)); - int skipped = summary?.SkipCount - ?? materializedTests.Count(t => string.Equals(t.State, "Skipped", StringComparison.OrdinalIgnoreCase)); - - double duration = summary?.Duration - ?? materializedTests.Sum(t => t.DurationSeconds); - - int total = summary != null ? passed + failed + skipped : materializedTests.Count; - - var summaryPayload = new TestRunSummary( - total, - passed, - failed, - skipped, - duration, - summary?.ResultState ?? "Unknown"); - - return new TestRunResult(summaryPayload, materializedTests); - } - } - - public sealed class TestRunSummary - { - internal TestRunSummary(int total, int passed, int failed, int skipped, double durationSeconds, string resultState) - { - Total = total; - Passed = passed; - Failed = failed; - Skipped = skipped; - DurationSeconds = durationSeconds; - ResultState = resultState; - } - - public int Total { get; } - public int Passed { get; } - public int Failed { get; } - public int Skipped { get; } - public double DurationSeconds { get; } - public string ResultState { get; } - - internal object ToSerializable() - { - return new - { - total = Total, - passed = Passed, - failed = Failed, - skipped = Skipped, - durationSeconds = DurationSeconds, - resultState = ResultState, - }; - } - } - - public sealed class TestRunTestResult - { - internal TestRunTestResult( - string name, - string fullName, - string state, - double durationSeconds, - string message, - string stackTrace, - string output) - { - Name = name; - FullName = fullName; - State = state; - DurationSeconds = durationSeconds; - Message = message; - StackTrace = stackTrace; - Output = output; - } - - public string Name { get; } - public string FullName { get; } - public string State { get; } - public double DurationSeconds { get; } - public string Message { get; } - public string StackTrace { get; } - public string Output { get; } - - internal object ToSerializable() - { - return new - { - name = Name, - fullName = FullName, - state = State, - durationSeconds = DurationSeconds, - message = Message, - stackTrace = StackTrace, - output = Output, - }; - } - - internal static TestRunTestResult FromAdaptor(ITestResultAdaptor adaptor) - { - if (adaptor == null) - { - return new TestRunTestResult(string.Empty, string.Empty, "Unknown", 0.0, string.Empty, string.Empty, string.Empty); - } - - return new TestRunTestResult( - adaptor.Name, - adaptor.FullName, - adaptor.ResultState, - adaptor.Duration, - adaptor.Message, - adaptor.StackTrace, - adaptor.Output); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/TestRunnerService.cs.meta b/Assets/MCPForUnity/Editor/Services/TestRunnerService.cs.meta deleted file mode 100644 index afb5bc2..0000000 --- a/Assets/MCPForUnity/Editor/Services/TestRunnerService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 18db1e25b13e14b0b9b186c751e397d0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/TestRunnerService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/ToolDiscoveryService.cs b/Assets/MCPForUnity/Editor/Services/ToolDiscoveryService.cs deleted file mode 100644 index b5b86c0..0000000 --- a/Assets/MCPForUnity/Editor/Services/ToolDiscoveryService.cs +++ /dev/null @@ -1,248 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Tools; -using UnityEditor; - -namespace MCPForUnity.Editor.Services -{ - public class ToolDiscoveryService : IToolDiscoveryService - { - private Dictionary _cachedTools; - - - public List DiscoverAllTools() - { - if (_cachedTools != null) - { - return _cachedTools.Values.ToList(); - } - - _cachedTools = new Dictionary(); - - var toolTypes = TypeCache.GetTypesWithAttribute(); - foreach (var type in toolTypes) - { - McpForUnityToolAttribute toolAttr; - try - { - toolAttr = type.GetCustomAttribute(); - } - catch (Exception ex) - { - McpLog.Warn($"Failed to read [McpForUnityTool] for {type.FullName}: {ex.Message}"); - continue; - } - - if (toolAttr == null) - { - continue; - } - - var metadata = ExtractToolMetadata(type, toolAttr); - if (metadata != null) - { - if (_cachedTools.ContainsKey(metadata.Name)) - { - McpLog.Warn($"Duplicate tool name '{metadata.Name}' from {type.FullName}; overwriting previous registration."); - } - _cachedTools[metadata.Name] = metadata; - EnsurePreferenceInitialized(metadata); - } - } - - McpLog.Info($"Discovered {_cachedTools.Count} MCP tools via reflection", false); - return _cachedTools.Values.ToList(); - } - - public ToolMetadata GetToolMetadata(string toolName) - { - if (_cachedTools == null) - { - DiscoverAllTools(); - } - - return _cachedTools.TryGetValue(toolName, out var metadata) ? metadata : null; - } - - public List GetEnabledTools() - { - return DiscoverAllTools() - .Where(tool => IsToolEnabled(tool.Name)) - .ToList(); - } - - public bool IsToolEnabled(string toolName) - { - if (string.IsNullOrEmpty(toolName)) - { - return false; - } - - string key = GetToolPreferenceKey(toolName); - if (EditorPrefs.HasKey(key)) - { - return EditorPrefs.GetBool(key, true); - } - - var metadata = GetToolMetadata(toolName); - return metadata?.AutoRegister ?? false; - } - - public void SetToolEnabled(string toolName, bool enabled) - { - if (string.IsNullOrEmpty(toolName)) - { - return; - } - - string key = GetToolPreferenceKey(toolName); - EditorPrefs.SetBool(key, enabled); - } - - private ToolMetadata ExtractToolMetadata(Type type, McpForUnityToolAttribute toolAttr) - { - try - { - // Get tool name - string toolName = toolAttr.Name; - if (string.IsNullOrEmpty(toolName)) - { - // Derive from class name: CaptureScreenshotTool -> capture_screenshot - toolName = ConvertToSnakeCase(type.Name.Replace("Tool", "")); - } - - // Get description - string description = toolAttr.Description ?? $"Tool: {toolName}"; - - // Extract parameters - var parameters = ExtractParameters(type); - - var metadata = new ToolMetadata - { - Name = toolName, - Description = description, - StructuredOutput = toolAttr.StructuredOutput, - Parameters = parameters, - ClassName = type.Name, - Namespace = type.Namespace ?? "", - AssemblyName = type.Assembly.GetName().Name, - AutoRegister = toolAttr.AutoRegister, - RequiresPolling = toolAttr.RequiresPolling, - PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction - }; - - metadata.IsBuiltIn = StringCaseUtility.IsBuiltInMcpType( - type, metadata.AssemblyName, "MCPForUnity.Editor.Tools"); - - return metadata; - - } - catch (Exception ex) - { - McpLog.Error($"Failed to extract metadata for {type.Name}: {ex.Message}"); - return null; - } - } - - private List ExtractParameters(Type type) - { - var parameters = new List(); - - // Look for nested Parameters class - var parametersType = type.GetNestedType("Parameters"); - if (parametersType == null) - { - return parameters; - } - - // Get all properties with [ToolParameter] - var properties = parametersType.GetProperties(BindingFlags.Public | BindingFlags.Instance); - - foreach (var prop in properties) - { - var paramAttr = prop.GetCustomAttribute(); - if (paramAttr == null) - continue; - - string paramName = prop.Name; - string paramType = GetParameterType(prop.PropertyType); - - parameters.Add(new ParameterMetadata - { - Name = paramName, - Description = paramAttr.Description, - Type = paramType, - Required = paramAttr.Required, - DefaultValue = paramAttr.DefaultValue - }); - } - - return parameters; - } - - private string GetParameterType(Type type) - { - // Handle nullable types - if (Nullable.GetUnderlyingType(type) != null) - { - type = Nullable.GetUnderlyingType(type); - } - - // Map C# types to JSON schema types - if (type == typeof(string)) - return "string"; - if (type == typeof(int) || type == typeof(long)) - return "integer"; - if (type == typeof(float) || type == typeof(double)) - return "number"; - if (type == typeof(bool)) - return "boolean"; - if (type.IsArray || typeof(System.Collections.IEnumerable).IsAssignableFrom(type)) - return "array"; - - return "object"; - } - - private string ConvertToSnakeCase(string input) => StringCaseUtility.ToSnakeCase(input); - - public void InvalidateCache() - { - _cachedTools = null; - } - - private void EnsurePreferenceInitialized(ToolMetadata metadata) - { - if (metadata == null || string.IsNullOrEmpty(metadata.Name)) - { - return; - } - - string key = GetToolPreferenceKey(metadata.Name); - if (!EditorPrefs.HasKey(key)) - { - bool defaultValue = metadata.AutoRegister || metadata.IsBuiltIn; - EditorPrefs.SetBool(key, defaultValue); - return; - } - - if (metadata.IsBuiltIn && !metadata.AutoRegister) - { - bool currentValue = EditorPrefs.GetBool(key, metadata.AutoRegister); - if (currentValue == metadata.AutoRegister) - { - EditorPrefs.SetBool(key, true); - } - } - } - - private static string GetToolPreferenceKey(string toolName) - { - return EditorPrefKeys.ToolEnabledPrefix + toolName; - } - - } -} diff --git a/Assets/MCPForUnity/Editor/Services/ToolDiscoveryService.cs.meta b/Assets/MCPForUnity/Editor/Services/ToolDiscoveryService.cs.meta deleted file mode 100644 index c8dcba1..0000000 --- a/Assets/MCPForUnity/Editor/Services/ToolDiscoveryService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: ec81a561be4c14c9cb243855d3273a94 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/ToolDiscoveryService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Transport.meta b/Assets/MCPForUnity/Editor/Services/Transport.meta deleted file mode 100644 index 58fe0d7..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 8d189635a5d364f55a810203798c09ba -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs b/Assets/MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs deleted file mode 100644 index 3d8584f..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Threading.Tasks; - -namespace MCPForUnity.Editor.Services.Transport -{ - /// - /// Abstraction for MCP transport implementations (e.g. WebSocket push, stdio). - /// - public interface IMcpTransportClient - { - bool IsConnected { get; } - string TransportName { get; } - TransportState State { get; } - - Task StartAsync(); - Task StopAsync(); - Task VerifyAsync(); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs.meta b/Assets/MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs.meta deleted file mode 100644 index 56a9007..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 042446a50a4744170bb294acf827376f -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs b/Assets/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs deleted file mode 100644 index 447db69..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs +++ /dev/null @@ -1,450 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Models; -using MCPForUnity.Editor.Services; -using MCPForUnity.Editor.Tools; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; - -namespace MCPForUnity.Editor.Services.Transport -{ - /// - /// Centralised command execution pipeline shared by all transport implementations. - /// Guarantees that MCP commands are executed on the Unity main thread while preserving - /// the legacy response format expected by the server. - /// - [InitializeOnLoad] - internal static class TransportCommandDispatcher - { - private static SynchronizationContext _mainThreadContext; - private static int _mainThreadId; - private static int _processingFlag; - - private sealed class PendingCommand - { - public PendingCommand( - string commandJson, - TaskCompletionSource completionSource, - CancellationToken cancellationToken, - CancellationTokenRegistration registration) - { - CommandJson = commandJson; - CompletionSource = completionSource; - CancellationToken = cancellationToken; - CancellationRegistration = registration; - QueuedAt = DateTime.UtcNow; - } - - public string CommandJson { get; } - public TaskCompletionSource CompletionSource { get; } - public CancellationToken CancellationToken { get; } - public CancellationTokenRegistration CancellationRegistration { get; } - public bool IsExecuting { get; set; } - public DateTime QueuedAt { get; } - - public void Dispose() - { - CancellationRegistration.Dispose(); - } - - public void TrySetResult(string payload) - { - CompletionSource.TrySetResult(payload); - } - - public void TrySetCanceled() - { - CompletionSource.TrySetCanceled(CancellationToken); - } - } - - private static readonly Dictionary Pending = new(); - private static readonly object PendingLock = new(); - private static bool updateHooked; - private static bool initialised; - - static TransportCommandDispatcher() - { - // Ensure this runs on the Unity main thread at editor load. - _mainThreadContext = SynchronizationContext.Current; - _mainThreadId = Thread.CurrentThread.ManagedThreadId; - - EnsureInitialised(); - - // Always keep the update hook installed so commands arriving from background - // websocket tasks don't depend on a background-thread event subscription. - if (!updateHooked) - { - updateHooked = true; - EditorApplication.update += ProcessQueue; - } - } - - /// - /// Schedule a command for execution on the Unity main thread and await its JSON response. - /// - public static Task ExecuteCommandJsonAsync(string commandJson, CancellationToken cancellationToken) - { - if (commandJson is null) - { - throw new ArgumentNullException(nameof(commandJson)); - } - - EnsureInitialised(); - - var id = Guid.NewGuid().ToString("N"); - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var registration = cancellationToken.CanBeCanceled - ? cancellationToken.Register(() => CancelPending(id, cancellationToken)) - : default; - - var pending = new PendingCommand(commandJson, tcs, cancellationToken, registration); - - lock (PendingLock) - { - Pending[id] = pending; - } - - // Proactively wake up the main thread execution loop. This improves responsiveness - // in scenarios where EditorApplication.update is throttled or temporarily not firing - // (e.g., Unity unfocused, compiling, or during domain reload transitions). - RequestMainThreadPump(); - - return tcs.Task; - } - - internal static Task RunOnMainThreadAsync(Func func, CancellationToken cancellationToken) - { - if (func is null) - { - throw new ArgumentNullException(nameof(func)); - } - - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - var registration = cancellationToken.CanBeCanceled - ? cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)) - : default; - - void Invoke() - { - try - { - if (tcs.Task.IsCompleted) - { - return; - } - - var result = func(); - tcs.TrySetResult(result); - } - catch (Exception ex) - { - tcs.TrySetException(ex); - } - finally - { - registration.Dispose(); - } - } - - // Best-effort nudge: if we're posting from a background thread (e.g., websocket receive), - // encourage Unity to run a loop iteration so the posted callback can execute even when unfocused. - try { EditorApplication.QueuePlayerLoopUpdate(); } catch { } - - if (_mainThreadContext != null && Thread.CurrentThread.ManagedThreadId != _mainThreadId) - { - _mainThreadContext.Post(_ => Invoke(), null); - return tcs.Task; - } - - Invoke(); - return tcs.Task; - } - - private static void RequestMainThreadPump() - { - void Pump() - { - try - { - // Hint Unity to run a loop iteration soon. - EditorApplication.QueuePlayerLoopUpdate(); - } - catch - { - // Best-effort only. - } - - ProcessQueue(); - } - - if (_mainThreadContext != null && Thread.CurrentThread.ManagedThreadId != _mainThreadId) - { - _mainThreadContext.Post(_ => Pump(), null); - return; - } - - Pump(); - } - - private static void EnsureInitialised() - { - if (initialised) - { - return; - } - - CommandRegistry.Initialize(); - initialised = true; - } - - private static void HookUpdate() - { - // Deprecated: we keep the update hook installed permanently (see static ctor). - if (updateHooked) return; - updateHooked = true; - EditorApplication.update += ProcessQueue; - } - - private static void UnhookUpdateIfIdle() - { - // Intentionally no-op: keep update hook installed so background commands always process. - // This avoids "must focus Unity to re-establish contact" edge cases. - return; - } - - private static void ProcessQueue() - { - if (Interlocked.Exchange(ref _processingFlag, 1) == 1) - { - return; - } - - try - { - List<(string id, PendingCommand pending)> ready; - - lock (PendingLock) - { - // Early exit inside lock to prevent per-frame List allocations (GitHub issue #577) - if (Pending.Count == 0) - { - return; - } - - ready = new List<(string, PendingCommand)>(Pending.Count); - foreach (var kvp in Pending) - { - if (kvp.Value.IsExecuting) - { - continue; - } - - kvp.Value.IsExecuting = true; - ready.Add((kvp.Key, kvp.Value)); - } - - if (ready.Count == 0) - { - UnhookUpdateIfIdle(); - return; - } - } - - foreach (var (id, pending) in ready) - { - ProcessCommand(id, pending); - } - } - finally - { - Interlocked.Exchange(ref _processingFlag, 0); - } - } - - private static void ProcessCommand(string id, PendingCommand pending) - { - if (pending.CancellationToken.IsCancellationRequested) - { - RemovePending(id, pending); - pending.TrySetCanceled(); - return; - } - - string commandText = pending.CommandJson?.Trim(); - if (string.IsNullOrEmpty(commandText)) - { - pending.TrySetResult(SerializeError("Empty command received")); - RemovePending(id, pending); - return; - } - - if (string.Equals(commandText, "ping", StringComparison.OrdinalIgnoreCase)) - { - var pingResponse = new - { - status = "success", - result = new { message = "pong" } - }; - pending.TrySetResult(JsonConvert.SerializeObject(pingResponse)); - RemovePending(id, pending); - return; - } - - if (!IsValidJson(commandText)) - { - var invalidJsonResponse = new - { - status = "error", - error = "Invalid JSON format", - receivedText = commandText.Length > 50 ? commandText[..50] + "..." : commandText - }; - pending.TrySetResult(JsonConvert.SerializeObject(invalidJsonResponse)); - RemovePending(id, pending); - return; - } - - try - { - var command = JsonConvert.DeserializeObject(commandText); - if (command == null) - { - pending.TrySetResult(SerializeError("Command deserialized to null", "Unknown", commandText)); - RemovePending(id, pending); - return; - } - - if (string.IsNullOrWhiteSpace(command.type)) - { - pending.TrySetResult(SerializeError("Command type cannot be empty")); - RemovePending(id, pending); - return; - } - - if (string.Equals(command.type, "ping", StringComparison.OrdinalIgnoreCase)) - { - var pingResponse = new - { - status = "success", - result = new { message = "pong" } - }; - pending.TrySetResult(JsonConvert.SerializeObject(pingResponse)); - RemovePending(id, pending); - return; - } - - var parameters = command.@params ?? new JObject(); - - // Block execution of disabled resources - var resourceMeta = MCPServiceLocator.ResourceDiscovery.GetResourceMetadata(command.type); - if (resourceMeta != null && !MCPServiceLocator.ResourceDiscovery.IsResourceEnabled(command.type)) - { - pending.TrySetResult(SerializeError( - $"Resource '{command.type}' is disabled in the Unity Editor.")); - RemovePending(id, pending); - return; - } - - // Block execution of disabled tools - var toolMeta = MCPServiceLocator.ToolDiscovery.GetToolMetadata(command.type); - if (toolMeta != null && !MCPServiceLocator.ToolDiscovery.IsToolEnabled(command.type)) - { - pending.TrySetResult(SerializeError( - $"Tool '{command.type}' is disabled in the Unity Editor.")); - RemovePending(id, pending); - return; - } - - var result = CommandRegistry.ExecuteCommand(command.type, parameters, pending.CompletionSource); - - if (result == null) - { - // Async command – cleanup after completion on next editor frame to preserve order. - pending.CompletionSource.Task.ContinueWith(_ => - { - EditorApplication.delayCall += () => RemovePending(id, pending); - }, TaskScheduler.Default); - return; - } - - var response = new { status = "success", result }; - pending.TrySetResult(JsonConvert.SerializeObject(response)); - RemovePending(id, pending); - } - catch (Exception ex) - { - McpLog.Error($"Error processing command: {ex.Message}\n{ex.StackTrace}"); - pending.TrySetResult(SerializeError(ex.Message, "Unknown (error during processing)", ex.StackTrace)); - RemovePending(id, pending); - } - } - - private static void CancelPending(string id, CancellationToken token) - { - PendingCommand pending = null; - lock (PendingLock) - { - if (Pending.Remove(id, out pending)) - { - UnhookUpdateIfIdle(); - } - } - - pending?.TrySetCanceled(); - pending?.Dispose(); - } - - private static void RemovePending(string id, PendingCommand pending) - { - lock (PendingLock) - { - Pending.Remove(id); - UnhookUpdateIfIdle(); - } - - pending.Dispose(); - } - - private static string SerializeError(string message, string commandType = null, string stackTrace = null) - { - var errorResponse = new - { - status = "error", - error = message, - command = commandType ?? "Unknown", - stackTrace - }; - return JsonConvert.SerializeObject(errorResponse); - } - - private static bool IsValidJson(string text) - { - if (string.IsNullOrWhiteSpace(text)) - { - return false; - } - - text = text.Trim(); - if ((text.StartsWith("{") && text.EndsWith("}")) || (text.StartsWith("[") && text.EndsWith("]"))) - { - try - { - JToken.Parse(text); - return true; - } - catch - { - return false; - } - } - - return false; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs.meta b/Assets/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs.meta deleted file mode 100644 index f8dae48..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 27407cc9c1ea0412d80b9f8964a5a29d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Transport/TransportManager.cs b/Assets/MCPForUnity/Editor/Services/Transport/TransportManager.cs deleted file mode 100644 index 1204e70..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/TransportManager.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Threading.Tasks; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services.Transport.Transports; - -namespace MCPForUnity.Editor.Services.Transport -{ - /// - /// Coordinates the active transport client and exposes lifecycle helpers. - /// - public class TransportManager - { - private IMcpTransportClient _httpClient; - private IMcpTransportClient _stdioClient; - private TransportState _httpState = TransportState.Disconnected("http"); - private TransportState _stdioState = TransportState.Disconnected("stdio"); - private Func _webSocketFactory; - private Func _stdioFactory; - - public TransportManager() - { - Configure( - () => new WebSocketTransportClient(MCPServiceLocator.ToolDiscovery), - () => new StdioTransportClient()); - } - - public void Configure( - Func webSocketFactory, - Func stdioFactory) - { - _webSocketFactory = webSocketFactory ?? throw new ArgumentNullException(nameof(webSocketFactory)); - _stdioFactory = stdioFactory ?? throw new ArgumentNullException(nameof(stdioFactory)); - } - - private IMcpTransportClient GetOrCreateClient(TransportMode mode) - { - return mode switch - { - TransportMode.Http => _httpClient ??= _webSocketFactory(), - TransportMode.Stdio => _stdioClient ??= _stdioFactory(), - _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported transport mode"), - }; - } - - private IMcpTransportClient GetClient(TransportMode mode) - { - return mode switch - { - TransportMode.Http => _httpClient, - TransportMode.Stdio => _stdioClient, - _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported transport mode"), - }; - } - - public async Task StartAsync(TransportMode mode) - { - IMcpTransportClient client = GetOrCreateClient(mode); - - bool started = await client.StartAsync(); - if (!started) - { - try - { - await client.StopAsync(); - } - catch (Exception ex) - { - McpLog.Warn($"Error while stopping transport {client.TransportName}: {ex.Message}"); - } - UpdateState(mode, TransportState.Disconnected(client.TransportName, client.State?.Error ?? "Failed to start")); - return false; - } - - UpdateState(mode, client.State ?? TransportState.Connected(client.TransportName)); - return true; - } - - public async Task StopAsync(TransportMode? mode = null) - { - async Task StopClient(IMcpTransportClient client, TransportMode clientMode) - { - if (client == null) return; - try { await client.StopAsync(); } - catch (Exception ex) { McpLog.Warn($"Error while stopping transport {client.TransportName}: {ex.Message}"); } - finally { UpdateState(clientMode, TransportState.Disconnected(client.TransportName)); } - } - - if (mode == null) - { - await StopClient(_httpClient, TransportMode.Http); - await StopClient(_stdioClient, TransportMode.Stdio); - return; - } - - if (mode == TransportMode.Http) - { - await StopClient(_httpClient, TransportMode.Http); - } - else - { - await StopClient(_stdioClient, TransportMode.Stdio); - } - } - - public async Task VerifyAsync(TransportMode mode) - { - IMcpTransportClient client = GetClient(mode); - if (client == null) - { - return false; - } - - bool ok = await client.VerifyAsync(); - var state = client.State ?? TransportState.Disconnected(client.TransportName, "No state reported"); - UpdateState(mode, state); - return ok; - } - - public TransportState GetState(TransportMode mode) - { - return mode switch - { - TransportMode.Http => _httpState, - TransportMode.Stdio => _stdioState, - _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported transport mode"), - }; - } - - public bool IsRunning(TransportMode mode) => GetState(mode).IsConnected; - - private void UpdateState(TransportMode mode, TransportState state) - { - switch (mode) - { - case TransportMode.Http: - _httpState = state; - break; - case TransportMode.Stdio: - _stdioState = state; - break; - default: - throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported transport mode"); - } - } - } - - public enum TransportMode - { - Http, - Stdio - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Transport/TransportManager.cs.meta b/Assets/MCPForUnity/Editor/Services/Transport/TransportManager.cs.meta deleted file mode 100644 index 9cc4320..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/TransportManager.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 65fc8ff4c9efb4fc98a0910ba7ca8b02 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Transport/TransportManager.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Transport/TransportState.cs b/Assets/MCPForUnity/Editor/Services/Transport/TransportState.cs deleted file mode 100644 index 7fb6f20..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/TransportState.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace MCPForUnity.Editor.Services.Transport -{ - /// - /// Lightweight snapshot of a transport's runtime status for editor UI and diagnostics. - /// - public sealed class TransportState - { - public bool IsConnected { get; } - public string TransportName { get; } - public int? Port { get; } - public string SessionId { get; } - public string Details { get; } - public string Error { get; } - - private TransportState( - bool isConnected, - string transportName, - int? port, - string sessionId, - string details, - string error) - { - IsConnected = isConnected; - TransportName = transportName; - Port = port; - SessionId = sessionId; - Details = details; - Error = error; - } - - public static TransportState Connected( - string transportName, - int? port = null, - string sessionId = null, - string details = null) - => new TransportState(true, transportName, port, sessionId, details, null); - - public static TransportState Disconnected( - string transportName, - string error = null, - int? port = null) - => new TransportState(false, transportName, port, null, null, error); - - public TransportState WithError(string error) => new TransportState( - IsConnected, - TransportName, - Port, - SessionId, - Details, - error); - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Transport/TransportState.cs.meta b/Assets/MCPForUnity/Editor/Services/Transport/TransportState.cs.meta deleted file mode 100644 index d89732a..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/TransportState.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 67ab8e43f6a804698bb5b216cdef0645 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Transport/TransportState.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Transport/Transports.meta b/Assets/MCPForUnity/Editor/Services/Transport/Transports.meta deleted file mode 100644 index 878b705..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/Transports.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 3d467a63b6fad42fa975c731af4b83b3 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs b/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs deleted file mode 100644 index ab3af5d..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs +++ /dev/null @@ -1,1095 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Models; -using MCPForUnity.Editor.Services; -using MCPForUnity.Editor.Services.Transport; -using MCPForUnity.Editor.Tools; -using MCPForUnity.Editor.Tools.Prefabs; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Services.Transport.Transports -{ - class Outbound - { - public byte[] Payload; - public string Tag; - public int? ReqId; - } - - class QueuedCommand - { - public string CommandJson; - public TaskCompletionSource Tcs; - public bool IsExecuting; - } - - [InitializeOnLoad] - public static class StdioBridgeHost - { - private static TcpListener listener; - private static bool isRunning = false; - private static readonly object lockObj = new(); - private static readonly object startStopLock = new(); - private static readonly object clientsLock = new(); - private static readonly HashSet activeClients = new(); - private static readonly BlockingCollection _outbox = new(new ConcurrentQueue()); - private static CancellationTokenSource cts; - private static Task listenerTask; - private static int processingCommands = 0; - private static bool initScheduled = false; - private static bool ensureUpdateHooked = false; - private static bool isStarting = false; - private static double nextStartAt = 0.0f; - private static double nextHeartbeatAt = 0.0f; - private static int heartbeatSeq = 0; - private static Dictionary commandQueue = new(); - private static int mainThreadId; - private static int currentUnityPort = 6400; - private static bool isAutoConnectMode = false; - private const ulong MaxFrameBytes = 64UL * 1024 * 1024; - private const int FrameIOTimeoutMs = 30000; - - private static long _ioSeq = 0; - private static void IoInfo(string s) { McpLog.Info(s, always: false); } - - private static bool IsDebugEnabled() - { - try { return EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false); } catch { return false; } - } - - private static void LogBreadcrumb(string stage) - { - if (IsDebugEnabled()) - { - McpLog.Info($"[{stage}]", always: false); - } - } - - public static bool IsRunning => isRunning; - public static int GetCurrentPort() => currentUnityPort; - public static bool IsAutoConnectMode() => isAutoConnectMode; - - public static void StartAutoConnect() - { - Stop(); - - try - { - currentUnityPort = PortManager.GetPortWithFallback(); - Start(); - isAutoConnectMode = true; - - TelemetryHelper.RecordBridgeStartup(); - } - catch (Exception ex) - { - McpLog.Error($"Auto-connect failed: {ex.Message}"); - TelemetryHelper.RecordBridgeConnection(false, ex.Message); - throw; - } - } - - public static bool FolderExists(string path) - { - if (string.IsNullOrEmpty(path)) - { - return false; - } - - if (path.Equals("Assets", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - string fullPath = Path.Combine( - Application.dataPath, - path.StartsWith("Assets/") ? path[7..] : path - ); - return Directory.Exists(fullPath); - } - - static StdioBridgeHost() - { - try { mainThreadId = Thread.CurrentThread.ManagedThreadId; } catch { mainThreadId = 0; } - try - { - var writerThread = new Thread(() => - { - foreach (var item in _outbox.GetConsumingEnumerable()) - { - try - { - long seq = Interlocked.Increment(ref _ioSeq); - IoInfo($"[IO] ➜ write start seq={seq} tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")}"); - var sw = System.Diagnostics.Stopwatch.StartNew(); - sw.Stop(); - IoInfo($"[IO] ✓ write end tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")} durMs={sw.Elapsed.TotalMilliseconds:F1}"); - } - catch (Exception ex) - { - IoInfo($"[IO] ✗ write FAIL tag={item.Tag} reqId={(item.ReqId?.ToString() ?? "?")} {ex.GetType().Name}: {ex.Message}"); - } - } - }) - { IsBackground = true, Name = "MCP-Writer" }; - writerThread.Start(); - } - catch { } - - if (Application.isBatchMode && string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("UNITY_MCP_ALLOW_BATCH"))) - { - return; - } - if (ShouldAutoStartBridge()) - { - ScheduleInitRetry(); - if (!ensureUpdateHooked) - { - ensureUpdateHooked = true; - EditorApplication.update += EnsureStartedOnEditorIdle; - } - } - EditorApplication.quitting += Stop; - EditorApplication.playModeStateChanged += _ => - { - if (ShouldAutoStartBridge()) - { - ScheduleInitRetry(); - } - }; - } - - private static void InitializeAfterCompilation() - { - initScheduled = false; - - if (IsCompiling()) - { - ScheduleInitRetry(); - return; - } - - if (!isRunning) - { - Start(); - if (!isRunning) - { - ScheduleInitRetry(); - } - } - } - - private static void ScheduleInitRetry() - { - if (initScheduled) - { - return; - } - initScheduled = true; - nextStartAt = EditorApplication.timeSinceStartup + 0.20f; - if (!ensureUpdateHooked) - { - ensureUpdateHooked = true; - EditorApplication.update += EnsureStartedOnEditorIdle; - } - EditorApplication.delayCall += InitializeAfterCompilation; - } - - private static bool ShouldAutoStartBridge() - { - try - { - bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport; - return !useHttpTransport; - } - catch - { - return true; - } - } - - private static void EnsureStartedOnEditorIdle() - { - if (IsCompiling()) - { - return; - } - - if (isRunning) - { - EditorApplication.update -= EnsureStartedOnEditorIdle; - ensureUpdateHooked = false; - return; - } - - if (nextStartAt > 0 && EditorApplication.timeSinceStartup < nextStartAt) - { - return; - } - - if (isStarting) - { - return; - } - - isStarting = true; - try - { - Start(); - } - finally - { - isStarting = false; - } - if (isRunning) - { - EditorApplication.update -= EnsureStartedOnEditorIdle; - ensureUpdateHooked = false; - } - } - - private static bool IsCompiling() - { - if (EditorApplication.isCompiling) - { - return true; - } - try - { - Type pipeline = Type.GetType("UnityEditor.Compilation.CompilationPipeline, UnityEditor"); - var prop = pipeline?.GetProperty("isCompiling", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); - if (prop != null) - { - return (bool)prop.GetValue(null); - } - } - catch { } - return false; - } - - public static void Start() - { - lock (startStopLock) - { - if (isRunning && listener != null) - { - if (IsDebugEnabled()) - { - McpLog.Info($"StdioBridgeHost already running on port {currentUnityPort}"); - } - return; - } - - Stop(); - - try - { - currentUnityPort = PortManager.GetPortWithFallback(); - - LogBreadcrumb("Start"); - - const int maxImmediateRetries = 3; - const int retrySleepMs = 75; - int attempt = 0; - for (; ; ) - { - try - { - listener = CreateConfiguredListener(currentUnityPort); - listener.Start(); - break; - } - catch (SocketException se) when (se.SocketErrorCode == SocketError.AddressAlreadyInUse && attempt < maxImmediateRetries) - { - attempt++; - Thread.Sleep(retrySleepMs); - continue; - } - catch (SocketException se) when (se.SocketErrorCode == SocketError.AddressAlreadyInUse && attempt >= maxImmediateRetries) - { - int oldPort = currentUnityPort; - - // Before switching ports, give the old one a brief chance to release if it looks like ours - try - { - if (PortManager.IsPortUsedByMCPForUnity(oldPort)) - { - const int waitStepMs = 100; - int waited = 0; - while (waited < 300 && !PortManager.IsPortAvailable(oldPort)) - { - Thread.Sleep(waitStepMs); - waited += waitStepMs; - } - } - } - catch { } - - currentUnityPort = PortManager.DiscoverNewPort(); - - // Persist the new port so next time we start on this port - try - { - EditorPrefs.SetInt(EditorPrefKeys.UnitySocketPort, currentUnityPort); - } - catch { } - - if (IsDebugEnabled()) - { - if (currentUnityPort == oldPort) - { - McpLog.Info($"Port {oldPort} became available, proceeding"); - } - else - { - McpLog.Info($"Port {oldPort} occupied, switching to port {currentUnityPort}"); - } - } - - listener = CreateConfiguredListener(currentUnityPort); - listener.Start(); - break; - } - } - - isRunning = true; - isAutoConnectMode = false; - string platform = Application.platform.ToString(); - string serverVer = AssetPathUtility.GetPackageVersion(); - McpLog.Info($"StdioBridgeHost started on port {currentUnityPort}. (OS={platform}, server={serverVer})"); - cts = new CancellationTokenSource(); - listenerTask = Task.Run(() => ListenerLoopAsync(cts.Token)); - CommandRegistry.Initialize(); - EditorApplication.update += ProcessCommands; - try { EditorApplication.quitting -= Stop; } catch { } - try { EditorApplication.quitting += Stop; } catch { } - heartbeatSeq++; - WriteHeartbeat(false, "ready"); - nextHeartbeatAt = EditorApplication.timeSinceStartup + 0.5f; - } - catch (SocketException ex) - { - McpLog.Error($"Failed to start TCP listener: {ex.Message}"); - } - } - } - - private static TcpListener CreateConfiguredListener(int port) - { - var newListener = new TcpListener(IPAddress.Loopback, port); -#if !UNITY_EDITOR_OSX - newListener.Server.SetSocketOption( - SocketOptionLevel.Socket, - SocketOptionName.ReuseAddress, - true - ); -#endif -#if UNITY_EDITOR_WIN - try - { - newListener.ExclusiveAddressUse = false; - } - catch { } -#endif - try - { - newListener.Server.LingerState = new LingerOption(true, 0); - } - catch (Exception) - { - } - return newListener; - } - - public static void Stop() - { - Task toWait = null; - lock (startStopLock) - { - if (!isRunning) - { - return; - } - - try - { - isRunning = false; - - var cancel = cts; - cts = null; - try { cancel?.Cancel(); } catch { } - - try { listener?.Stop(); } catch { } - listener = null; - - toWait = listenerTask; - listenerTask = null; - } - catch (Exception ex) - { - McpLog.Error($"Error stopping StdioBridgeHost: {ex.Message}"); - } - } - - TcpClient[] toClose; - lock (clientsLock) - { - toClose = activeClients.ToArray(); - activeClients.Clear(); - } - foreach (var c in toClose) - { - try { c.Close(); } catch { } - } - - if (toWait != null) - { - try { toWait.Wait(100); } catch { } - } - - try { EditorApplication.update -= ProcessCommands; } catch { } - try { EditorApplication.quitting -= Stop; } catch { } - - try - { - string dir = Environment.GetEnvironmentVariable("UNITY_MCP_STATUS_DIR"); - if (string.IsNullOrWhiteSpace(dir)) - { - dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp"); - } - string statusFile = Path.Combine(dir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json"); - if (File.Exists(statusFile)) - { - File.Delete(statusFile); - if (IsDebugEnabled()) McpLog.Info($"Deleted status file: {statusFile}"); - } - } - catch (Exception ex) - { - if (IsDebugEnabled()) McpLog.Warn($"Failed to delete status file: {ex.Message}"); - } - - if (IsDebugEnabled()) McpLog.Info("StdioBridgeHost stopped."); - } - - private static async Task ListenerLoopAsync(CancellationToken token) - { - while (isRunning && !token.IsCancellationRequested) - { - try - { - TcpClient client = await listener.AcceptTcpClientAsync(); - client.Client.SetSocketOption( - SocketOptionLevel.Socket, - SocketOptionName.KeepAlive, - true - ); - - client.ReceiveTimeout = 60000; - - _ = Task.Run(() => HandleClientAsync(client, token), token); - } - catch (ObjectDisposedException) - { - if (!isRunning || token.IsCancellationRequested) - { - break; - } - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - if (isRunning && !token.IsCancellationRequested) - { - if (IsDebugEnabled()) McpLog.Error($"Listener error: {ex.Message}"); - } - } - } - } - - private static async Task HandleClientAsync(TcpClient client, CancellationToken token) - { - using (client) - using (NetworkStream stream = client.GetStream()) - { - lock (clientsLock) { activeClients.Add(client); } - try - { - try - { - if (IsDebugEnabled()) - { - var ep = client.Client?.RemoteEndPoint?.ToString() ?? "unknown"; - McpLog.Info($"Client connected {ep}"); - } - } - catch { } - try - { - client.NoDelay = true; - } - catch { } - try - { - string handshake = "WELCOME UNITY-MCP 1 FRAMING=1\n"; - byte[] handshakeBytes = System.Text.Encoding.ASCII.GetBytes(handshake); - using var cts = new CancellationTokenSource(FrameIOTimeoutMs); -#if NETSTANDARD2_1 || NET6_0_OR_GREATER - await stream.WriteAsync(handshakeBytes.AsMemory(0, handshakeBytes.Length), cts.Token).ConfigureAwait(false); -#else - await stream.WriteAsync(handshakeBytes, 0, handshakeBytes.Length, cts.Token).ConfigureAwait(false); -#endif - if (IsDebugEnabled()) McpLog.Info("Sent handshake FRAMING=1 (strict)", always: false); - } - catch (Exception ex) - { - if (IsDebugEnabled()) McpLog.Warn($"Handshake failed: {ex.Message}"); - return; - } - - while (isRunning && !token.IsCancellationRequested) - { - try - { - string commandText = await ReadFrameAsUtf8Async(stream, FrameIOTimeoutMs, token).ConfigureAwait(false); - - try - { - if (IsDebugEnabled()) - { - var preview = commandText.Length > 120 ? commandText.Substring(0, 120) + "…" : commandText; - McpLog.Info($"recv framed: {preview}", always: false); - } - } - catch { } - string commandId = Guid.NewGuid().ToString(); - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - if (commandText.Trim() == "ping") - { - byte[] pingResponseBytes = System.Text.Encoding.UTF8.GetBytes( - "{\"status\":\"success\",\"result\":{\"message\":\"pong\"}}" - ); - await WriteFrameAsync(stream, pingResponseBytes); - continue; - } - - lock (lockObj) - { - commandQueue[commandId] = new QueuedCommand - { - CommandJson = commandText, - Tcs = tcs, - IsExecuting = false - }; - } - - string response; - try - { - using var respCts = new CancellationTokenSource(FrameIOTimeoutMs); - var completed = await Task.WhenAny(tcs.Task, Task.Delay(FrameIOTimeoutMs, respCts.Token)).ConfigureAwait(false); - if (completed == tcs.Task) - { - respCts.Cancel(); - response = tcs.Task.Result; - } - else - { - var timeoutResponse = new - { - status = "error", - error = $"Command processing timed out after {FrameIOTimeoutMs} ms", - }; - response = JsonConvert.SerializeObject(timeoutResponse); - } - } - catch (Exception ex) - { - var errorResponse = new - { - status = "error", - error = ex.Message, - }; - response = JsonConvert.SerializeObject(errorResponse); - } - - if (IsDebugEnabled()) - { - try { McpLog.Info("[MCP] sending framed response", always: false); } catch { } - } - long seq = Interlocked.Increment(ref _ioSeq); - byte[] responseBytes; - try - { - responseBytes = System.Text.Encoding.UTF8.GetBytes(response); - IoInfo($"[IO] ➜ write start seq={seq} tag=response len={responseBytes.Length} reqId=?"); - } - catch (Exception ex) - { - IoInfo($"[IO] ✗ serialize FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}"); - throw; - } - - var swDirect = System.Diagnostics.Stopwatch.StartNew(); - try - { - await WriteFrameAsync(stream, responseBytes); - swDirect.Stop(); - IoInfo($"[IO] ✓ write end tag=response len={responseBytes.Length} reqId=? durMs={swDirect.Elapsed.TotalMilliseconds:F1}"); - } - catch (Exception ex) - { - IoInfo($"[IO] ✗ write FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}"); - throw; - } - } - catch (Exception ex) - { - string msg = ex.Message ?? string.Empty; - bool isBenign = - msg.IndexOf("Connection closed before reading expected bytes", StringComparison.OrdinalIgnoreCase) >= 0 - || msg.IndexOf("Read timed out", StringComparison.OrdinalIgnoreCase) >= 0 - || ex is IOException; - if (isBenign) - { - if (IsDebugEnabled()) McpLog.Info($"Client handler: {msg}", always: false); - } - else - { - McpLog.Error($"Client handler error: {msg}"); - } - break; - } - } - } - finally - { - lock (clientsLock) { activeClients.Remove(client); } - } - } - } - - private static async Task ReadExactAsync(NetworkStream stream, int count, int timeoutMs, CancellationToken cancel = default) - { - byte[] buffer = new byte[count]; - int offset = 0; - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - - while (offset < count) - { - int remaining = count - offset; - int remainingTimeout = timeoutMs <= 0 - ? Timeout.Infinite - : timeoutMs - (int)stopwatch.ElapsedMilliseconds; - - if (remainingTimeout != Timeout.Infinite && remainingTimeout <= 0) - { - throw new IOException("Read timed out"); - } - - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancel); - if (remainingTimeout != Timeout.Infinite) - { - cts.CancelAfter(remainingTimeout); - } - - try - { -#if NETSTANDARD2_1 || NET6_0_OR_GREATER - int read = await stream.ReadAsync(buffer.AsMemory(offset, remaining), cts.Token).ConfigureAwait(false); -#else - int read = await stream.ReadAsync(buffer, offset, remaining, cts.Token).ConfigureAwait(false); -#endif - if (read == 0) - { - throw new IOException("Connection closed before reading expected bytes"); - } - offset += read; - } - catch (OperationCanceledException) when (!cancel.IsCancellationRequested) - { - throw new IOException("Read timed out"); - } - } - - return buffer; - } - - private static Task WriteFrameAsync(NetworkStream stream, byte[] payload) - { - using var cts = new CancellationTokenSource(FrameIOTimeoutMs); - return WriteFrameAsync(stream, payload, cts.Token); - } - - private static async Task WriteFrameAsync(NetworkStream stream, byte[] payload, CancellationToken cancel) - { - if (payload == null) - { - throw new ArgumentNullException(nameof(payload)); - } - if ((ulong)payload.LongLength > MaxFrameBytes) - { - throw new IOException($"Frame too large: {payload.LongLength}"); - } - byte[] header = new byte[8]; - WriteUInt64BigEndian(header, (ulong)payload.LongLength); -#if NETSTANDARD2_1 || NET6_0_OR_GREATER - await stream.WriteAsync(header.AsMemory(0, header.Length), cancel).ConfigureAwait(false); - await stream.WriteAsync(payload.AsMemory(0, payload.Length), cancel).ConfigureAwait(false); -#else - await stream.WriteAsync(header, 0, header.Length, cancel).ConfigureAwait(false); - await stream.WriteAsync(payload, 0, payload.Length, cancel).ConfigureAwait(false); -#endif - } - - private static async Task ReadFrameAsUtf8Async(NetworkStream stream, int timeoutMs, CancellationToken cancel) - { - byte[] header = await ReadExactAsync(stream, 8, timeoutMs, cancel).ConfigureAwait(false); - ulong payloadLen = ReadUInt64BigEndian(header); - if (payloadLen > MaxFrameBytes) - { - throw new IOException($"Invalid framed length: {payloadLen}"); - } - if (payloadLen == 0UL) - throw new IOException("Zero-length frames are not allowed"); - if (payloadLen > int.MaxValue) - { - throw new IOException("Frame too large for buffer"); - } - int count = (int)payloadLen; - byte[] payload = await ReadExactAsync(stream, count, timeoutMs, cancel).ConfigureAwait(false); - return System.Text.Encoding.UTF8.GetString(payload); - } - - private static ulong ReadUInt64BigEndian(byte[] buffer) - { - if (buffer == null || buffer.Length < 8) return 0UL; - return ((ulong)buffer[0] << 56) - | ((ulong)buffer[1] << 48) - | ((ulong)buffer[2] << 40) - | ((ulong)buffer[3] << 32) - | ((ulong)buffer[4] << 24) - | ((ulong)buffer[5] << 16) - | ((ulong)buffer[6] << 8) - | buffer[7]; - } - - private static void WriteUInt64BigEndian(byte[] dest, ulong value) - { - if (dest == null || dest.Length < 8) - { - throw new ArgumentException("Destination buffer too small for UInt64"); - } - dest[0] = (byte)(value >> 56); - dest[1] = (byte)(value >> 48); - dest[2] = (byte)(value >> 40); - dest[3] = (byte)(value >> 32); - dest[4] = (byte)(value >> 24); - dest[5] = (byte)(value >> 16); - dest[6] = (byte)(value >> 8); - dest[7] = (byte)(value); - } - - private static void ProcessCommands() - { - if (!isRunning) return; - if (Interlocked.Exchange(ref processingCommands, 1) == 1) return; - try - { - double now = EditorApplication.timeSinceStartup; - if (now >= nextHeartbeatAt) - { - WriteHeartbeat(false); - nextHeartbeatAt = now + 0.5f; - } - - List<(string id, QueuedCommand command)> work; - lock (lockObj) - { - // Early exit inside lock to prevent per-frame List allocations (GitHub issue #577) - if (commandQueue.Count == 0) - { - return; - } - - work = new List<(string, QueuedCommand)>(commandQueue.Count); - foreach (var kvp in commandQueue) - { - var queued = kvp.Value; - if (queued.IsExecuting) continue; - queued.IsExecuting = true; - work.Add((kvp.Key, queued)); - } - } - - foreach (var item in work) - { - string id = item.id; - QueuedCommand queuedCommand = item.command; - string commandText = queuedCommand.CommandJson; - TaskCompletionSource tcs = queuedCommand.Tcs; - - if (string.IsNullOrWhiteSpace(commandText)) - { - var emptyResponse = new - { - status = "error", - error = "Empty command received", - }; - tcs.SetResult(JsonConvert.SerializeObject(emptyResponse)); - lock (lockObj) { commandQueue.Remove(id); } - continue; - } - - commandText = commandText.Trim(); - if (commandText == "ping") - { - var pingResponse = new - { - status = "success", - result = new { message = "pong" }, - }; - tcs.SetResult(JsonConvert.SerializeObject(pingResponse)); - lock (lockObj) { commandQueue.Remove(id); } - continue; - } - - if (!IsValidJson(commandText)) - { - var invalidJsonResponse = new - { - status = "error", - error = "Invalid JSON format", - receivedText = commandText.Length > 50 - ? commandText[..50] + "..." - : commandText, - }; - tcs.SetResult(JsonConvert.SerializeObject(invalidJsonResponse)); - lock (lockObj) { commandQueue.Remove(id); } - continue; - } - - ExecuteQueuedCommand(id, commandText, tcs); - } - } - finally - { - Interlocked.Exchange(ref processingCommands, 0); - } - } - - private static void ExecuteQueuedCommand(string commandId, string payload, TaskCompletionSource completionSource) - { - async void Runner() - { - try - { - using var cts = new CancellationTokenSource(FrameIOTimeoutMs); - string response = await TransportCommandDispatcher.ExecuteCommandJsonAsync(payload, cts.Token).ConfigureAwait(true); - completionSource.TrySetResult(response); - } - catch (OperationCanceledException) - { - var timeoutResponse = new - { - status = "error", - error = $"Command processing timed out after {FrameIOTimeoutMs} ms", - }; - completionSource.TrySetResult(JsonConvert.SerializeObject(timeoutResponse)); - } - catch (Exception ex) - { - McpLog.Error($"Error processing command: {ex.Message}\n{ex.StackTrace}"); - var response = new - { - status = "error", - error = ex.Message, - receivedText = payload?.Length > 50 - ? payload[..50] + "..." - : payload, - }; - completionSource.TrySetResult(JsonConvert.SerializeObject(response)); - } - finally - { - lock (lockObj) - { - commandQueue.Remove(commandId); - } - } - } - - Runner(); - } - - private static object InvokeOnMainThreadWithTimeout(Func func, int timeoutMs) - { - if (func == null) return null; - try - { - if (mainThreadId == 0) - { - try { return func(); } - catch (Exception ex) { throw new InvalidOperationException($"Main thread handler error: {ex.Message}", ex); } - } - try - { - if (Thread.CurrentThread.ManagedThreadId == mainThreadId) - { - return func(); - } - } - catch { } - - object result = null; - Exception captured = null; - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - EditorApplication.delayCall += () => - { - try - { - result = func(); - } - catch (Exception ex) - { - captured = ex; - } - finally - { - try { tcs.TrySetResult(true); } catch { } - } - }; - - bool completed = tcs.Task.Wait(timeoutMs); - if (!completed) - { - return null; - } - if (captured != null) - { - throw new InvalidOperationException($"Main thread handler error: {captured.Message}", captured); - } - return result; - } - catch (Exception ex) - { - throw new InvalidOperationException($"Failed to invoke on main thread: {ex.Message}", ex); - } - } - - private static bool IsValidJson(string text) - { - if (string.IsNullOrWhiteSpace(text)) - { - return false; - } - - text = text.Trim(); - if ( - (text.StartsWith("{") && text.EndsWith("}")) - || - (text.StartsWith("[") && text.EndsWith("]")) - ) - { - try - { - JToken.Parse(text); - return true; - } - catch - { - return false; - } - } - - return false; - } - - - public static void WriteHeartbeat(bool reloading, string reason = null) - { - try - { - string dir = Environment.GetEnvironmentVariable("UNITY_MCP_STATUS_DIR"); - if (string.IsNullOrWhiteSpace(dir)) - { - dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp"); - } - Directory.CreateDirectory(dir); - string filePath = Path.Combine(dir, $"unity-mcp-status-{ComputeProjectHash(Application.dataPath)}.json"); - - string projectName = "Unknown"; - try - { - string projectPath = Application.dataPath; - if (!string.IsNullOrEmpty(projectPath)) - { - projectPath = projectPath.TrimEnd('/', '\\'); - if (projectPath.EndsWith("Assets", StringComparison.OrdinalIgnoreCase)) - { - projectPath = projectPath.Substring(0, projectPath.Length - 6).TrimEnd('/', '\\'); - } - projectName = Path.GetFileName(projectPath); - if (string.IsNullOrEmpty(projectName)) - { - projectName = "Unknown"; - } - } - } - catch { } - - var payload = new - { - unity_port = currentUnityPort, - reloading, - reason = reason ?? (reloading ? "reloading" : "ready"), - seq = heartbeatSeq, - project_path = Application.dataPath, - project_name = projectName, - unity_version = Application.unityVersion, - last_heartbeat = DateTime.UtcNow.ToString("O") - }; - File.WriteAllText(filePath, JsonConvert.SerializeObject(payload), new System.Text.UTF8Encoding(false)); - } - catch (Exception) - { - } - } - - private static string ComputeProjectHash(string input) - { - try - { - using var sha1 = System.Security.Cryptography.SHA1.Create(); - byte[] bytes = System.Text.Encoding.UTF8.GetBytes(input ?? string.Empty); - byte[] hashBytes = sha1.ComputeHash(bytes); - var sb = new System.Text.StringBuilder(); - foreach (byte b in hashBytes) - { - sb.Append(b.ToString("x2")); - } - return sb.ToString()[..8]; - } - catch - { - return "default"; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs.meta b/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs.meta deleted file mode 100644 index 543a015..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: fd295cefe518e438693c12e9c7f37488 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioTransportClient.cs b/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioTransportClient.cs deleted file mode 100644 index ea3ed1a..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioTransportClient.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Threading.Tasks; -using MCPForUnity.Editor.Helpers; - -namespace MCPForUnity.Editor.Services.Transport.Transports -{ - /// - /// Adapts the existing TCP bridge into the transport abstraction. - /// - public class StdioTransportClient : IMcpTransportClient - { - private TransportState _state = TransportState.Disconnected("stdio"); - - public bool IsConnected => StdioBridgeHost.IsRunning; - public string TransportName => "stdio"; - public TransportState State => _state; - - public Task StartAsync() - { - try - { - StdioBridgeHost.StartAutoConnect(); - _state = TransportState.Connected("stdio", port: StdioBridgeHost.GetCurrentPort()); - return Task.FromResult(true); - } - catch (Exception ex) - { - _state = TransportState.Disconnected("stdio", ex.Message); - return Task.FromResult(false); - } - } - - public Task StopAsync() - { - StdioBridgeHost.Stop(); - _state = TransportState.Disconnected("stdio"); - return Task.CompletedTask; - } - - public Task VerifyAsync() - { - bool running = StdioBridgeHost.IsRunning; - _state = running - ? TransportState.Connected("stdio", port: StdioBridgeHost.GetCurrentPort()) - : TransportState.Disconnected("stdio", "Bridge not running"); - return Task.FromResult(running); - } - - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioTransportClient.cs.meta b/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioTransportClient.cs.meta deleted file mode 100644 index cff5c0a..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioTransportClient.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b2743f3468d5f433dbf2220f0838d8d1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Transport/Transports/StdioTransportClient.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs b/Assets/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs deleted file mode 100644 index b94c083..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs +++ /dev/null @@ -1,741 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; -using MCPForUnity.Editor.Services.Transport; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Services.Transport.Transports -{ - /// - /// Maintains a persistent WebSocket connection to the MCP server plugin hub. - /// Handles registration, keep-alives, and command dispatch back into Unity via - /// . - /// - public class WebSocketTransportClient : IMcpTransportClient, IDisposable - { - private const string TransportDisplayName = "websocket"; - private static readonly TimeSpan[] ReconnectSchedule = - { - TimeSpan.Zero, - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(3), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10), - TimeSpan.FromSeconds(30) - }; - - private static readonly TimeSpan DefaultKeepAliveInterval = TimeSpan.FromSeconds(15); - private static readonly TimeSpan DefaultCommandTimeout = TimeSpan.FromSeconds(30); - - private readonly IToolDiscoveryService _toolDiscoveryService; - private ClientWebSocket _socket; - private CancellationTokenSource _lifecycleCts; - private CancellationTokenSource _connectionCts; - private Task _receiveTask; - private Task _keepAliveTask; - private readonly SemaphoreSlim _sendLock = new(1, 1); - - private Uri _endpointUri; - private string _sessionId; - private string _projectHash; - private string _projectName; - private string _projectPath; - private string _unityVersion; - private TimeSpan _keepAliveInterval = DefaultKeepAliveInterval; - private TimeSpan _socketKeepAliveInterval = DefaultKeepAliveInterval; - private volatile bool _isConnected; - private int _isReconnectingFlag; - private TransportState _state = TransportState.Disconnected(TransportDisplayName, "Transport not started"); - private string _apiKey; - private bool _disposed; - - public WebSocketTransportClient(IToolDiscoveryService toolDiscoveryService = null) - { - _toolDiscoveryService = toolDiscoveryService; - } - - public bool IsConnected => _isConnected; - public string TransportName => TransportDisplayName; - public TransportState State => _state; - - private Task> GetEnabledToolsOnMainThreadAsync(CancellationToken token) - { - return TransportCommandDispatcher.RunOnMainThreadAsync( - () => _toolDiscoveryService?.GetEnabledTools() ?? new List(), - token); - } - - public async Task StartAsync() - { - // Capture identity values on the main thread before any async context switching - _projectName = ProjectIdentityUtility.GetProjectName(); - _projectHash = ProjectIdentityUtility.GetProjectHash(); - _unityVersion = Application.unityVersion; - _apiKey = HttpEndpointUtility.IsRemoteScope() - ? EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty) - : string.Empty; - - // Get project root path (strip /Assets from dataPath) for focus nudging - string dataPath = Application.dataPath; - if (!string.IsNullOrEmpty(dataPath)) - { - string normalized = dataPath.TrimEnd('/', '\\'); - if (string.Equals(System.IO.Path.GetFileName(normalized), "Assets", StringComparison.Ordinal)) - { - _projectPath = System.IO.Path.GetDirectoryName(normalized) ?? normalized; - } - else - { - _projectPath = normalized; // Fallback if path doesn't end with Assets - } - } - - await StopAsync(); - - _lifecycleCts = new CancellationTokenSource(); - _endpointUri = BuildWebSocketUri(HttpEndpointUtility.GetBaseUrl()); - _sessionId = null; - - if (!await EstablishConnectionAsync(_lifecycleCts.Token)) - { - await StopAsync(); - return false; - } - - // State is connected but session ID might be pending until 'registered' message - _state = TransportState.Connected(TransportDisplayName, sessionId: "pending", details: _endpointUri.ToString()); - _isConnected = true; - return true; - } - - public async Task StopAsync() - { - if (_lifecycleCts == null) - { - return; - } - - try - { - _lifecycleCts.Cancel(); - } - catch { } - - await StopConnectionLoopsAsync().ConfigureAwait(false); - - if (_socket != null) - { - try - { - if (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.CloseReceived) - { - await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Shutdown", CancellationToken.None).ConfigureAwait(false); - } - } - catch { } - finally - { - _socket.Dispose(); - _socket = null; - } - } - - _isConnected = false; - _state = TransportState.Disconnected(TransportDisplayName); - - _lifecycleCts.Dispose(); - _lifecycleCts = null; - } - - public async Task VerifyAsync() - { - if (_socket == null || _socket.State != WebSocketState.Open) - { - return false; - } - - if (_lifecycleCts == null) - { - return false; - } - - try - { - using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(_lifecycleCts.Token); - timeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); - await SendPongAsync(timeoutCts.Token).ConfigureAwait(false); - return true; - } - catch (Exception ex) - { - McpLog.Warn($"[WebSocket] Verify ping failed: {ex.Message}"); - return false; - } - } - - public void Dispose() - { - if (_disposed) - { - return; - } - - try - { - // Ensure background loops are stopped before disposing shared resources - StopAsync().GetAwaiter().GetResult(); - } - catch (Exception ex) - { - McpLog.Warn($"[WebSocket] Dispose failed to stop cleanly: {ex.Message}"); - } - - _sendLock?.Dispose(); - _socket?.Dispose(); - _lifecycleCts?.Dispose(); - _disposed = true; - } - - private async Task EstablishConnectionAsync(CancellationToken token) - { - await StopConnectionLoopsAsync().ConfigureAwait(false); - - _connectionCts?.Dispose(); - _connectionCts = CancellationTokenSource.CreateLinkedTokenSource(token); - CancellationToken connectionToken = _connectionCts.Token; - - _socket?.Dispose(); - _socket = new ClientWebSocket(); - _socket.Options.KeepAliveInterval = _socketKeepAliveInterval; - - // Add API key header if configured (for remote-hosted mode) - if (!string.IsNullOrEmpty(_apiKey)) - { - _socket.Options.SetRequestHeader(AuthConstants.ApiKeyHeader, _apiKey); - } - - try - { - await _socket.ConnectAsync(_endpointUri, connectionToken).ConfigureAwait(false); - } - catch (Exception ex) - { - string errorMsg = "Connection failed. Check that the server URL is correct, the server is running, and your API key (if required) is valid."; - McpLog.Error($"[WebSocket] {errorMsg} (Detail: {ex.Message})"); - _state = TransportState.Disconnected(TransportDisplayName, errorMsg); - return false; - } - - StartBackgroundLoops(connectionToken); - - try - { - await SendRegisterAsync(connectionToken).ConfigureAwait(false); - } - catch (Exception ex) - { - string regMsg = $"Registration with server failed: {ex.Message}"; - McpLog.Error($"[WebSocket] {regMsg}"); - _state = TransportState.Disconnected(TransportDisplayName, regMsg); - return false; - } - - return true; - } - - /// - /// Stops the connection loops and disposes of the connection CTS. - /// Particularly useful when reconnecting, we want to ensure that background loops are cancelled correctly before starting new oens - /// - /// Whether to await the receive and keep alive tasks before disposing. - private async Task StopConnectionLoopsAsync(bool awaitTasks = true) - { - if (_connectionCts != null && !_connectionCts.IsCancellationRequested) - { - try { _connectionCts.Cancel(); } catch { } - } - - if (_receiveTask != null) - { - if (awaitTasks) - { - try { await _receiveTask.ConfigureAwait(false); } catch { } - _receiveTask = null; - } - else if (_receiveTask.IsCompleted) - { - _receiveTask = null; - } - } - - if (_keepAliveTask != null) - { - if (awaitTasks) - { - try { await _keepAliveTask.ConfigureAwait(false); } catch { } - _keepAliveTask = null; - } - else if (_keepAliveTask.IsCompleted) - { - _keepAliveTask = null; - } - } - - if (_connectionCts != null) - { - _connectionCts.Dispose(); - _connectionCts = null; - } - } - - private void StartBackgroundLoops(CancellationToken token) - { - if ((_receiveTask != null && !_receiveTask.IsCompleted) || (_keepAliveTask != null && !_keepAliveTask.IsCompleted)) - { - return; - } - - _receiveTask = Task.Run(() => ReceiveLoopAsync(token), CancellationToken.None); - _keepAliveTask = Task.Run(() => KeepAliveLoopAsync(token), CancellationToken.None); - } - - private async Task ReceiveLoopAsync(CancellationToken token) - { - while (!token.IsCancellationRequested) - { - try - { - string message = await ReceiveMessageAsync(token).ConfigureAwait(false); - if (message == null) - { - continue; - } - await HandleMessageAsync(message, token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - break; - } - catch (WebSocketException wse) - { - McpLog.Warn($"[WebSocket] Receive loop error: {wse.Message}"); - await HandleSocketClosureAsync(wse.Message).ConfigureAwait(false); - break; - } - catch (Exception ex) - { - McpLog.Warn($"[WebSocket] Unexpected receive error: {ex.Message}"); - await HandleSocketClosureAsync(ex.Message).ConfigureAwait(false); - break; - } - } - } - - private async Task ReceiveMessageAsync(CancellationToken token) - { - if (_socket == null) - { - return null; - } - - byte[] rentedBuffer = System.Buffers.ArrayPool.Shared.Rent(8192); - var buffer = new ArraySegment(rentedBuffer); - using var ms = new MemoryStream(8192); - - try - { - while (!token.IsCancellationRequested) - { - WebSocketReceiveResult result = await _socket.ReceiveAsync(buffer, token).ConfigureAwait(false); - - if (result.MessageType == WebSocketMessageType.Close) - { - await HandleSocketClosureAsync(result.CloseStatusDescription ?? "Server closed connection").ConfigureAwait(false); - return null; - } - - if (result.Count > 0) - { - ms.Write(buffer.Array!, buffer.Offset, result.Count); - } - - if (result.EndOfMessage) - { - break; - } - } - - if (ms.Length == 0) - { - return null; - } - - return Encoding.UTF8.GetString(ms.ToArray()); - } - finally - { - System.Buffers.ArrayPool.Shared.Return(rentedBuffer); - } - } - - private async Task HandleMessageAsync(string message, CancellationToken token) - { - JObject payload; - try - { - payload = JObject.Parse(message); - } - catch (Exception ex) - { - McpLog.Warn($"[WebSocket] Invalid JSON payload: {ex.Message}"); - return; - } - - string messageType = payload.Value("type") ?? string.Empty; - - switch (messageType) - { - case "welcome": - ApplyWelcome(payload); - break; - case "registered": - await HandleRegisteredAsync(payload, token).ConfigureAwait(false); - break; - case "execute": - await HandleExecuteAsync(payload, token).ConfigureAwait(false); - break; - case "ping": - await SendPongAsync(token).ConfigureAwait(false); - break; - default: - // No-op for unrecognised types (keep-alives, telemetry, etc.) - break; - } - } - - private void ApplyWelcome(JObject payload) - { - int? keepAliveSeconds = payload.Value("keepAliveInterval"); - if (keepAliveSeconds.HasValue && keepAliveSeconds.Value > 0) - { - _keepAliveInterval = TimeSpan.FromSeconds(keepAliveSeconds.Value); - _socketKeepAliveInterval = _keepAliveInterval; - } - - int? serverTimeoutSeconds = payload.Value("serverTimeout"); - if (serverTimeoutSeconds.HasValue) - { - int sourceSeconds = keepAliveSeconds ?? serverTimeoutSeconds.Value; - int safeSeconds = Math.Max(5, Math.Min(serverTimeoutSeconds.Value, sourceSeconds)); - _socketKeepAliveInterval = TimeSpan.FromSeconds(safeSeconds); - } - } - - private async Task HandleRegisteredAsync(JObject payload, CancellationToken token) - { - string newSessionId = payload.Value("session_id"); - if (!string.IsNullOrEmpty(newSessionId)) - { - _sessionId = newSessionId; - ProjectIdentityUtility.SetSessionId(_sessionId); - _state = TransportState.Connected(TransportDisplayName, sessionId: _sessionId, details: _endpointUri.ToString()); - McpLog.Info($"[WebSocket] Registered with session ID: {_sessionId}", false); - - await SendRegisterToolsAsync(token).ConfigureAwait(false); - } - } - - private async Task SendRegisterToolsAsync(CancellationToken token) - { - if (_toolDiscoveryService == null) return; - - token.ThrowIfCancellationRequested(); - var tools = await GetEnabledToolsOnMainThreadAsync(token).ConfigureAwait(false); - token.ThrowIfCancellationRequested(); - McpLog.Info($"[WebSocket] Preparing to register {tools.Count} tool(s) with the bridge.", false); - var toolsArray = new JArray(); - - foreach (var tool in tools) - { - var toolObj = new JObject - { - ["name"] = tool.Name, - ["description"] = tool.Description, - ["structured_output"] = tool.StructuredOutput, - ["requires_polling"] = tool.RequiresPolling, - ["poll_action"] = tool.PollAction - }; - - var paramsArray = new JArray(); - if (tool.Parameters != null) - { - foreach (var p in tool.Parameters) - { - paramsArray.Add(new JObject - { - ["name"] = p.Name, - ["description"] = p.Description, - ["type"] = p.Type, - ["required"] = p.Required, - ["default_value"] = p.DefaultValue - }); - } - } - toolObj["parameters"] = paramsArray; - toolsArray.Add(toolObj); - } - - var payload = new JObject - { - ["type"] = "register_tools", - ["tools"] = toolsArray - }; - - await SendJsonAsync(payload, token).ConfigureAwait(false); - McpLog.Info($"[WebSocket] Sent {tools.Count} tools registration", false); - } - - private async Task HandleExecuteAsync(JObject payload, CancellationToken token) - { - string commandId = payload.Value("id"); - string commandName = payload.Value("name"); - JObject parameters = payload.Value("params") ?? new JObject(); - int timeoutSeconds = payload.Value("timeout") ?? (int)DefaultCommandTimeout.TotalSeconds; - - if (string.IsNullOrEmpty(commandId) || string.IsNullOrEmpty(commandName)) - { - McpLog.Warn("[WebSocket] Invalid execute payload (missing id or name)"); - return; - } - - var commandEnvelope = new JObject - { - ["type"] = commandName, - ["params"] = parameters - }; - - string responseJson; - try - { - using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(token); - timeoutCts.CancelAfter(TimeSpan.FromSeconds(Math.Max(1, timeoutSeconds))); - responseJson = await TransportCommandDispatcher.ExecuteCommandJsonAsync(commandEnvelope.ToString(Formatting.None), timeoutCts.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - responseJson = JsonConvert.SerializeObject(new - { - status = "error", - error = $"Command '{commandName}' timed out after {timeoutSeconds} seconds" - }); - } - catch (Exception ex) - { - responseJson = JsonConvert.SerializeObject(new - { - status = "error", - error = ex.Message - }); - } - - JToken resultToken; - try - { - resultToken = JToken.Parse(responseJson); - } - catch - { - resultToken = new JObject - { - ["status"] = "error", - ["error"] = "Invalid response payload" - }; - } - - var responsePayload = new JObject - { - ["type"] = "command_result", - ["id"] = commandId, - ["result"] = resultToken - }; - - await SendJsonAsync(responsePayload, token).ConfigureAwait(false); - } - - private async Task KeepAliveLoopAsync(CancellationToken token) - { - while (!token.IsCancellationRequested) - { - try - { - await Task.Delay(_keepAliveInterval, token).ConfigureAwait(false); - if (_socket == null || _socket.State != WebSocketState.Open) - { - break; - } - await SendPongAsync(token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - McpLog.Warn($"[WebSocket] Keep-alive failed: {ex.Message}"); - await HandleSocketClosureAsync(ex.Message).ConfigureAwait(false); - break; - } - } - } - - private async Task SendRegisterAsync(CancellationToken token) - { - var registerPayload = new JObject - { - ["type"] = "register", - // session_id is now server-authoritative; omitted here or sent as null - ["project_name"] = _projectName, - ["project_hash"] = _projectHash, - ["unity_version"] = _unityVersion, - ["project_path"] = _projectPath - }; - - await SendJsonAsync(registerPayload, token).ConfigureAwait(false); - } - - private Task SendPongAsync(CancellationToken token) - { - var payload = new JObject - { - ["type"] = "pong", - ["session_id"] = _sessionId // Include session ID for server-side tracking - }; - return SendJsonAsync(payload, token); - } - - private async Task SendJsonAsync(JObject payload, CancellationToken token) - { - if (_socket == null) - { - throw new InvalidOperationException("WebSocket is not initialised"); - } - - string json = payload.ToString(Formatting.None); - byte[] bytes = Encoding.UTF8.GetBytes(json); - var buffer = new ArraySegment(bytes); - - await _sendLock.WaitAsync(token).ConfigureAwait(false); - try - { - if (_socket.State != WebSocketState.Open) - { - throw new InvalidOperationException("WebSocket is not open"); - } - - await _socket.SendAsync(buffer, WebSocketMessageType.Text, true, token).ConfigureAwait(false); - } - finally - { - _sendLock.Release(); - } - } - - private async Task HandleSocketClosureAsync(string reason) - { - // Capture stack trace for debugging disconnection triggers - var stackTrace = new System.Diagnostics.StackTrace(true); - McpLog.Debug($"[WebSocket] HandleSocketClosureAsync called. Reason: {reason}\nStack trace:\n{stackTrace}"); - - if (_lifecycleCts == null || _lifecycleCts.IsCancellationRequested) - { - return; - } - - if (Interlocked.CompareExchange(ref _isReconnectingFlag, 1, 0) != 0) - { - return; - } - - _isConnected = false; - _state = _state.WithError(reason ?? "Connection closed"); - McpLog.Warn($"[WebSocket] Connection closed: {reason}"); - - await StopConnectionLoopsAsync(awaitTasks: false).ConfigureAwait(false); - - _ = Task.Run(() => AttemptReconnectAsync(_lifecycleCts.Token), CancellationToken.None); - } - - private async Task AttemptReconnectAsync(CancellationToken token) - { - try - { - await StopConnectionLoopsAsync().ConfigureAwait(false); - - foreach (TimeSpan delay in ReconnectSchedule) - { - if (token.IsCancellationRequested) - { - return; - } - - if (delay > TimeSpan.Zero) - { - try { await Task.Delay(delay, token).ConfigureAwait(false); } - catch (OperationCanceledException) { return; } - } - - if (await EstablishConnectionAsync(token).ConfigureAwait(false)) - { - _state = TransportState.Connected(TransportDisplayName, sessionId: _sessionId, details: _endpointUri.ToString()); - _isConnected = true; - McpLog.Info("[WebSocket] Reconnected to MCP server", false); - return; - } - } - } - finally - { - Interlocked.Exchange(ref _isReconnectingFlag, 0); - } - - _state = TransportState.Disconnected(TransportDisplayName, "Failed to reconnect"); - } - - private static Uri BuildWebSocketUri(string baseUrl) - { - if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var httpUri)) - { - throw new InvalidOperationException($"Invalid MCP base URL: {baseUrl}"); - } - - // Replace bind-only addresses with localhost for client connections - // 0.0.0.0 and :: are only valid for server binding, not client connections - string host = httpUri.Host; - if (host == "0.0.0.0" || host == "::") - { - McpLog.Warn($"[WebSocket] Base URL host '{host}' is bind-only; using 'localhost' for client connection."); - host = "localhost"; - } - - var builder = new UriBuilder(httpUri) - { - Scheme = httpUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) ? "wss" : "ws", - Host = host, - Path = httpUri.AbsolutePath.TrimEnd('/') + "/hub/plugin" - }; - - return builder.Uri; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs.meta b/Assets/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs.meta deleted file mode 100644 index 8dba006..0000000 --- a/Assets/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 044c8f7beb4af4a77a14d677190c21dc -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Setup.meta b/Assets/MCPForUnity/Editor/Setup.meta deleted file mode 100644 index 1157b1e..0000000 --- a/Assets/MCPForUnity/Editor/Setup.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 600c9cb20c329d761bfa799158a87bac -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Setup/SetupWindowService.cs b/Assets/MCPForUnity/Editor/Setup/SetupWindowService.cs deleted file mode 100644 index 5a63bb9..0000000 --- a/Assets/MCPForUnity/Editor/Setup/SetupWindowService.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Dependencies; -using MCPForUnity.Editor.Dependencies.Models; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Windows; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Setup -{ - /// - /// Handles automatic triggering of the MCP setup window and exposes menu entry points - /// - public static class SetupWindowService - { - private const string SETUP_COMPLETED_KEY = EditorPrefKeys.SetupCompleted; - private const string SETUP_DISMISSED_KEY = EditorPrefKeys.SetupDismissed; - - // Use SessionState to persist "checked this editor session" across domain reloads. - // SessionState survives assembly reloads within the same Editor session, which prevents - // the setup window from reappearing after code reloads / playmode transitions. - private const string SessionCheckedKey = "MCPForUnity.SetupWindowCheckedThisEditorSession"; - - static SetupWindowService() - { - // Skip in batch mode - if (Application.isBatchMode) - return; - - // Show Setup Window on package import - EditorApplication.delayCall += CheckSetupNeeded; - } - - /// - /// Check if Setup Window should be shown - /// - private static void CheckSetupNeeded() - { - // Ensure we only run once per Editor session (survives domain reloads). - // This avoids showing the setup dialog repeatedly when scripts recompile or Play mode toggles. - if (SessionState.GetBool(SessionCheckedKey, false)) - return; - - SessionState.SetBool(SessionCheckedKey, true); - - try - { - // Check if setup was already completed or dismissed in previous sessions - bool setupCompleted = EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false); - bool setupDismissed = EditorPrefs.GetBool(SETUP_DISMISSED_KEY, false); - - // Only show Setup Window if it hasn't been completed or dismissed before - if (!(setupCompleted || setupDismissed)) - { - McpLog.Info("Package imported - showing Setup Window", always: false); - - var dependencyResult = DependencyManager.CheckAllDependencies(); - EditorApplication.delayCall += () => ShowSetupWindow(dependencyResult); - } - else - { - McpLog.Info( - "Setup Window skipped - previously completed or dismissed", - always: false - ); - } - } - catch (Exception ex) - { - McpLog.Error($"Error checking setup status: {ex.Message}"); - } - } - - /// - /// Show the setup window - /// - public static void ShowSetupWindow(DependencyCheckResult dependencyResult = null) - { - try - { - dependencyResult ??= DependencyManager.CheckAllDependencies(); - MCPSetupWindow.ShowWindow(dependencyResult); - } - catch (Exception ex) - { - McpLog.Error($"Error showing setup window: {ex.Message}"); - } - } - - /// - /// Mark setup as completed - /// - public static void MarkSetupCompleted() - { - EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true); - McpLog.Info("Setup marked as completed"); - } - - /// - /// Mark setup as dismissed - /// - public static void MarkSetupDismissed() - { - EditorPrefs.SetBool(SETUP_DISMISSED_KEY, true); - McpLog.Info("Setup marked as dismissed"); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Setup/SetupWindowService.cs.meta b/Assets/MCPForUnity/Editor/Setup/SetupWindowService.cs.meta deleted file mode 100644 index 0089200..0000000 --- a/Assets/MCPForUnity/Editor/Setup/SetupWindowService.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: d1bf468667bb649989e3ef53dafddea6 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Setup/SetupWindowService.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools.meta b/Assets/MCPForUnity/Editor/Tools.meta deleted file mode 100644 index 2bc55f0..0000000 --- a/Assets/MCPForUnity/Editor/Tools.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: c97b83a6ac92a704b864eef27c3d285b -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Tools/BatchExecute.cs b/Assets/MCPForUnity/Editor/Tools/BatchExecute.cs deleted file mode 100644 index d9df336..0000000 --- a/Assets/MCPForUnity/Editor/Tools/BatchExecute.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Executes multiple MCP commands within a single Unity-side handler. Commands are executed sequentially - /// on the main thread to preserve determinism and Unity API safety. - /// - [McpForUnityTool("batch_execute", AutoRegister = false)] - public static class BatchExecute - { - private const int MaxCommandsPerBatch = 25; - - public static async Task HandleCommand(JObject @params) - { - if (@params == null) - { - return new ErrorResponse("'commands' payload is required."); - } - - var commandsToken = @params["commands"] as JArray; - if (commandsToken == null || commandsToken.Count == 0) - { - return new ErrorResponse("Provide at least one command entry in 'commands'."); - } - - if (commandsToken.Count > MaxCommandsPerBatch) - { - return new ErrorResponse($"A maximum of {MaxCommandsPerBatch} commands are allowed per batch."); - } - - bool failFast = @params.Value("failFast") ?? false; - bool parallelRequested = @params.Value("parallel") ?? false; - int? maxParallel = @params.Value("maxParallelism"); - - if (parallelRequested) - { - McpLog.Warn("batch_execute parallel mode requested, but commands will run sequentially on the main thread for safety."); - } - - var commandResults = new List(commandsToken.Count); - int invocationSuccessCount = 0; - int invocationFailureCount = 0; - bool anyCommandFailed = false; - - foreach (var token in commandsToken) - { - if (token is not JObject commandObj) - { - invocationFailureCount++; - anyCommandFailed = true; - commandResults.Add(new - { - tool = (string)null, - callSucceeded = false, - error = "Command entries must be JSON objects." - }); - if (failFast) - { - break; - } - continue; - } - - string toolName = commandObj["tool"]?.ToString(); - var rawParams = commandObj["params"] as JObject ?? new JObject(); - var commandParams = NormalizeParameterKeys(rawParams); - - if (string.IsNullOrWhiteSpace(toolName)) - { - invocationFailureCount++; - anyCommandFailed = true; - commandResults.Add(new - { - tool = toolName, - callSucceeded = false, - error = "Each command must include a non-empty 'tool' field." - }); - if (failFast) - { - break; - } - continue; - } - - try - { - var result = await CommandRegistry.InvokeCommandAsync(toolName, commandParams).ConfigureAwait(true); - bool callSucceeded = DetermineCallSucceeded(result); - if (callSucceeded) - { - invocationSuccessCount++; - } - else - { - invocationFailureCount++; - anyCommandFailed = true; - } - - commandResults.Add(new - { - tool = toolName, - callSucceeded, - result - }); - - if (!callSucceeded && failFast) - { - break; - } - } - catch (Exception ex) - { - invocationFailureCount++; - anyCommandFailed = true; - commandResults.Add(new - { - tool = toolName, - callSucceeded = false, - error = ex.Message - }); - - if (failFast) - { - break; - } - } - } - - bool overallSuccess = !anyCommandFailed; - var data = new - { - results = commandResults, - callSuccessCount = invocationSuccessCount, - callFailureCount = invocationFailureCount, - parallelRequested, - parallelApplied = false, - maxParallelism = maxParallel - }; - - return overallSuccess - ? new SuccessResponse("Batch execution completed.", data) - : new ErrorResponse("One or more commands failed.", data); - } - - private static bool DetermineCallSucceeded(object result) - { - if (result == null) - { - return true; - } - - if (result is IMcpResponse response) - { - return response.Success; - } - - if (result is JObject obj) - { - var successToken = obj["success"]; - if (successToken != null && successToken.Type == JTokenType.Boolean) - { - return successToken.Value(); - } - } - - if (result is JToken token) - { - var successToken = token["success"]; - if (successToken != null && successToken.Type == JTokenType.Boolean) - { - return successToken.Value(); - } - } - - return true; - } - - private static JObject NormalizeParameterKeys(JObject source) - { - if (source == null) - { - return new JObject(); - } - - var normalized = new JObject(); - foreach (var property in source.Properties()) - { - string normalizedName = ToCamelCase(property.Name); - normalized[normalizedName] = NormalizeToken(property.Value); - } - return normalized; - } - - private static JArray NormalizeArray(JArray source) - { - var normalized = new JArray(); - foreach (var token in source) - { - normalized.Add(NormalizeToken(token)); - } - return normalized; - } - - private static JToken NormalizeToken(JToken token) - { - return token switch - { - JObject obj => NormalizeParameterKeys(obj), - JArray arr => NormalizeArray(arr), - _ => token.DeepClone() - }; - } - - private static string ToCamelCase(string key) => StringCaseUtility.ToCamelCase(key); - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/BatchExecute.cs.meta b/Assets/MCPForUnity/Editor/Tools/BatchExecute.cs.meta deleted file mode 100644 index 7c62c22..0000000 --- a/Assets/MCPForUnity/Editor/Tools/BatchExecute.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4e1e2d8f3a454a37b18d06a7a7b6c3fb -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/BatchExecute.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/CommandRegistry.cs b/Assets/MCPForUnity/Editor/Tools/CommandRegistry.cs deleted file mode 100644 index ca39ea5..0000000 --- a/Assets/MCPForUnity/Editor/Tools/CommandRegistry.cs +++ /dev/null @@ -1,426 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Resources; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Holds information about a registered command handler. - /// - class HandlerInfo - { - public string CommandName { get; } - public Func SyncHandler { get; } - public Func> AsyncHandler { get; } - - public bool IsAsync => AsyncHandler != null; - - public HandlerInfo(string commandName, Func syncHandler, Func> asyncHandler) - { - CommandName = commandName; - SyncHandler = syncHandler; - AsyncHandler = asyncHandler; - } - } - - /// - /// Registry for all MCP command handlers via reflection. - /// Handles both MCP tools and resources. - /// - public static class CommandRegistry - { - private static readonly Dictionary _handlers = new(); - private static bool _initialized = false; - - /// - /// Initialize and auto-discover all tools and resources marked with - /// [McpForUnityTool] or [McpForUnityResource] - /// - public static void Initialize() - { - if (_initialized) return; - - AutoDiscoverCommands(); - _initialized = true; - } - - private static string ToSnakeCase(string name) => StringCaseUtility.ToSnakeCase(name); - - /// - /// Auto-discover all types with [McpForUnityTool] or [McpForUnityResource] attributes - /// - private static void AutoDiscoverCommands() - { - try - { - var allTypes = AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic) - .SelectMany(a => - { - try { return a.GetTypes(); } - catch { return new Type[0]; } - }) - .ToList(); - - // Discover tools - var toolTypes = allTypes.Where(t => t.GetCustomAttribute() != null); - int toolCount = 0; - foreach (var type in toolTypes) - { - if (RegisterCommandType(type, isResource: false)) - toolCount++; - } - - // Discover resources - var resourceTypes = allTypes.Where(t => t.GetCustomAttribute() != null); - int resourceCount = 0; - foreach (var type in resourceTypes) - { - if (RegisterCommandType(type, isResource: true)) - resourceCount++; - } - - McpLog.Info($"Auto-discovered {toolCount} tools and {resourceCount} resources ({_handlers.Count} total handlers)", false); - } - catch (Exception ex) - { - McpLog.Error($"Failed to auto-discover MCP commands: {ex.Message}"); - } - } - - /// - /// Register a command type (tool or resource) with the registry. - /// Returns true if successfully registered, false otherwise. - /// - private static bool RegisterCommandType(Type type, bool isResource) - { - string commandName; - string typeLabel = isResource ? "resource" : "tool"; - - // Get command name from appropriate attribute - if (isResource) - { - var resourceAttr = type.GetCustomAttribute(); - commandName = resourceAttr.ResourceName; - } - else - { - var toolAttr = type.GetCustomAttribute(); - commandName = toolAttr.CommandName; - } - - // Auto-generate command name if not explicitly provided - if (string.IsNullOrEmpty(commandName)) - { - commandName = ToSnakeCase(type.Name); - } - - // Check for duplicate command names - if (_handlers.ContainsKey(commandName)) - { - McpLog.Warn( - $"Duplicate command name '{commandName}' detected. " + - $"{typeLabel} {type.Name} will override previously registered handler." - ); - } - - // Find HandleCommand method - var method = type.GetMethod( - "HandleCommand", - BindingFlags.Public | BindingFlags.Static, - null, - new[] { typeof(JObject) }, - null - ); - - if (method == null) - { - McpLog.Warn( - $"MCP {typeLabel} {type.Name} is marked with [McpForUnity{(isResource ? "Resource" : "Tool")}] " + - $"but has no public static HandleCommand(JObject) method" - ); - return false; - } - - try - { - HandlerInfo handlerInfo; - - if (typeof(Task).IsAssignableFrom(method.ReturnType)) - { - var asyncHandler = CreateAsyncHandlerDelegate(method, commandName); - handlerInfo = new HandlerInfo(commandName, null, asyncHandler); - } - else - { - var handler = (Func)Delegate.CreateDelegate( - typeof(Func), - method - ); - handlerInfo = new HandlerInfo(commandName, handler, null); - } - - _handlers[commandName] = handlerInfo; - return true; - } - catch (Exception ex) - { - McpLog.Error($"Failed to register {typeLabel} {type.Name}: {ex.Message}"); - return false; - } - } - - /// - /// Get a command handler by name - /// - private static HandlerInfo GetHandlerInfo(string commandName) - { - if (!_handlers.TryGetValue(commandName, out var handler)) - { - throw new InvalidOperationException( - $"Unknown or unsupported command type: {commandName}" - ); - } - return handler; - } - - /// - /// Get a synchronous command handler by name. - /// Throws if the command is asynchronous. - /// - /// - /// - /// - public static Func GetHandler(string commandName) - { - var handlerInfo = GetHandlerInfo(commandName); - if (handlerInfo.IsAsync) - { - throw new InvalidOperationException( - $"Command '{commandName}' is asynchronous and must be executed via ExecuteCommand" - ); - } - - return handlerInfo.SyncHandler; - } - - /// - /// Execute a command handler, supporting both synchronous and asynchronous (coroutine) handlers. - /// If the handler returns an IEnumerator, it will be executed as a coroutine. - /// - /// The command name to execute - /// Command parameters - /// TaskCompletionSource to complete when async operation finishes - /// The result for synchronous commands, or null for async commands (TCS will be completed later) - public static object ExecuteCommand(string commandName, JObject @params, TaskCompletionSource tcs) - { - var handlerInfo = GetHandlerInfo(commandName); - - if (handlerInfo.IsAsync) - { - ExecuteAsyncHandler(handlerInfo, @params, commandName, tcs); - return null; - } - - if (handlerInfo.SyncHandler == null) - { - throw new InvalidOperationException($"Handler for '{commandName}' does not provide a synchronous implementation"); - } - - return handlerInfo.SyncHandler(@params); - } - - /// - /// Execute a command handler and return its raw result, regardless of sync or async implementation. - /// Used internally for features like batch execution where commands need to be composed. - /// - /// The registered command to execute. - /// Parameters to pass to the command (optional). - public static Task InvokeCommandAsync(string commandName, JObject @params) - { - var handlerInfo = GetHandlerInfo(commandName); - var payload = @params ?? new JObject(); - - if (handlerInfo.IsAsync) - { - if (handlerInfo.AsyncHandler == null) - { - throw new InvalidOperationException($"Async handler for '{commandName}' is not configured correctly"); - } - - return handlerInfo.AsyncHandler(payload); - } - - if (handlerInfo.SyncHandler == null) - { - throw new InvalidOperationException($"Handler for '{commandName}' does not provide a synchronous implementation"); - } - - object result = handlerInfo.SyncHandler(payload); - return Task.FromResult(result); - } - - /// - /// Create a delegate for an async handler method that returns Task or Task. - /// The delegate will invoke the method and await its completion, returning the result. - /// - /// - /// - /// - /// - private static Func> CreateAsyncHandlerDelegate(MethodInfo method, string commandName) - { - return async (JObject parameters) => - { - object rawResult; - - try - { - rawResult = method.Invoke(null, new object[] { parameters }); - } - catch (TargetInvocationException ex) - { - throw ex.InnerException ?? ex; - } - - if (rawResult == null) - { - return null; - } - - if (rawResult is not Task task) - { - throw new InvalidOperationException( - $"Async handler '{commandName}' returned an object that is not a Task" - ); - } - - await task.ConfigureAwait(true); - - var taskType = task.GetType(); - if (taskType.IsGenericType) - { - var resultProperty = taskType.GetProperty("Result"); - if (resultProperty != null) - { - return resultProperty.GetValue(task); - } - } - - return null; - }; - } - - private static void ExecuteAsyncHandler( - HandlerInfo handlerInfo, - JObject parameters, - string commandName, - TaskCompletionSource tcs) - { - if (handlerInfo.AsyncHandler == null) - { - throw new InvalidOperationException($"Async handler for '{commandName}' is not configured correctly"); - } - - Task handlerTask; - - try - { - handlerTask = handlerInfo.AsyncHandler(parameters); - } - catch (Exception ex) - { - ReportAsyncFailure(commandName, tcs, ex); - return; - } - - if (handlerTask == null) - { - CompleteAsyncCommand(commandName, tcs, null); - return; - } - - async void AwaitHandler() - { - try - { - var finalResult = await handlerTask.ConfigureAwait(true); - CompleteAsyncCommand(commandName, tcs, finalResult); - } - catch (Exception ex) - { - ReportAsyncFailure(commandName, tcs, ex); - } - } - - AwaitHandler(); - } - - /// - /// Complete the TaskCompletionSource for an async command with a success result. - /// - /// - /// - /// - private static void CompleteAsyncCommand(string commandName, TaskCompletionSource tcs, object result) - { - try - { - var response = new { status = "success", result }; - string json = JsonConvert.SerializeObject(response); - - if (!tcs.TrySetResult(json)) - { - McpLog.Warn($"TCS for async command '{commandName}' was already completed"); - } - } - catch (Exception ex) - { - McpLog.Error($"Error completing async command '{commandName}': {ex.Message}\n{ex.StackTrace}"); - ReportAsyncFailure(commandName, tcs, ex); - } - } - - /// - /// Report an error that occurred during async command execution. - /// Completes the TaskCompletionSource with an error response. - /// - /// - /// - /// - private static void ReportAsyncFailure(string commandName, TaskCompletionSource tcs, Exception ex) - { - McpLog.Error($"Error in async command '{commandName}': {ex.Message}\n{ex.StackTrace}"); - - var errorResponse = new - { - status = "error", - error = ex.Message, - command = commandName, - stackTrace = ex.StackTrace - }; - - string json; - try - { - json = JsonConvert.SerializeObject(errorResponse); - } - catch (Exception serializationEx) - { - McpLog.Error($"Failed to serialize error response for '{commandName}': {serializationEx.Message}"); - json = "{\"status\":\"error\",\"error\":\"Failed to complete command\"}"; - } - - if (!tcs.TrySetResult(json)) - { - McpLog.Warn($"TCS for async command '{commandName}' was already completed when trying to report error"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/CommandRegistry.cs.meta b/Assets/MCPForUnity/Editor/Tools/CommandRegistry.cs.meta deleted file mode 100644 index bad3002..0000000 --- a/Assets/MCPForUnity/Editor/Tools/CommandRegistry.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 5b61b5a84813b5749a5c64422694a0fa -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/CommandRegistry.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs b/Assets/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs deleted file mode 100644 index f606f77..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; - -namespace MCPForUnity.Editor.Tools -{ - [McpForUnityTool("execute_menu_item", AutoRegister = false)] - /// - /// Tool to execute a Unity Editor menu item by its path. - /// - public static class ExecuteMenuItem - { - // Basic blacklist to prevent execution of disruptive menu items. - private static readonly HashSet _menuPathBlacklist = new HashSet( - StringComparer.OrdinalIgnoreCase) - { - "File/Quit", - }; - - public static object HandleCommand(JObject @params) - { - McpLog.Info("[ExecuteMenuItem] Handling menu item command"); - string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString(); - if (string.IsNullOrWhiteSpace(menuPath)) - { - return new ErrorResponse("Required parameter 'menu_path' or 'menuPath' is missing or empty."); - } - - if (_menuPathBlacklist.Contains(menuPath)) - { - return new ErrorResponse($"Execution of menu item '{menuPath}' is blocked for safety reasons."); - } - - try - { - bool executed = EditorApplication.ExecuteMenuItem(menuPath); - if (!executed) - { - McpLog.Error($"[MenuItemExecutor] Failed to execute menu item '{menuPath}'. It might be invalid, disabled, or context-dependent."); - return new ErrorResponse($"Failed to execute menu item '{menuPath}'. It might be invalid, disabled, or context-dependent."); - } - return new SuccessResponse($"Attempted to execute menu item: '{menuPath}'. Check Unity logs for confirmation or errors."); - } - catch (Exception e) - { - McpLog.Error($"[MenuItemExecutor] Failed to setup execution for '{menuPath}': {e}"); - return new ErrorResponse($"Error setting up execution for menu item '{menuPath}': {e.Message}"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs.meta b/Assets/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs.meta deleted file mode 100644 index ac8d107..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 269232350d16a464091aea9e9fcc9b55 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/ExecuteMenuItem.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/FindGameObjects.cs b/Assets/MCPForUnity/Editor/Tools/FindGameObjects.cs deleted file mode 100644 index d04f094..0000000 --- a/Assets/MCPForUnity/Editor/Tools/FindGameObjects.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Tool for searching GameObjects in the scene. - /// Returns only instance IDs with pagination support. - /// - /// This is a focused search tool that returns lightweight results (IDs only). - /// For detailed GameObject data, use the unity://scene/gameobject/{id} resource. - /// - [McpForUnityTool("find_gameobjects")] - public static class FindGameObjects - { - /// - /// Handles the find_gameobjects command. - /// - /// Command parameters - /// Paginated list of instance IDs - public static object HandleCommand(JObject @params) - { - if (@params == null) - { - return new ErrorResponse("Parameters cannot be null."); - } - - var p = new ToolParams(@params); - - // Parse search parameters - string searchMethod = p.Get("searchMethod", "by_name"); - - // Try searchTerm, search_term, or target (for backwards compatibility) - string searchTerm = p.Get("searchTerm"); - if (string.IsNullOrEmpty(searchTerm)) - { - searchTerm = p.Get("target"); - } - - if (string.IsNullOrEmpty(searchTerm)) - { - return new ErrorResponse("'searchTerm' or 'target' parameter is required."); - } - - // Pagination parameters using standard PaginationRequest - var pagination = PaginationRequest.FromParams(@params, defaultPageSize: 50); - pagination.PageSize = Mathf.Clamp(pagination.PageSize, 1, 500); - - // Search options (supports multiple parameter name variants) - bool includeInactive = p.GetBool("includeInactive", false) || - p.GetBool("searchInactive", false); - - try - { - // Get all matching instance IDs - var allIds = GameObjectLookup.SearchGameObjects(searchMethod, searchTerm, includeInactive, 0); - - // Use standard pagination response - var paginatedResult = PaginationResponse.Create(allIds, pagination); - - return new SuccessResponse("Found GameObjects", new - { - instanceIDs = paginatedResult.Items, - pageSize = paginatedResult.PageSize, - cursor = paginatedResult.Cursor, - nextCursor = paginatedResult.NextCursor, - totalCount = paginatedResult.TotalCount, - hasMore = paginatedResult.HasMore - }); - } - catch (System.Exception ex) - { - McpLog.Error($"[FindGameObjects] Error searching GameObjects: {ex.Message}"); - return new ErrorResponse($"Error searching GameObjects: {ex.Message}"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/FindGameObjects.cs.meta b/Assets/MCPForUnity/Editor/Tools/FindGameObjects.cs.meta deleted file mode 100644 index 28e9519..0000000 --- a/Assets/MCPForUnity/Editor/Tools/FindGameObjects.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4511082b395b14922b34e90f7a23027e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/FindGameObjects.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects.meta b/Assets/MCPForUnity/Editor/Tools/GameObjects.meta deleted file mode 100644 index a1ba389..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: b61d0e8082ed14c1fb500648007bba7a -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/ComponentResolver.cs b/Assets/MCPForUnity/Editor/Tools/GameObjects/ComponentResolver.cs deleted file mode 100644 index 589374a..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/ComponentResolver.cs +++ /dev/null @@ -1,142 +0,0 @@ -#nullable disable -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MCPForUnity.Editor.Helpers; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Component resolver that delegates to UnityTypeResolver. - /// Kept for backwards compatibility. - /// - internal static class ComponentResolver - { - /// - /// Resolve a Component/MonoBehaviour type by short or fully-qualified name. - /// Delegates to UnityTypeResolver.TryResolve with Component constraint. - /// - public static bool TryResolve(string nameOrFullName, out Type type, out string error) - { - return UnityTypeResolver.TryResolve(nameOrFullName, out type, out error, typeof(Component)); - } - - /// - /// Gets all accessible property and field names from a component type. - /// - public static List GetAllComponentProperties(Type componentType) - { - if (componentType == null) return new List(); - - var properties = componentType.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => p.CanRead && p.CanWrite) - .Select(p => p.Name); - - var fields = componentType.GetFields(BindingFlags.Public | BindingFlags.Instance) - .Where(f => !f.IsInitOnly && !f.IsLiteral) - .Select(f => f.Name); - - // Also include SerializeField private fields (common in Unity) - var serializeFields = componentType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance) - .Where(f => f.GetCustomAttribute() != null) - .Select(f => f.Name); - - return properties.Concat(fields).Concat(serializeFields).Distinct().OrderBy(x => x).ToList(); - } - - /// - /// Suggests the most likely property matches for a user's input using fuzzy matching. - /// Uses Levenshtein distance, substring matching, and common naming pattern heuristics. - /// - public static List GetFuzzyPropertySuggestions(string userInput, List availableProperties) - { - if (string.IsNullOrWhiteSpace(userInput) || !availableProperties.Any()) - return new List(); - - var cacheKey = $"{userInput.ToLowerInvariant()}:{string.Join(",", availableProperties)}"; - if (PropertySuggestionCache.TryGetValue(cacheKey, out var cached)) - return cached; - - try - { - var suggestions = GetRuleBasedSuggestions(userInput, availableProperties); - PropertySuggestionCache[cacheKey] = suggestions; - return suggestions; - } - catch (Exception ex) - { - McpLog.Warn($"[Property Matching] Error getting suggestions for '{userInput}': {ex.Message}"); - return new List(); - } - } - - private static readonly Dictionary> PropertySuggestionCache = new(); - - /// - /// Rule-based suggestions that mimic AI behavior for property matching. - /// This provides immediate value while we could add real AI integration later. - /// - private static List GetRuleBasedSuggestions(string userInput, List availableProperties) - { - var suggestions = new List(); - var cleanedInput = userInput.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", ""); - - foreach (var property in availableProperties) - { - var cleanedProperty = property.ToLowerInvariant().Replace(" ", "").Replace("-", "").Replace("_", ""); - - if (cleanedProperty == cleanedInput) - { - suggestions.Add(property); - continue; - } - - var inputWords = userInput.ToLowerInvariant().Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries); - if (inputWords.All(word => cleanedProperty.Contains(word.ToLowerInvariant()))) - { - suggestions.Add(property); - continue; - } - - if (LevenshteinDistance(cleanedInput, cleanedProperty) <= Math.Max(2, cleanedInput.Length / 4)) - { - suggestions.Add(property); - } - } - - return suggestions.OrderBy(s => LevenshteinDistance(cleanedInput, s.ToLowerInvariant().Replace(" ", ""))) - .Take(3) - .ToList(); - } - - /// - /// Calculates Levenshtein distance between two strings for similarity matching. - /// - private static int LevenshteinDistance(string s1, string s2) - { - if (string.IsNullOrEmpty(s1)) return s2?.Length ?? 0; - if (string.IsNullOrEmpty(s2)) return s1.Length; - - var matrix = new int[s1.Length + 1, s2.Length + 1]; - - for (int i = 0; i <= s1.Length; i++) matrix[i, 0] = i; - for (int j = 0; j <= s2.Length; j++) matrix[0, j] = j; - - for (int i = 1; i <= s1.Length; i++) - { - for (int j = 1; j <= s2.Length; j++) - { - int cost = (s2[j - 1] == s1[i - 1]) ? 0 : 1; - matrix[i, j] = Math.Min(Math.Min( - matrix[i - 1, j] + 1, - matrix[i, j - 1] + 1), - matrix[i - 1, j - 1] + cost); - } - } - - return matrix[s1.Length, s2.Length]; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/ComponentResolver.cs.meta b/Assets/MCPForUnity/Editor/Tools/GameObjects/ComponentResolver.cs.meta deleted file mode 100644 index 0c41298..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/ComponentResolver.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: f5e5a46bdebc040c68897fa4b5e689c7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/GameObjects/ComponentResolver.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs deleted file mode 100644 index e37d70a..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs +++ /dev/null @@ -1,410 +0,0 @@ -#nullable disable -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Tools; -using MCPForUnity.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.GameObjects -{ - internal static class GameObjectComponentHelpers - { - internal static object AddComponentInternal(GameObject targetGo, string typeName, JObject properties) - { - Type componentType = FindType(typeName); - if (componentType == null) - { - return new ErrorResponse($"Component type '{typeName}' not found or is not a valid Component."); - } - if (!typeof(Component).IsAssignableFrom(componentType)) - { - return new ErrorResponse($"Type '{typeName}' is not a Component."); - } - - if (componentType == typeof(Transform)) - { - return new ErrorResponse("Cannot add another Transform component."); - } - - bool isAdding2DPhysics = typeof(Rigidbody2D).IsAssignableFrom(componentType) || typeof(Collider2D).IsAssignableFrom(componentType); - bool isAdding3DPhysics = typeof(Rigidbody).IsAssignableFrom(componentType) || typeof(Collider).IsAssignableFrom(componentType); - - if (isAdding2DPhysics) - { - if (targetGo.GetComponent() != null || targetGo.GetComponent() != null) - { - return new ErrorResponse($"Cannot add 2D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 3D Rigidbody or Collider."); - } - } - else if (isAdding3DPhysics) - { - if (targetGo.GetComponent() != null || targetGo.GetComponent() != null) - { - return new ErrorResponse($"Cannot add 3D physics component '{typeName}' because the GameObject '{targetGo.name}' already has a 2D Rigidbody or Collider."); - } - } - - try - { - Component newComponent = Undo.AddComponent(targetGo, componentType); - if (newComponent == null) - { - return new ErrorResponse($"Failed to add component '{typeName}' to '{targetGo.name}'. It might be disallowed (e.g., adding script twice)." - ); - } - - if (newComponent is Light light) - { - light.type = LightType.Directional; - } - - if (properties != null) - { - var setResult = SetComponentPropertiesInternal(targetGo, typeName, properties, newComponent); - if (setResult != null) - { - Undo.DestroyObjectImmediate(newComponent); - return setResult; - } - } - - return null; - } - catch (Exception e) - { - return new ErrorResponse($"Error adding component '{typeName}' to '{targetGo.name}': {e.Message}"); - } - } - - internal static object RemoveComponentInternal(GameObject targetGo, string typeName) - { - if (targetGo == null) - { - return new ErrorResponse("Target GameObject is null."); - } - - Type componentType = FindType(typeName); - if (componentType == null) - { - return new ErrorResponse($"Component type '{typeName}' not found for removal."); - } - - if (componentType == typeof(Transform)) - { - return new ErrorResponse("Cannot remove the Transform component."); - } - - Component componentToRemove = targetGo.GetComponent(componentType); - if (componentToRemove == null) - { - return new ErrorResponse($"Component '{typeName}' not found on '{targetGo.name}' to remove."); - } - - try - { - Undo.DestroyObjectImmediate(componentToRemove); - return null; - } - catch (Exception e) - { - return new ErrorResponse($"Error removing component '{typeName}' from '{targetGo.name}': {e.Message}"); - } - } - - internal static object SetComponentPropertiesInternal(GameObject targetGo, string componentTypeName, JObject properties, Component targetComponentInstance = null) - { - Component targetComponent = targetComponentInstance; - if (targetComponent == null) - { - if (ComponentResolver.TryResolve(componentTypeName, out var compType, out var compError)) - { - targetComponent = targetGo.GetComponent(compType); - } - else - { - targetComponent = targetGo.GetComponent(componentTypeName); - } - } - if (targetComponent == null) - { - return new ErrorResponse($"Component '{componentTypeName}' not found on '{targetGo.name}' to set properties."); - } - - Undo.RecordObject(targetComponent, "Set Component Properties"); - - var failures = new List(); - foreach (var prop in properties.Properties()) - { - string propName = prop.Name; - JToken propValue = prop.Value; - - try - { - bool setResult = SetProperty(targetComponent, propName, propValue); - if (!setResult) - { - var availableProperties = ComponentResolver.GetAllComponentProperties(targetComponent.GetType()); - var suggestions = ComponentResolver.GetFuzzyPropertySuggestions(propName, availableProperties); - var msg = suggestions.Any() - ? $"Property '{propName}' not found. Did you mean: {string.Join(", ", suggestions)}? Available: [{string.Join(", ", availableProperties)}]" - : $"Property '{propName}' not found. Available: [{string.Join(", ", availableProperties)}]"; - McpLog.Warn($"[ManageGameObject] {msg}"); - failures.Add(msg); - } - } - catch (Exception e) - { - McpLog.Error($"[ManageGameObject] Error setting property '{propName}' on '{componentTypeName}': {e.Message}"); - failures.Add($"Error setting '{propName}': {e.Message}"); - } - } - - EditorUtility.SetDirty(targetComponent); - return failures.Count == 0 - ? null - : new ErrorResponse($"One or more properties failed on '{componentTypeName}'.", new { errors = failures }); - } - - private static JsonSerializer InputSerializer => UnityJsonSerializer.Instance; - - private static bool SetProperty(object target, string memberName, JToken value) - { - Type type = target.GetType(); - BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; - - string normalizedName = Helpers.ParamCoercion.NormalizePropertyName(memberName); - var inputSerializer = InputSerializer; - - try - { - if (memberName.Contains('.') || memberName.Contains('[')) - { - return SetNestedProperty(target, memberName, value, inputSerializer); - } - - PropertyInfo propInfo = type.GetProperty(memberName, flags) ?? type.GetProperty(normalizedName, flags); - if (propInfo != null && propInfo.CanWrite) - { - object convertedValue = ConvertJTokenToType(value, propInfo.PropertyType, inputSerializer); - if (convertedValue != null || value.Type == JTokenType.Null) - { - propInfo.SetValue(target, convertedValue); - return true; - } - } - else - { - FieldInfo fieldInfo = type.GetField(memberName, flags) ?? type.GetField(normalizedName, flags); - if (fieldInfo != null) - { - object convertedValue = ConvertJTokenToType(value, fieldInfo.FieldType, inputSerializer); - if (convertedValue != null || value.Type == JTokenType.Null) - { - fieldInfo.SetValue(target, convertedValue); - return true; - } - } - else - { - var npField = type.GetField(memberName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase) - ?? type.GetField(normalizedName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase); - if (npField != null && npField.GetCustomAttribute() != null) - { - object convertedValue = ConvertJTokenToType(value, npField.FieldType, inputSerializer); - if (convertedValue != null || value.Type == JTokenType.Null) - { - npField.SetValue(target, convertedValue); - return true; - } - } - } - } - } - catch (Exception ex) - { - McpLog.Error($"[SetProperty] Failed to set '{memberName}' on {type.Name}: {ex.Message}\nToken: {value.ToString(Formatting.None)}"); - } - return false; - } - - private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer) - { - try - { - string[] pathParts = SplitPropertyPath(path); - if (pathParts.Length == 0) - return false; - - object currentObject = target; - Type currentType = currentObject.GetType(); - BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase; - - for (int i = 0; i < pathParts.Length - 1; i++) - { - string part = pathParts[i]; - bool isArray = false; - int arrayIndex = -1; - - if (part.Contains("[")) - { - int startBracket = part.IndexOf('['); - int endBracket = part.IndexOf(']'); - if (startBracket > 0 && endBracket > startBracket) - { - string indexStr = part.Substring(startBracket + 1, endBracket - startBracket - 1); - if (int.TryParse(indexStr, out arrayIndex)) - { - isArray = true; - part = part.Substring(0, startBracket); - } - } - } - - PropertyInfo propInfo = currentType.GetProperty(part, flags); - FieldInfo fieldInfo = null; - if (propInfo == null) - { - fieldInfo = currentType.GetField(part, flags); - if (fieldInfo == null) - { - McpLog.Warn($"[SetNestedProperty] Could not find property or field '{part}' on type '{currentType.Name}'"); - return false; - } - } - - currentObject = propInfo != null ? propInfo.GetValue(currentObject) : fieldInfo.GetValue(currentObject); - if (currentObject == null) - { - McpLog.Warn($"[SetNestedProperty] Property '{part}' is null, cannot access nested properties."); - return false; - } - - if (isArray) - { - if (currentObject is Material[]) - { - var materials = currentObject as Material[]; - if (arrayIndex < 0 || arrayIndex >= materials.Length) - { - McpLog.Warn($"[SetNestedProperty] Material index {arrayIndex} out of range (0-{materials.Length - 1})"); - return false; - } - currentObject = materials[arrayIndex]; - } - else if (currentObject is System.Collections.IList) - { - var list = currentObject as System.Collections.IList; - if (arrayIndex < 0 || arrayIndex >= list.Count) - { - McpLog.Warn($"[SetNestedProperty] Index {arrayIndex} out of range (0-{list.Count - 1})"); - return false; - } - currentObject = list[arrayIndex]; - } - else - { - McpLog.Warn($"[SetNestedProperty] Property '{part}' is not an array or list, cannot access by index."); - return false; - } - } - - currentType = currentObject.GetType(); - } - - string finalPart = pathParts[pathParts.Length - 1]; - - if (currentObject is Material material && finalPart.StartsWith("_")) - { - return MaterialOps.TrySetShaderProperty(material, finalPart, value, inputSerializer); - } - - PropertyInfo finalPropInfo = currentType.GetProperty(finalPart, flags); - if (finalPropInfo != null && finalPropInfo.CanWrite) - { - object convertedValue = ConvertJTokenToType(value, finalPropInfo.PropertyType, inputSerializer); - if (convertedValue != null || value.Type == JTokenType.Null) - { - finalPropInfo.SetValue(currentObject, convertedValue); - return true; - } - } - else - { - FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags); - if (finalFieldInfo != null) - { - object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType, inputSerializer); - if (convertedValue != null || value.Type == JTokenType.Null) - { - finalFieldInfo.SetValue(currentObject, convertedValue); - return true; - } - } - } - } - catch (Exception ex) - { - McpLog.Error($"[SetNestedProperty] Error setting nested property '{path}': {ex.Message}\nToken: {value.ToString(Formatting.None)}"); - } - - return false; - } - - private static string[] SplitPropertyPath(string path) - { - List parts = new List(); - int startIndex = 0; - bool inBrackets = false; - - for (int i = 0; i < path.Length; i++) - { - char c = path[i]; - - if (c == '[') - { - inBrackets = true; - } - else if (c == ']') - { - inBrackets = false; - } - else if (c == '.' && !inBrackets) - { - parts.Add(path.Substring(startIndex, i - startIndex)); - startIndex = i + 1; - } - } - if (startIndex < path.Length) - { - parts.Add(path.Substring(startIndex)); - } - return parts.ToArray(); - } - - private static object ConvertJTokenToType(JToken token, Type targetType, JsonSerializer inputSerializer) - { - return PropertyConversion.ConvertToType(token, targetType); - } - - private static Type FindType(string typeName) - { - if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error)) - { - return resolvedType; - } - - if (!string.IsNullOrEmpty(error)) - { - McpLog.Warn($"[FindType] {error}"); - } - - return null; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs.meta b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs.meta deleted file mode 100644 index c56d67b..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b580af06e2d3a4788960f3f779edac54 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectCreate.cs b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectCreate.cs deleted file mode 100644 index 6954a26..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectCreate.cs +++ /dev/null @@ -1,338 +0,0 @@ -#nullable disable -using System; -using System.Linq; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEditorInternal; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.GameObjects -{ - internal static class GameObjectCreate - { - internal static object Handle(JObject @params) - { - string name = @params["name"]?.ToString(); - if (string.IsNullOrEmpty(name)) - { - return new ErrorResponse("'name' parameter is required for 'create' action."); - } - - // Get prefab creation parameters - bool saveAsPrefab = @params["saveAsPrefab"]?.ToObject() ?? false; - string prefabPath = @params["prefabPath"]?.ToString(); - string tag = @params["tag"]?.ToString(); - string primitiveType = @params["primitiveType"]?.ToString(); - GameObject newGo = null; - - // --- Try Instantiating Prefab First --- - string originalPrefabPath = prefabPath; - if (!saveAsPrefab && !string.IsNullOrEmpty(prefabPath)) - { - string extension = System.IO.Path.GetExtension(prefabPath); - - if (!prefabPath.Contains("/") && (string.IsNullOrEmpty(extension) || extension.Equals(".prefab", StringComparison.OrdinalIgnoreCase))) - { - string prefabNameOnly = prefabPath; - McpLog.Info($"[ManageGameObject.Create] Searching for prefab named: '{prefabNameOnly}'"); - string[] guids = AssetDatabase.FindAssets($"t:Prefab {prefabNameOnly}"); - if (guids.Length == 0) - { - return new ErrorResponse($"Prefab named '{prefabNameOnly}' not found anywhere in the project."); - } - else if (guids.Length > 1) - { - string foundPaths = string.Join(", ", guids.Select(g => AssetDatabase.GUIDToAssetPath(g))); - return new ErrorResponse($"Multiple prefabs found matching name '{prefabNameOnly}': {foundPaths}. Please provide a more specific path."); - } - else - { - prefabPath = AssetDatabase.GUIDToAssetPath(guids[0]); - McpLog.Info($"[ManageGameObject.Create] Found unique prefab at path: '{prefabPath}'"); - } - } - else if (prefabPath.Contains("/") && string.IsNullOrEmpty(extension)) - { - McpLog.Warn($"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' has no extension. Assuming it's a prefab and appending .prefab."); - prefabPath += ".prefab"; - } - else if (!prefabPath.Contains("/") && !string.IsNullOrEmpty(extension) && !extension.Equals(".prefab", StringComparison.OrdinalIgnoreCase)) - { - string fileName = prefabPath; - string fileNameWithoutExtension = System.IO.Path.GetFileNameWithoutExtension(fileName); - McpLog.Info($"[ManageGameObject.Create] Searching for asset file named: '{fileName}'"); - - string[] guids = AssetDatabase.FindAssets(fileNameWithoutExtension); - var matches = guids - .Select(g => AssetDatabase.GUIDToAssetPath(g)) - .Where(p => p.EndsWith("/" + fileName, StringComparison.OrdinalIgnoreCase) || p.Equals(fileName, StringComparison.OrdinalIgnoreCase)) - .ToArray(); - - if (matches.Length == 0) - { - return new ErrorResponse($"Asset file '{fileName}' not found anywhere in the project."); - } - else if (matches.Length > 1) - { - string foundPaths = string.Join(", ", matches); - return new ErrorResponse($"Multiple assets found matching file name '{fileName}': {foundPaths}. Please provide a more specific path."); - } - else - { - prefabPath = matches[0]; - McpLog.Info($"[ManageGameObject.Create] Found unique asset at path: '{prefabPath}'"); - } - } - - GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); - if (prefabAsset != null) - { - try - { - newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject; - - if (newGo == null) - { - McpLog.Error($"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject."); - return new ErrorResponse($"Failed to instantiate prefab at '{prefabPath}'."); - } - if (!string.IsNullOrEmpty(name)) - { - newGo.name = name; - } - Undo.RegisterCreatedObjectUndo(newGo, $"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'"); - McpLog.Info($"[ManageGameObject.Create] Instantiated prefab '{prefabAsset.name}' from path '{prefabPath}' as '{newGo.name}'."); - } - catch (Exception e) - { - return new ErrorResponse($"Error instantiating prefab '{prefabPath}': {e.Message}"); - } - } - else - { - return new ErrorResponse($"Asset not found or not a GameObject at path: '{prefabPath}'."); - } - } - - // --- Fallback: Create Primitive or Empty GameObject --- - bool createdNewObject = false; - if (newGo == null) - { - if (!string.IsNullOrEmpty(primitiveType)) - { - try - { - PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true); - newGo = GameObject.CreatePrimitive(type); - if (!string.IsNullOrEmpty(name)) - { - newGo.name = name; - } - else - { - UnityEngine.Object.DestroyImmediate(newGo); - return new ErrorResponse("'name' parameter is required when creating a primitive."); - } - createdNewObject = true; - } - catch (ArgumentException) - { - return new ErrorResponse($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}"); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to create primitive '{primitiveType}': {e.Message}"); - } - } - else - { - if (string.IsNullOrEmpty(name)) - { - return new ErrorResponse("'name' parameter is required for 'create' action when not instantiating a prefab or creating a primitive."); - } - newGo = new GameObject(name); - createdNewObject = true; - } - - if (createdNewObject) - { - Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'"); - } - } - - if (newGo == null) - { - return new ErrorResponse("Failed to create or instantiate the GameObject."); - } - - Undo.RecordObject(newGo.transform, "Set GameObject Transform"); - Undo.RecordObject(newGo, "Set GameObject Properties"); - - // Set Parent - JToken parentToken = @params["parent"]; - if (parentToken != null) - { - GameObject parentGo = ManageGameObjectCommon.FindObjectInternal(parentToken, "by_id_or_name_or_path"); - if (parentGo == null) - { - UnityEngine.Object.DestroyImmediate(newGo); - return new ErrorResponse($"Parent specified ('{parentToken}') but not found."); - } - newGo.transform.SetParent(parentGo.transform, true); - } - - // Set Transform - Vector3? position = VectorParsing.ParseVector3(@params["position"]); - Vector3? rotation = VectorParsing.ParseVector3(@params["rotation"]); - Vector3? scale = VectorParsing.ParseVector3(@params["scale"]); - - if (position.HasValue) newGo.transform.localPosition = position.Value; - if (rotation.HasValue) newGo.transform.localEulerAngles = rotation.Value; - if (scale.HasValue) newGo.transform.localScale = scale.Value; - - // Set Tag - if (!string.IsNullOrEmpty(tag)) - { - if (tag != "Untagged" && !System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tag)) - { - McpLog.Info($"[ManageGameObject.Create] Tag '{tag}' not found. Creating it."); - try - { - InternalEditorUtility.AddTag(tag); - } - catch (Exception ex) - { - UnityEngine.Object.DestroyImmediate(newGo); - return new ErrorResponse($"Failed to create tag '{tag}': {ex.Message}."); - } - } - - try - { - newGo.tag = tag; - } - catch (Exception ex) - { - UnityEngine.Object.DestroyImmediate(newGo); - return new ErrorResponse($"Failed to set tag to '{tag}' during creation: {ex.Message}."); - } - } - - // Set Layer - string layerName = @params["layer"]?.ToString(); - if (!string.IsNullOrEmpty(layerName)) - { - int layerId = LayerMask.NameToLayer(layerName); - if (layerId != -1) - { - newGo.layer = layerId; - } - else - { - McpLog.Warn($"[ManageGameObject.Create] Layer '{layerName}' not found. Using default layer."); - } - } - - // Add Components - if (@params["componentsToAdd"] is JArray componentsToAddArray) - { - foreach (var compToken in componentsToAddArray) - { - string typeName = null; - JObject properties = null; - - if (compToken.Type == JTokenType.String) - { - typeName = compToken.ToString(); - } - else if (compToken is JObject compObj) - { - typeName = compObj["typeName"]?.ToString(); - properties = compObj["properties"] as JObject; - } - - if (!string.IsNullOrEmpty(typeName)) - { - var addResult = GameObjectComponentHelpers.AddComponentInternal(newGo, typeName, properties); - if (addResult != null) - { - UnityEngine.Object.DestroyImmediate(newGo); - return addResult; - } - } - else - { - McpLog.Warn($"[ManageGameObject] Invalid component format in componentsToAdd: {compToken}"); - } - } - } - - // Save as Prefab ONLY if we *created* a new object AND saveAsPrefab is true - GameObject finalInstance = newGo; - if (createdNewObject && saveAsPrefab) - { - string finalPrefabPath = prefabPath; - if (string.IsNullOrEmpty(finalPrefabPath)) - { - UnityEngine.Object.DestroyImmediate(newGo); - return new ErrorResponse("'prefabPath' is required when 'saveAsPrefab' is true and creating a new object."); - } - if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) - { - McpLog.Info($"[ManageGameObject.Create] Appending .prefab extension to save path: '{finalPrefabPath}' -> '{finalPrefabPath}.prefab'"); - finalPrefabPath += ".prefab"; - } - - try - { - string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath); - if (!string.IsNullOrEmpty(directoryPath) && !System.IO.Directory.Exists(directoryPath)) - { - System.IO.Directory.CreateDirectory(directoryPath); - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); - McpLog.Info($"[ManageGameObject.Create] Created directory for prefab: {directoryPath}"); - } - - finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(newGo, finalPrefabPath, InteractionMode.UserAction); - - if (finalInstance == null) - { - UnityEngine.Object.DestroyImmediate(newGo); - return new ErrorResponse($"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions."); - } - McpLog.Info($"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected."); - } - catch (Exception e) - { - UnityEngine.Object.DestroyImmediate(newGo); - return new ErrorResponse($"Error saving prefab '{finalPrefabPath}': {e.Message}"); - } - } - - Selection.activeGameObject = finalInstance; - - string messagePrefabPath = - finalInstance == null - ? originalPrefabPath - : AssetDatabase.GetAssetPath(PrefabUtility.GetCorrespondingObjectFromSource(finalInstance) ?? (UnityEngine.Object)finalInstance); - - string successMessage; - if (!createdNewObject && !string.IsNullOrEmpty(messagePrefabPath)) - { - successMessage = $"Prefab '{messagePrefabPath}' instantiated successfully as '{finalInstance.name}'."; - } - else if (createdNewObject && saveAsPrefab && !string.IsNullOrEmpty(messagePrefabPath)) - { - successMessage = $"GameObject '{finalInstance.name}' created and saved as prefab to '{messagePrefabPath}'."; - } - else - { - successMessage = $"GameObject '{finalInstance.name}' created successfully in scene."; - } - - return new SuccessResponse(successMessage, Helpers.GameObjectSerializer.GetGameObjectData(finalInstance)); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectCreate.cs.meta b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectCreate.cs.meta deleted file mode 100644 index ba239b0..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectCreate.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 0931774a07e4b4626b4261dd8d0974c2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectCreate.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDelete.cs b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDelete.cs deleted file mode 100644 index f568185..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDelete.cs +++ /dev/null @@ -1,48 +0,0 @@ -#nullable disable -using System.Collections.Generic; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.GameObjects -{ - internal static class GameObjectDelete - { - internal static object Handle(JToken targetToken, string searchMethod) - { - List targets = ManageGameObjectCommon.FindObjectsInternal(targetToken, searchMethod, true); - - if (targets.Count == 0) - { - return new ErrorResponse($"Target GameObject(s) ('{targetToken}') not found using method '{searchMethod ?? "default"}'."); - } - - List deletedObjects = new List(); - foreach (var targetGo in targets) - { - if (targetGo != null) - { - string goName = targetGo.name; - int goId = targetGo.GetInstanceID(); - // Note: Undo.DestroyObjectImmediate doesn't work reliably in test context, - // so we use Object.DestroyImmediate. This means delete isn't undoable. - // TODO: Investigate Undo.DestroyObjectImmediate behavior in Unity 2022+ - Object.DestroyImmediate(targetGo); - deletedObjects.Add(new { name = goName, instanceID = goId }); - } - } - - if (deletedObjects.Count > 0) - { - string message = - targets.Count == 1 - ? $"GameObject '{((dynamic)deletedObjects[0]).name}' deleted successfully." - : $"{deletedObjects.Count} GameObjects deleted successfully."; - return new SuccessResponse(message, deletedObjects); - } - - return new ErrorResponse("Failed to delete target GameObject(s)."); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDelete.cs.meta b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDelete.cs.meta deleted file mode 100644 index 0a53914..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDelete.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 505a482aaf60b415abd794737a630b10 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDelete.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDuplicate.cs b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDuplicate.cs deleted file mode 100644 index 6faeb2a..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDuplicate.cs +++ /dev/null @@ -1,86 +0,0 @@ -#nullable disable -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.GameObjects -{ - internal static class GameObjectDuplicate - { - internal static object Handle(JObject @params, JToken targetToken, string searchMethod) - { - GameObject sourceGo = ManageGameObjectCommon.FindObjectInternal(targetToken, searchMethod); - if (sourceGo == null) - { - return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'."); - } - - string newName = @params["new_name"]?.ToString(); - Vector3? position = VectorParsing.ParseVector3(@params["position"]); - Vector3? offset = VectorParsing.ParseVector3(@params["offset"]); - JToken parentToken = @params["parent"]; - - GameObject duplicatedGo = UnityEngine.Object.Instantiate(sourceGo); - Undo.RegisterCreatedObjectUndo(duplicatedGo, $"Duplicate {sourceGo.name}"); - - if (!string.IsNullOrEmpty(newName)) - { - duplicatedGo.name = newName; - } - else - { - duplicatedGo.name = sourceGo.name.Replace("(Clone)", "").Trim() + "_Copy"; - } - - if (position.HasValue) - { - duplicatedGo.transform.position = position.Value; - } - else if (offset.HasValue) - { - duplicatedGo.transform.position = sourceGo.transform.position + offset.Value; - } - - if (parentToken != null) - { - if (parentToken.Type == JTokenType.Null || (parentToken.Type == JTokenType.String && string.IsNullOrEmpty(parentToken.ToString()))) - { - duplicatedGo.transform.SetParent(null); - } - else - { - GameObject newParent = ManageGameObjectCommon.FindObjectInternal(parentToken, "by_id_or_name_or_path"); - if (newParent != null) - { - duplicatedGo.transform.SetParent(newParent.transform, true); - } - else - { - McpLog.Warn($"[ManageGameObject.Duplicate] Parent '{parentToken}' not found. Object will remain at root level."); - } - } - } - else - { - duplicatedGo.transform.SetParent(sourceGo.transform.parent, true); - } - - EditorUtility.SetDirty(duplicatedGo); - EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); - - Selection.activeGameObject = duplicatedGo; - - return new SuccessResponse( - $"Duplicated '{sourceGo.name}' as '{duplicatedGo.name}'.", - new - { - originalName = sourceGo.name, - originalId = sourceGo.GetInstanceID(), - duplicatedObject = Helpers.GameObjectSerializer.GetGameObjectData(duplicatedGo) - } - ); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDuplicate.cs.meta b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDuplicate.cs.meta deleted file mode 100644 index 2c2a755..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDuplicate.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 698728d56425a47af92a45377031a48b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectDuplicate.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectHandlers.cs b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectHandlers.cs deleted file mode 100644 index bca3687..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectHandlers.cs +++ /dev/null @@ -1,22 +0,0 @@ -#nullable disable -using Newtonsoft.Json.Linq; - -namespace MCPForUnity.Editor.Tools.GameObjects -{ - internal static class GameObjectHandlers - { - internal static object Create(JObject @params) => GameObjectCreate.Handle(@params); - - internal static object Modify(JObject @params, JToken targetToken, string searchMethod) - => GameObjectModify.Handle(@params, targetToken, searchMethod); - - internal static object Delete(JToken targetToken, string searchMethod) - => GameObjectDelete.Handle(targetToken, searchMethod); - - internal static object Duplicate(JObject @params, JToken targetToken, string searchMethod) - => GameObjectDuplicate.Handle(@params, targetToken, searchMethod); - - internal static object MoveRelative(JObject @params, JToken targetToken, string searchMethod) - => GameObjectMoveRelative.Handle(@params, targetToken, searchMethod); - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectHandlers.cs.meta b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectHandlers.cs.meta deleted file mode 100644 index 0538a15..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectHandlers.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: f3cf2313460d44a09b258d2ee04c5ef0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectHandlers.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs deleted file mode 100644 index 44511e9..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs +++ /dev/null @@ -1,297 +0,0 @@ -#nullable disable -using System; -using System.Collections.Generic; -using System.Linq; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEditorInternal; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.GameObjects -{ - internal static class GameObjectModify - { - internal static object Handle(JObject @params, JToken targetToken, string searchMethod) - { - // When setActive=true is specified, we need to search for inactive objects - // otherwise we can't find an inactive object to activate it - JObject findParams = null; - if (@params["setActive"]?.ToObject() == true) - { - findParams = new JObject { ["searchInactive"] = true }; - } - - GameObject targetGo = ManageGameObjectCommon.FindObjectInternal(targetToken, searchMethod, findParams); - if (targetGo == null) - { - return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'."); - } - - Undo.RecordObject(targetGo.transform, "Modify GameObject Transform"); - Undo.RecordObject(targetGo, "Modify GameObject Properties"); - - bool modified = false; - - string name = @params["name"]?.ToString(); - if (!string.IsNullOrEmpty(name) && targetGo.name != name) - { - // Check if we're renaming the root object of an open prefab stage - var prefabStageForRename = PrefabStageUtility.GetCurrentPrefabStage(); - bool isRenamingPrefabRoot = prefabStageForRename != null && - prefabStageForRename.prefabContentsRoot == targetGo; - - if (isRenamingPrefabRoot) - { - // Rename the prefab asset file to match the new name (avoids Unity dialog) - string assetPath = prefabStageForRename.assetPath; - string directory = System.IO.Path.GetDirectoryName(assetPath); - string newAssetPath = AssetPathUtility.NormalizeSeparators(System.IO.Path.Combine(directory, name + ".prefab")); - - // Only rename if the path actually changes - if (newAssetPath != assetPath) - { - // Check for collision using GUID comparison - string currentGuid = AssetDatabase.AssetPathToGUID(assetPath); - string existingGuid = AssetDatabase.AssetPathToGUID(newAssetPath); - - // Collision only if there's a different asset at the new path - if (!string.IsNullOrEmpty(existingGuid) && existingGuid != currentGuid) - { - return new ErrorResponse($"Cannot rename prefab root to '{name}': a prefab already exists at '{newAssetPath}'."); - } - - // Rename the asset file - string renameError = AssetDatabase.RenameAsset(assetPath, name); - if (!string.IsNullOrEmpty(renameError)) - { - return new ErrorResponse($"Failed to rename prefab asset: {renameError}"); - } - - McpLog.Info($"[GameObjectModify] Renamed prefab asset from '{assetPath}' to '{newAssetPath}'"); - } - } - - targetGo.name = name; - modified = true; - } - - JToken parentToken = @params["parent"]; - if (parentToken != null) - { - GameObject newParentGo = ManageGameObjectCommon.FindObjectInternal(parentToken, "by_id_or_name_or_path"); - if ( - newParentGo == null - && !(parentToken.Type == JTokenType.Null - || (parentToken.Type == JTokenType.String && string.IsNullOrEmpty(parentToken.ToString()))) - ) - { - return new ErrorResponse($"New parent ('{parentToken}') not found."); - } - if (newParentGo != null && newParentGo.transform.IsChildOf(targetGo.transform)) - { - return new ErrorResponse($"Cannot parent '{targetGo.name}' to '{newParentGo.name}', as it would create a hierarchy loop."); - } - if (targetGo.transform.parent != (newParentGo?.transform)) - { - targetGo.transform.SetParent(newParentGo?.transform, true); - modified = true; - } - } - - bool? setActive = @params["setActive"]?.ToObject(); - if (setActive.HasValue && targetGo.activeSelf != setActive.Value) - { - targetGo.SetActive(setActive.Value); - modified = true; - } - - string tag = @params["tag"]?.ToString(); - if (tag != null && targetGo.tag != tag) - { - string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag; - - if (tagToSet != "Untagged" && !System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tagToSet)) - { - McpLog.Info($"[ManageGameObject] Tag '{tagToSet}' not found. Creating it."); - try - { - InternalEditorUtility.AddTag(tagToSet); - } - catch (Exception ex) - { - return new ErrorResponse($"Failed to create tag '{tagToSet}': {ex.Message}."); - } - } - - try - { - targetGo.tag = tagToSet; - modified = true; - } - catch (Exception ex) - { - return new ErrorResponse($"Failed to set tag to '{tagToSet}': {ex.Message}."); - } - } - - string layerName = @params["layer"]?.ToString(); - if (!string.IsNullOrEmpty(layerName)) - { - int layerId = LayerMask.NameToLayer(layerName); - if (layerId == -1) - { - return new ErrorResponse($"Invalid layer specified: '{layerName}'. Use a valid layer name."); - } - if (layerId != -1 && targetGo.layer != layerId) - { - targetGo.layer = layerId; - modified = true; - } - } - - Vector3? position = VectorParsing.ParseVector3(@params["position"]); - Vector3? rotation = VectorParsing.ParseVector3(@params["rotation"]); - Vector3? scale = VectorParsing.ParseVector3(@params["scale"]); - - if (position.HasValue && targetGo.transform.localPosition != position.Value) - { - targetGo.transform.localPosition = position.Value; - modified = true; - } - if (rotation.HasValue && targetGo.transform.localEulerAngles != rotation.Value) - { - targetGo.transform.localEulerAngles = rotation.Value; - modified = true; - } - if (scale.HasValue && targetGo.transform.localScale != scale.Value) - { - targetGo.transform.localScale = scale.Value; - modified = true; - } - - if (@params["componentsToRemove"] is JArray componentsToRemoveArray) - { - foreach (var compToken in componentsToRemoveArray) - { - string typeName = compToken.ToString(); - if (!string.IsNullOrEmpty(typeName)) - { - var removeResult = GameObjectComponentHelpers.RemoveComponentInternal(targetGo, typeName); - if (removeResult != null) - return removeResult; - modified = true; - } - } - } - - if (@params["componentsToAdd"] is JArray componentsToAddArrayModify) - { - foreach (var compToken in componentsToAddArrayModify) - { - string typeName = null; - JObject properties = null; - if (compToken.Type == JTokenType.String) - typeName = compToken.ToString(); - else if (compToken is JObject compObj) - { - typeName = compObj["typeName"]?.ToString(); - properties = compObj["properties"] as JObject; - } - - if (!string.IsNullOrEmpty(typeName)) - { - var addResult = GameObjectComponentHelpers.AddComponentInternal(targetGo, typeName, properties); - if (addResult != null) - return addResult; - modified = true; - } - } - } - - var componentErrors = new List(); - if (@params["componentProperties"] is JObject componentPropertiesObj) - { - foreach (var prop in componentPropertiesObj.Properties()) - { - string compName = prop.Name; - JObject propertiesToSet = prop.Value as JObject; - if (propertiesToSet != null) - { - var setResult = GameObjectComponentHelpers.SetComponentPropertiesInternal(targetGo, compName, propertiesToSet); - if (setResult != null) - { - componentErrors.Add(setResult); - } - else - { - modified = true; - } - } - } - } - - if (componentErrors.Count > 0) - { - var aggregatedErrors = new List(); - foreach (var errorObj in componentErrors) - { - try - { - var dataProp = errorObj?.GetType().GetProperty("data"); - var dataVal = dataProp?.GetValue(errorObj); - if (dataVal != null) - { - var errorsProp = dataVal.GetType().GetProperty("errors"); - var errorsEnum = errorsProp?.GetValue(dataVal) as System.Collections.IEnumerable; - if (errorsEnum != null) - { - foreach (var item in errorsEnum) - { - var s = item?.ToString(); - if (!string.IsNullOrEmpty(s)) aggregatedErrors.Add(s); - } - } - } - } - catch (Exception ex) - { - McpLog.Warn($"[GameObjectModify] Error aggregating component errors: {ex.Message}"); - } - } - - return new ErrorResponse( - $"One or more component property operations failed on '{targetGo.name}'.", - new { componentErrors = componentErrors, errors = aggregatedErrors } - ); - } - - if (!modified) - { - return new SuccessResponse( - $"No modifications applied to GameObject '{targetGo.name}'.", - Helpers.GameObjectSerializer.GetGameObjectData(targetGo) - ); - } - - EditorUtility.SetDirty(targetGo); - - // Mark the appropriate scene as dirty (handles both regular scenes and prefab stages) - var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); - if (prefabStage != null) - { - EditorSceneManager.MarkSceneDirty(prefabStage.scene); - } - else - { - EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); - } - - return new SuccessResponse( - $"GameObject '{targetGo.name}' modified successfully.", - Helpers.GameObjectSerializer.GetGameObjectData(targetGo) - ); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs.meta b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs.meta deleted file mode 100644 index 2d7f4f9..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: ec5e33513bd094257a26ef6f75ea4574 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectModify.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectMoveRelative.cs b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectMoveRelative.cs deleted file mode 100644 index 3335d28..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectMoveRelative.cs +++ /dev/null @@ -1,119 +0,0 @@ -#nullable disable -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.GameObjects -{ - internal static class GameObjectMoveRelative - { - internal static object Handle(JObject @params, JToken targetToken, string searchMethod) - { - GameObject targetGo = ManageGameObjectCommon.FindObjectInternal(targetToken, searchMethod); - if (targetGo == null) - { - return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'."); - } - - JToken referenceToken = @params["reference_object"]; - if (referenceToken == null) - { - return new ErrorResponse("'reference_object' parameter is required for 'move_relative' action."); - } - - GameObject referenceGo = ManageGameObjectCommon.FindObjectInternal(referenceToken, "by_id_or_name_or_path"); - if (referenceGo == null) - { - return new ErrorResponse($"Reference object '{referenceToken}' not found."); - } - - string direction = @params["direction"]?.ToString()?.ToLower(); - float distance = @params["distance"]?.ToObject() ?? 1f; - Vector3? customOffset = VectorParsing.ParseVector3(@params["offset"]); - bool useWorldSpace = @params["world_space"]?.ToObject() ?? true; - - Undo.RecordObject(targetGo.transform, $"Move {targetGo.name} relative to {referenceGo.name}"); - - Vector3 newPosition; - - if (customOffset.HasValue) - { - if (useWorldSpace) - { - newPosition = referenceGo.transform.position + customOffset.Value; - } - else - { - newPosition = referenceGo.transform.TransformPoint(customOffset.Value); - } - } - else if (!string.IsNullOrEmpty(direction)) - { - Vector3 directionVector = GetDirectionVector(direction, referenceGo.transform, useWorldSpace); - newPosition = referenceGo.transform.position + directionVector * distance; - } - else - { - return new ErrorResponse("Either 'direction' or 'offset' parameter is required for 'move_relative' action."); - } - - targetGo.transform.position = newPosition; - - EditorUtility.SetDirty(targetGo); - EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); - - return new SuccessResponse( - $"Moved '{targetGo.name}' relative to '{referenceGo.name}'.", - new - { - movedObject = targetGo.name, - referenceObject = referenceGo.name, - newPosition = new[] { targetGo.transform.position.x, targetGo.transform.position.y, targetGo.transform.position.z }, - direction = direction, - distance = distance, - gameObject = Helpers.GameObjectSerializer.GetGameObjectData(targetGo) - } - ); - } - - private static Vector3 GetDirectionVector(string direction, Transform referenceTransform, bool useWorldSpace) - { - if (useWorldSpace) - { - switch (direction) - { - case "right": return Vector3.right; - case "left": return Vector3.left; - case "up": return Vector3.up; - case "down": return Vector3.down; - case "forward": - case "front": return Vector3.forward; - case "back": - case "backward": - case "behind": return Vector3.back; - default: - McpLog.Warn($"[ManageGameObject.MoveRelative] Unknown direction '{direction}', defaulting to forward."); - return Vector3.forward; - } - } - - switch (direction) - { - case "right": return referenceTransform.right; - case "left": return -referenceTransform.right; - case "up": return referenceTransform.up; - case "down": return -referenceTransform.up; - case "forward": - case "front": return referenceTransform.forward; - case "back": - case "backward": - case "behind": return -referenceTransform.forward; - default: - McpLog.Warn($"[ManageGameObject.MoveRelative] Unknown direction '{direction}', defaulting to forward."); - return referenceTransform.forward; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectMoveRelative.cs.meta b/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectMoveRelative.cs.meta deleted file mode 100644 index 11f936c..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectMoveRelative.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 8b19997a165de45c2af3ada79a6d3f08 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/GameObjects/GameObjectMoveRelative.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs b/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs deleted file mode 100644 index 6900234..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs +++ /dev/null @@ -1,115 +0,0 @@ -#nullable disable -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MCPForUnity.Editor.Helpers; // For Response class -using Newtonsoft.Json.Linq; -using UnityEngine; -using UnityEngine.SceneManagement; - -namespace MCPForUnity.Editor.Tools.GameObjects -{ - /// - /// Handles GameObject manipulation within the current scene (CRUD, find, components). - /// - [McpForUnityTool("manage_gameobject", AutoRegister = false)] - public static class ManageGameObject - { - // --- Main Handler --- - - public static object HandleCommand(JObject @params) - { - if (@params == null) - { - return new ErrorResponse("Parameters cannot be null."); - } - - string action = @params["action"]?.ToString().ToLower(); - if (string.IsNullOrEmpty(action)) - { - return new ErrorResponse("Action parameter is required."); - } - - // Parameters used by various actions - JToken targetToken = @params["target"]; // Can be string (name/path) or int (instanceID) - string name = @params["name"]?.ToString(); - - // --- Usability Improvement: Alias 'name' to 'target' for modification actions --- - // If 'target' is missing but 'name' is provided, and we aren't creating a new object, - // assume the user meant "find object by name". - if (targetToken == null && !string.IsNullOrEmpty(name) && action != "create") - { - targetToken = name; - // We don't update @params["target"] because we use targetToken locally mostly, - // but some downstream methods might parse @params directly. Let's update @params too for safety. - @params["target"] = name; - } - // ------------------------------------------------------------------------------- - - string searchMethod = @params["searchMethod"]?.ToString().ToLower(); - string tag = @params["tag"]?.ToString(); - string layer = @params["layer"]?.ToString(); - JToken parentToken = @params["parent"]; - - // Coerce string JSON to JObject for 'componentProperties' if provided as a JSON string - var componentPropsToken = @params["componentProperties"]; - if (componentPropsToken != null && componentPropsToken.Type == JTokenType.String) - { - try - { - var parsed = JObject.Parse(componentPropsToken.ToString()); - @params["componentProperties"] = parsed; - } - catch (Exception e) - { - McpLog.Warn($"[ManageGameObject] Could not parse 'componentProperties' JSON string: {e.Message}"); - } - } - - // --- Prefab Asset Check --- - // Prefab assets require different tools. Only 'create' (instantiation) is valid here. - string targetPath = - targetToken?.Type == JTokenType.String ? targetToken.ToString() : null; - if ( - !string.IsNullOrEmpty(targetPath) - && targetPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase) - && action != "create" // Allow prefab instantiation - ) - { - return new ErrorResponse( - $"Target '{targetPath}' is a prefab asset. " + - $"Use 'manage_asset' with action='modify' for prefab asset modifications, " + - $"or 'manage_prefabs' with action='open_stage' to edit the prefab in isolation mode." - ); - } - // --- End Prefab Asset Check --- - - try - { - switch (action) - { - // --- Primary lifecycle actions (kept in manage_gameobject) --- - case "create": - return GameObjectCreate.Handle(@params); - case "modify": - return GameObjectModify.Handle(@params, targetToken, searchMethod); - case "delete": - return GameObjectDelete.Handle(targetToken, searchMethod); - case "duplicate": - return GameObjectDuplicate.Handle(@params, targetToken, searchMethod); - case "move_relative": - return GameObjectMoveRelative.Handle(@params, targetToken, searchMethod); - - default: - return new ErrorResponse($"Unknown action: '{action}'."); - } - } - catch (Exception e) - { - McpLog.Error($"[ManageGameObject] Action '{action}' failed: {e}"); - return new ErrorResponse($"Internal error processing action '{action}': {e.Message}"); - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs.meta b/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs.meta deleted file mode 100644 index eaab062..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 7641d7388f0f6634b9d83d34de87b2ee -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs b/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs deleted file mode 100644 index 8d8a36e..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs +++ /dev/null @@ -1,238 +0,0 @@ -#nullable disable -using System; -using System.Collections.Generic; -using System.Linq; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Tools; -using Newtonsoft.Json.Linq; -using UnityEditor.SceneManagement; -using UnityEngine; -using UnityEngine.SceneManagement; - -namespace MCPForUnity.Editor.Tools.GameObjects -{ - internal static class ManageGameObjectCommon - { - internal static GameObject FindObjectInternal(JToken targetToken, string searchMethod, JObject findParams = null) - { - bool findAll = findParams?["findAll"]?.ToObject() ?? false; - - if ( - targetToken?.Type == JTokenType.Integer - || (searchMethod == "by_id" && int.TryParse(targetToken?.ToString(), out _)) - ) - { - findAll = false; - } - - List results = FindObjectsInternal(targetToken, searchMethod, findAll, findParams); - return results.Count > 0 ? results[0] : null; - } - - internal static List FindObjectsInternal( - JToken targetToken, - string searchMethod, - bool findAll, - JObject findParams = null - ) - { - List results = new List(); - string searchTerm = findParams?["searchTerm"]?.ToString() ?? targetToken?.ToString(); - bool searchInChildren = findParams?["searchInChildren"]?.ToObject() ?? false; - bool searchInactive = findParams?["searchInactive"]?.ToObject() ?? false; - - if (string.IsNullOrEmpty(searchMethod)) - { - if (targetToken?.Type == JTokenType.Integer) - searchMethod = "by_id"; - else if (!string.IsNullOrEmpty(searchTerm) && searchTerm.Contains('/')) - searchMethod = "by_path"; - else - searchMethod = "by_name"; - } - - GameObject rootSearchObject = null; - if (searchInChildren && targetToken != null) - { - rootSearchObject = FindObjectInternal(targetToken, "by_id_or_name_or_path"); - if (rootSearchObject == null) - { - McpLog.Warn($"[ManageGameObject.Find] Root object '{targetToken}' for child search not found."); - return results; - } - } - - switch (searchMethod) - { - case "by_id": - if (int.TryParse(searchTerm, out int instanceId)) - { - var allObjects = GetAllSceneObjects(searchInactive); - GameObject obj = allObjects.FirstOrDefault(go => go.GetInstanceID() == instanceId); - if (obj != null) - results.Add(obj); - } - break; - - case "by_name": - var searchPoolName = rootSearchObject - ? rootSearchObject - .GetComponentsInChildren(searchInactive) - .Select(t => t.gameObject) - : GetAllSceneObjects(searchInactive); - results.AddRange(searchPoolName.Where(go => go.name == searchTerm)); - break; - - case "by_path": - if (rootSearchObject != null) - { - Transform foundTransform = rootSearchObject.transform.Find(searchTerm); - if (foundTransform != null) - results.Add(foundTransform.gameObject); - } - else - { - var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); - if (prefabStage != null || searchInactive) - { - // In Prefab Stage, GameObject.Find() doesn't work, need to search manually - var allObjects = GetAllSceneObjects(searchInactive); - foreach (var go in allObjects) - { - if (GameObjectLookup.MatchesPath(go, searchTerm)) - { - results.Add(go); - } - } - } - else - { - var found = GameObject.Find(searchTerm); - if (found != null) - results.Add(found); - } - } - break; - - case "by_tag": - var searchPoolTag = rootSearchObject - ? rootSearchObject - .GetComponentsInChildren(searchInactive) - .Select(t => t.gameObject) - : GetAllSceneObjects(searchInactive); - results.AddRange(searchPoolTag.Where(go => go.CompareTag(searchTerm))); - break; - - case "by_layer": - var searchPoolLayer = rootSearchObject - ? rootSearchObject - .GetComponentsInChildren(searchInactive) - .Select(t => t.gameObject) - : GetAllSceneObjects(searchInactive); - if (int.TryParse(searchTerm, out int layerIndex)) - { - results.AddRange(searchPoolLayer.Where(go => go.layer == layerIndex)); - } - else - { - int namedLayer = LayerMask.NameToLayer(searchTerm); - if (namedLayer != -1) - results.AddRange(searchPoolLayer.Where(go => go.layer == namedLayer)); - } - break; - - case "by_component": - Type componentType = FindType(searchTerm); - if (componentType != null) - { - IEnumerable searchPoolComp; - if (rootSearchObject) - { - searchPoolComp = rootSearchObject - .GetComponentsInChildren(componentType, searchInactive) - .Select(c => (c as Component).gameObject); - } - else - { -#if UNITY_2023_1_OR_NEWER - var inactive = searchInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude; - searchPoolComp = UnityEngine.Object.FindObjectsByType(componentType, inactive, FindObjectsSortMode.None) - .Cast() - .Select(c => c.gameObject); -#else - searchPoolComp = UnityEngine.Object.FindObjectsOfType(componentType, searchInactive) - .Cast() - .Select(c => c.gameObject); -#endif - } - results.AddRange(searchPoolComp.Where(go => go != null)); - } - else - { - McpLog.Warn($"[ManageGameObject.Find] Component type not found: {searchTerm}"); - } - break; - - case "by_id_or_name_or_path": - if (int.TryParse(searchTerm, out int id)) - { - var allObjectsId = GetAllSceneObjects(true); - GameObject objById = allObjectsId.FirstOrDefault(go => go.GetInstanceID() == id); - if (objById != null) - { - results.Add(objById); - break; - } - } - - // Try path search - in Prefab Stage, GameObject.Find() doesn't work - var allObjectsForPath = GetAllSceneObjects(true); - GameObject objByPath = allObjectsForPath.FirstOrDefault(go => - { - return GameObjectLookup.MatchesPath(go, searchTerm); - }); - if (objByPath != null) - { - results.Add(objByPath); - break; - } - - var allObjectsName = GetAllSceneObjects(true); - results.AddRange(allObjectsName.Where(go => go.name == searchTerm)); - break; - - default: - McpLog.Warn($"[ManageGameObject.Find] Unknown search method: {searchMethod}"); - break; - } - - if (!findAll && results.Count > 1) - { - return new List { results[0] }; - } - - return results.Distinct().ToList(); - } - - private static IEnumerable GetAllSceneObjects(bool includeInactive) - { - // Delegate to GameObjectLookup to avoid code duplication and ensure consistent behavior - return GameObjectLookup.GetAllSceneObjects(includeInactive); - } - - private static Type FindType(string typeName) - { - if (ComponentResolver.TryResolve(typeName, out Type resolvedType, out string error)) - { - return resolvedType; - } - - if (!string.IsNullOrEmpty(error)) - { - McpLog.Warn($"[FindType] {error}"); - } - - return null; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs.meta b/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs.meta deleted file mode 100644 index fb5ab14..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 6bf0edf3cd2af46729294682cee3bee4 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/GetTestJob.cs b/Assets/MCPForUnity/Editor/Tools/GetTestJob.cs deleted file mode 100644 index 30c4fbf..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GetTestJob.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; -using Newtonsoft.Json.Linq; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Poll a previously started async test job by job_id. - /// - [McpForUnityTool("get_test_job", AutoRegister = false)] - public static class GetTestJob - { - public static object HandleCommand(JObject @params) - { - string jobId = @params?["job_id"]?.ToString() ?? @params?["jobId"]?.ToString(); - if (string.IsNullOrWhiteSpace(jobId)) - { - return new ErrorResponse("Missing required parameter 'job_id'."); - } - - bool includeDetails = ParamCoercion.CoerceBool(@params?["includeDetails"], false); - bool includeFailedTests = ParamCoercion.CoerceBool(@params?["includeFailedTests"], false); - - var job = TestJobManager.GetJob(jobId); - if (job == null) - { - return new ErrorResponse("Unknown job_id."); - } - - var payload = TestJobManager.ToSerializable(job, includeDetails, includeFailedTests); - return new SuccessResponse("Test job status retrieved.", payload); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/GetTestJob.cs.meta b/Assets/MCPForUnity/Editor/Tools/GetTestJob.cs.meta deleted file mode 100644 index acc50c2..0000000 --- a/Assets/MCPForUnity/Editor/Tools/GetTestJob.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 7f92c2b67a2c4b5c9d1a3c0e6f9b2d10 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/GetTestJob.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/JsonUtil.cs b/Assets/MCPForUnity/Editor/Tools/JsonUtil.cs deleted file mode 100644 index 74b745d..0000000 --- a/Assets/MCPForUnity/Editor/Tools/JsonUtil.cs +++ /dev/null @@ -1,31 +0,0 @@ -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools -{ - internal static class JsonUtil - { - /// - /// If @params[paramName] is a JSON string, parse it to a JObject in-place. - /// Logs a warning on parse failure and leaves the original value. - /// - internal static void CoerceJsonStringParameter(JObject @params, string paramName) - { - if (@params == null || string.IsNullOrEmpty(paramName)) return; - var token = @params[paramName]; - if (token != null && token.Type == JTokenType.String) - { - try - { - var parsed = JObject.Parse(token.ToString()); - @params[paramName] = parsed; - } - catch (Newtonsoft.Json.JsonReaderException e) - { - McpLog.Warn($"[MCP] Could not parse '{paramName}' JSON string: {e.Message}"); - } - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/JsonUtil.cs.meta b/Assets/MCPForUnity/Editor/Tools/JsonUtil.cs.meta deleted file mode 100644 index 145cda8..0000000 --- a/Assets/MCPForUnity/Editor/Tools/JsonUtil.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: d4b3b6009d53e4b8f97fe7ab57888c65 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/JsonUtil.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/ManageAsset.cs b/Assets/MCPForUnity/Editor/Tools/ManageAsset.cs deleted file mode 100644 index ed6e6c9..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageAsset.cs +++ /dev/null @@ -1,1120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; -using MCPForUnity.Editor.Helpers; // For Response class -using MCPForUnity.Editor.Tools; - -#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 MCPForUnity.Editor.Tools -{ - /// - /// Handles asset management operations within the Unity project. - /// - [McpForUnityTool("manage_asset", AutoRegister = false)] - public static class ManageAsset - { - // --- Main Handler --- - - // Define the list of valid actions - private static readonly List ValidActions = new List - { - "import", - "create", - "modify", - "delete", - "duplicate", - "move", - "rename", - "search", - "get_info", - "create_folder", - "get_components", - }; - - public static object HandleCommand(JObject @params) - { - string action = @params["action"]?.ToString()?.ToLowerInvariant(); - if (string.IsNullOrEmpty(action)) - { - return new ErrorResponse("Action parameter is required."); - } - - // Check if the action is valid before switching - if (!ValidActions.Contains(action)) - { - string validActionsList = string.Join(", ", ValidActions); - return new ErrorResponse( - $"Unknown action: '{action}'. Valid actions are: {validActionsList}" - ); - } - - // Common parameters - string path = @params["path"]?.ToString(); - - // Coerce string JSON to JObject for 'properties' if provided as a JSON string - var propertiesToken = @params["properties"]; - if (propertiesToken != null && propertiesToken.Type == JTokenType.String) - { - try - { - var parsed = JObject.Parse(propertiesToken.ToString()); - @params["properties"] = parsed; - } - catch (Exception e) - { - McpLog.Warn($"[ManageAsset] Could not parse 'properties' JSON string: {e.Message}"); - } - } - - try - { - switch (action) - { - case "import": - // Note: Unity typically auto-imports. This might re-import or configure import settings. - return ReimportAsset(path, @params["properties"] as JObject); - case "create": - return CreateAsset(@params); - case "modify": - var properties = @params["properties"] as JObject; - return ModifyAsset(path, properties); - case "delete": - return DeleteAsset(path); - case "duplicate": - return DuplicateAsset(path, @params["destination"]?.ToString()); - case "move": // Often same as rename if within Assets/ - case "rename": - return MoveOrRenameAsset(path, @params["destination"]?.ToString()); - case "search": - return SearchAssets(@params); - case "get_info": - return GetAssetInfo( - path, - @params["generatePreview"]?.ToObject() ?? false - ); - case "create_folder": // Added specific action for clarity - return CreateFolder(path); - case "get_components": - return GetComponentsFromAsset(path); - - default: - // This error message is less likely to be hit now, but kept here as a fallback or for potential future modifications. - string validActionsListDefault = string.Join(", ", ValidActions); - return new ErrorResponse( - $"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}" - ); - } - } - catch (Exception e) - { - McpLog.Error($"[ManageAsset] Action '{action}' failed for path '{path}': {e}"); - return new ErrorResponse( - $"Internal error processing action '{action}' on '{path}': {e.Message}" - ); - } - } - - // --- Action Implementations --- - - private static object ReimportAsset(string path, JObject properties) - { - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for reimport."); - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - if (!AssetExists(fullPath)) - return new ErrorResponse($"Asset not found at path: {fullPath}"); - - try - { - // TODO: Apply importer properties before reimporting? - // This is complex as it requires getting the AssetImporter, casting it, - // applying properties via reflection or specific methods, saving, then reimporting. - if (properties != null && properties.HasValues) - { - McpLog.Warn( - "[ManageAsset.Reimport] Modifying importer properties before reimport is not fully implemented yet." - ); - // AssetImporter importer = AssetImporter.GetAtPath(fullPath); - // if (importer != null) { /* Apply properties */ AssetDatabase.WriteImportSettingsIfDirty(fullPath); } - } - - AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); - // AssetDatabase.Refresh(); // Usually ImportAsset handles refresh - return new SuccessResponse($"Asset '{fullPath}' reimported.", GetAssetData(fullPath)); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to reimport asset '{fullPath}': {e.Message}"); - } - } - - private static object CreateAsset(JObject @params) - { - string path = @params["path"]?.ToString(); - string assetType = - @params["assetType"]?.ToString() - ?? @params["asset_type"]?.ToString(); // tolerate snake_case payloads from batched commands - JObject properties = @params["properties"] as JObject; - - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for create."); - if (string.IsNullOrEmpty(assetType)) - return new ErrorResponse("'assetType' is required for create."); - - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - string directory = Path.GetDirectoryName(fullPath); - - // Ensure directory exists - if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), directory))) - { - Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), directory)); - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Make sure Unity knows about the new folder - } - - if (AssetExists(fullPath)) - return new ErrorResponse($"Asset already exists at path: {fullPath}"); - - try - { - UnityEngine.Object newAsset = null; - string lowerAssetType = assetType.ToLowerInvariant(); - - // Handle common asset types - if (lowerAssetType == "folder") - { - return CreateFolder(path); // Use dedicated method - } - else if (lowerAssetType == "material") - { - var requested = properties?["shader"]?.ToString(); - Shader shader = RenderPipelineUtility.ResolveShader(requested); - if (shader == null) - return new ErrorResponse($"Could not find a project-compatible shader (requested: '{requested ?? "none"}'). Consider installing URP/HDRP or provide an explicit shader path."); - - var mat = new Material(shader); - if (properties != null) - { - JObject propertiesForApply = properties; - if (propertiesForApply["shader"] != null) - { - propertiesForApply = (JObject)properties.DeepClone(); - propertiesForApply.Remove("shader"); - } - - if (propertiesForApply.HasValues) - { - MaterialOps.ApplyProperties(mat, propertiesForApply, UnityJsonSerializer.Instance); - } - } - AssetDatabase.CreateAsset(mat, fullPath); - 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 == "prefab") - { - // Creating prefabs usually involves saving an existing GameObject hierarchy. - // A common pattern is to create an empty GameObject, configure it, and then save it. - return new ErrorResponse( - "Creating prefabs programmatically usually requires a source GameObject. Use manage_gameobject to create/configure, then save as prefab via a separate mechanism or future enhancement." - ); - // Example (conceptual): - // GameObject source = GameObject.Find(properties["sourceGameObject"].ToString()); - // if(source != null) PrefabUtility.SaveAsPrefabAsset(source, fullPath); - } - // TODO: Add more asset types (Animation Controller, Scene, etc.) - else - { - // Generic creation attempt (might fail or create empty files) - // For some types, just creating the file might be enough if Unity imports it. - // File.Create(Path.Combine(Directory.GetCurrentDirectory(), fullPath)).Close(); - // AssetDatabase.ImportAsset(fullPath); // Let Unity try to import it - // newAsset = AssetDatabase.LoadAssetAtPath(fullPath); - return new ErrorResponse( - $"Creation for asset type '{assetType}' is not explicitly supported yet. Supported: Folder, Material, PhysicsMaterial." - ); - } - - if ( - newAsset == null - && !Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), fullPath)) - ) // Check if it wasn't a folder and asset wasn't created - { - return new ErrorResponse( - $"Failed to create asset '{assetType}' at '{fullPath}'. See logs for details." - ); - } - - AssetDatabase.SaveAssets(); - // AssetDatabase.Refresh(); // CreateAsset often handles refresh - return new SuccessResponse( - $"Asset '{fullPath}' created successfully.", - GetAssetData(fullPath) - ); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to create asset at '{fullPath}': {e.Message}"); - } - } - - private static object CreateFolder(string path) - { - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for create_folder."); - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - string parentDir = Path.GetDirectoryName(fullPath); - string folderName = Path.GetFileName(fullPath); - - if (AssetExists(fullPath)) - { - // Check if it's actually a folder already - if (AssetDatabase.IsValidFolder(fullPath)) - { - return new SuccessResponse( - $"Folder already exists at path: {fullPath}", - GetAssetData(fullPath) - ); - } - else - { - return new ErrorResponse( - $"An asset (not a folder) already exists at path: {fullPath}" - ); - } - } - - try - { - // Ensure parent exists - if (!string.IsNullOrEmpty(parentDir) && !AssetDatabase.IsValidFolder(parentDir)) - { - // Recursively create parent folders if needed (AssetDatabase handles this internally) - // Or we can do it manually: Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), parentDir)); AssetDatabase.Refresh(); - } - - string guid = AssetDatabase.CreateFolder(parentDir, folderName); - if (string.IsNullOrEmpty(guid)) - { - return new ErrorResponse( - $"Failed to create folder '{fullPath}'. Check logs and permissions." - ); - } - - // AssetDatabase.Refresh(); // CreateFolder usually handles refresh - return new SuccessResponse( - $"Folder '{fullPath}' created successfully.", - GetAssetData(fullPath) - ); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to create folder '{fullPath}': {e.Message}"); - } - } - - private static object ModifyAsset(string path, JObject properties) - { - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for modify."); - if (properties == null || !properties.HasValues) - return new ErrorResponse("'properties' are required for modify."); - - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - if (!AssetExists(fullPath)) - return new ErrorResponse($"Asset not found at path: {fullPath}"); - - try - { - UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath( - fullPath - ); - if (asset == null) - return new ErrorResponse($"Failed to load asset at path: {fullPath}"); - - bool modified = false; // Flag to track if any changes were made - - // --- NEW: Handle GameObject / Prefab Component Modification --- - if (asset is GameObject gameObject) - { - // Iterate through the properties JSON: keys are component names, values are properties objects for that component - foreach (var prop in properties.Properties()) - { - string componentName = prop.Name; // e.g., "Collectible" - // Check if the value associated with the component name is actually an object containing properties - if ( - prop.Value is JObject componentProperties - && componentProperties.HasValues - ) // e.g., {"bobSpeed": 2.0} - { - // Resolve component type via ComponentResolver, then fetch by Type - Component targetComponent = null; - bool resolved = ComponentResolver.TryResolve(componentName, out var compType, out var compError); - if (resolved) - { - targetComponent = gameObject.GetComponent(compType); - } - - // Only warn about resolution failure if component also not found - if (targetComponent == null && !resolved) - { - McpLog.Warn( - $"[ManageAsset.ModifyAsset] Failed to resolve component '{componentName}' on '{gameObject.name}': {compError}" - ); - } - - if (targetComponent != null) - { - // Apply the nested properties (e.g., bobSpeed) to the found component instance - // Use |= to ensure 'modified' becomes true if any component is successfully modified - modified |= ApplyObjectProperties( - targetComponent, - componentProperties - ); - } - else - { - // Log a warning if a specified component couldn't be found - McpLog.Warn( - $"[ManageAsset.ModifyAsset] Component '{componentName}' not found on GameObject '{gameObject.name}' in asset '{fullPath}'. Skipping modification for this component." - ); - } - } - else - { - // Log a warning if the structure isn't {"ComponentName": {"prop": value}} - // We could potentially try to apply this property directly to the GameObject here if needed, - // but the primary goal is component modification. - McpLog.Warn( - $"[ManageAsset.ModifyAsset] Property '{prop.Name}' for GameObject modification should have a JSON object value containing component properties. Value was: {prop.Value.Type}. Skipping." - ); - } - } - // Note: 'modified' is now true if ANY component property was successfully changed. - } - // --- End NEW --- - - // --- Existing logic for other asset types (now as else-if) --- - // Example: Modifying a Material - else if (asset is Material material) - { - // Apply properties directly to the material. If this modifies, it sets modified=true. - // Use |= in case the asset was already marked modified by previous logic (though unlikely here) - modified |= MaterialOps.ApplyProperties(material, properties, UnityJsonSerializer.Instance); - } - // Example: Modifying a ScriptableObject (Use manage_scriptable_object instead!) - else if (asset is ScriptableObject so) - { - // Deprecated: Prefer manage_scriptable_object for robust patching. - // Kept for simple property setting fallback on existing assets if manage_scriptable_object isn't used. - modified |= ApplyObjectProperties(so, properties); - } - // Example: Modifying TextureImporter settings - else if (asset is Texture) - { - AssetImporter importer = AssetImporter.GetAtPath(fullPath); - if (importer is TextureImporter textureImporter) - { - bool importerModified = ApplyObjectProperties(textureImporter, properties); - if (importerModified) - { - // Importer settings need saving and reimporting - AssetDatabase.WriteImportSettingsIfDirty(fullPath); - AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); // Reimport to apply changes - modified = true; // Mark overall operation as modified - } - } - else - { - McpLog.Warn($"Could not get TextureImporter for {fullPath}."); - } - } - // TODO: Add modification logic for other common asset types (Models, AudioClips importers, etc.) - else // Fallback for other asset types OR direct properties on non-GameObject assets - { - // This block handles non-GameObject/Material/ScriptableObject/Texture assets. - // Attempts to apply properties directly to the asset itself. - McpLog.Warn( - $"[ManageAsset.ModifyAsset] Asset type '{asset.GetType().Name}' at '{fullPath}' is not explicitly handled for component modification. Attempting generic property setting on the asset itself." - ); - modified |= ApplyObjectProperties(asset, properties); - } - // --- End Existing Logic --- - - // Check if any modification happened (either component or direct asset modification) - if (modified) - { - // Mark the asset as dirty (important for prefabs/SOs) so Unity knows to save it. - EditorUtility.SetDirty(asset); - // Save all modified assets to disk. - AssetDatabase.SaveAssets(); - // Refresh might be needed in some edge cases, but SaveAssets usually covers it. - // AssetDatabase.Refresh(); - return new SuccessResponse( - $"Asset '{fullPath}' modified successfully.", - GetAssetData(fullPath) - ); - } - else - { - // If no changes were made (e.g., component not found, property names incorrect, value unchanged), return a success message indicating nothing changed. - return new SuccessResponse( - $"No applicable or modifiable properties found for asset '{fullPath}'. Check component names, property names, and values.", - GetAssetData(fullPath) - ); - // Previous message: return new SuccessResponse($"No applicable properties found to modify for asset '{fullPath}'.", GetAssetData(fullPath)); - } - } - catch (Exception e) - { - // Log the detailed error internally - McpLog.Error($"[ManageAsset] Action 'modify' failed for path '{path}': {e}"); - // Return a user-friendly error message - return new ErrorResponse($"Failed to modify asset '{fullPath}': {e.Message}"); - } - } - - private static object DeleteAsset(string path) - { - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for delete."); - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - if (!AssetExists(fullPath)) - return new ErrorResponse($"Asset not found at path: {fullPath}"); - - try - { - bool success = AssetDatabase.DeleteAsset(fullPath); - if (success) - { - // AssetDatabase.Refresh(); // DeleteAsset usually handles refresh - return new SuccessResponse($"Asset '{fullPath}' deleted successfully."); - } - else - { - // This might happen if the file couldn't be deleted (e.g., locked) - return new ErrorResponse( - $"Failed to delete asset '{fullPath}'. Check logs or if the file is locked." - ); - } - } - catch (Exception e) - { - return new ErrorResponse($"Error deleting asset '{fullPath}': {e.Message}"); - } - } - - private static object DuplicateAsset(string path, string destinationPath) - { - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for duplicate."); - - string sourcePath = AssetPathUtility.SanitizeAssetPath(path); - if (!AssetExists(sourcePath)) - return new ErrorResponse($"Source asset not found at path: {sourcePath}"); - - string destPath; - if (string.IsNullOrEmpty(destinationPath)) - { - // Generate a unique path if destination is not provided - destPath = AssetDatabase.GenerateUniqueAssetPath(sourcePath); - } - else - { - destPath = AssetPathUtility.SanitizeAssetPath(destinationPath); - if (AssetExists(destPath)) - return new ErrorResponse($"Asset already exists at destination path: {destPath}"); - // Ensure destination directory exists - EnsureDirectoryExists(Path.GetDirectoryName(destPath)); - } - - try - { - bool success = AssetDatabase.CopyAsset(sourcePath, destPath); - if (success) - { - // AssetDatabase.Refresh(); - return new SuccessResponse( - $"Asset '{sourcePath}' duplicated to '{destPath}'.", - GetAssetData(destPath) - ); - } - else - { - return new ErrorResponse( - $"Failed to duplicate asset from '{sourcePath}' to '{destPath}'." - ); - } - } - catch (Exception e) - { - return new ErrorResponse($"Error duplicating asset '{sourcePath}': {e.Message}"); - } - } - - private static object MoveOrRenameAsset(string path, string destinationPath) - { - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for move/rename."); - if (string.IsNullOrEmpty(destinationPath)) - return new ErrorResponse("'destination' path is required for move/rename."); - - string sourcePath = AssetPathUtility.SanitizeAssetPath(path); - string destPath = AssetPathUtility.SanitizeAssetPath(destinationPath); - - if (!AssetExists(sourcePath)) - return new ErrorResponse($"Source asset not found at path: {sourcePath}"); - if (AssetExists(destPath)) - return new ErrorResponse( - $"An asset already exists at the destination path: {destPath}" - ); - - // Ensure destination directory exists - EnsureDirectoryExists(Path.GetDirectoryName(destPath)); - - try - { - // Validate will return an error string if failed, null if successful - string error = AssetDatabase.ValidateMoveAsset(sourcePath, destPath); - if (!string.IsNullOrEmpty(error)) - { - return new ErrorResponse( - $"Failed to move/rename asset from '{sourcePath}' to '{destPath}': {error}" - ); - } - - string guid = AssetDatabase.MoveAsset(sourcePath, destPath); - if (!string.IsNullOrEmpty(guid)) // MoveAsset returns the new GUID on success - { - // AssetDatabase.Refresh(); // MoveAsset usually handles refresh - return new SuccessResponse( - $"Asset moved/renamed from '{sourcePath}' to '{destPath}'.", - GetAssetData(destPath) - ); - } - else - { - // This case might not be reachable if ValidateMoveAsset passes, but good to have - return new ErrorResponse( - $"MoveAsset call failed unexpectedly for '{sourcePath}' to '{destPath}'." - ); - } - } - catch (Exception e) - { - return new ErrorResponse($"Error moving/renaming asset '{sourcePath}': {e.Message}"); - } - } - - private static object SearchAssets(JObject @params) - { - string searchPattern = @params["searchPattern"]?.ToString(); - string filterType = @params["filterType"]?.ToString(); - string pathScope = @params["path"]?.ToString(); // Use path as folder scope - string filterDateAfterStr = @params["filterDateAfter"]?.ToString(); - int pageSize = @params["pageSize"]?.ToObject() ?? 50; // Default page size - int pageNumber = @params["pageNumber"]?.ToObject() ?? 1; // Default page number (1-based) - bool generatePreview = @params["generatePreview"]?.ToObject() ?? false; - - List searchFilters = new List(); - if (!string.IsNullOrEmpty(searchPattern)) - searchFilters.Add(searchPattern); - if (!string.IsNullOrEmpty(filterType)) - searchFilters.Add($"t:{filterType}"); - - string[] folderScope = null; - if (!string.IsNullOrEmpty(pathScope)) - { - folderScope = new string[] { AssetPathUtility.SanitizeAssetPath(pathScope) }; - if (!AssetDatabase.IsValidFolder(folderScope[0])) - { - // Maybe the user provided a file path instead of a folder? - // We could search in the containing folder, or return an error. - McpLog.Warn( - $"Search path '{folderScope[0]}' is not a valid folder. Searching entire project." - ); - folderScope = null; // Search everywhere if path isn't a folder - } - } - - DateTime? filterDateAfter = null; - if (!string.IsNullOrEmpty(filterDateAfterStr)) - { - if ( - DateTime.TryParse( - filterDateAfterStr, - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, - out DateTime parsedDate - ) - ) - { - filterDateAfter = parsedDate; - } - else - { - McpLog.Warn( - $"Could not parse filterDateAfter: '{filterDateAfterStr}'. Expected ISO 8601 format." - ); - } - } - - try - { - string[] guids = AssetDatabase.FindAssets( - string.Join(" ", searchFilters), - folderScope - ); - List results = new List(); - int totalFound = 0; - - foreach (string guid in guids) - { - string assetPath = AssetDatabase.GUIDToAssetPath(guid); - if (string.IsNullOrEmpty(assetPath)) - continue; - - // Apply date filter if present - if (filterDateAfter.HasValue) - { - DateTime lastWriteTime = File.GetLastWriteTimeUtc( - Path.Combine(Directory.GetCurrentDirectory(), assetPath) - ); - if (lastWriteTime <= filterDateAfter.Value) - { - continue; // Skip assets older than or equal to the filter date - } - } - - totalFound++; // Count matching assets before pagination - results.Add(GetAssetData(assetPath, generatePreview)); - } - - // Apply pagination - int startIndex = (pageNumber - 1) * pageSize; - var pagedResults = results.Skip(startIndex).Take(pageSize).ToList(); - - return new SuccessResponse( - $"Found {totalFound} asset(s). Returning page {pageNumber} ({pagedResults.Count} assets).", - new - { - totalAssets = totalFound, - pageSize = pageSize, - pageNumber = pageNumber, - assets = pagedResults, - } - ); - } - catch (Exception e) - { - return new ErrorResponse($"Error searching assets: {e.Message}"); - } - } - - private static object GetAssetInfo(string path, bool generatePreview) - { - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for get_info."); - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - if (!AssetExists(fullPath)) - return new ErrorResponse($"Asset not found at path: {fullPath}"); - - try - { - return new SuccessResponse( - "Asset info retrieved.", - GetAssetData(fullPath, generatePreview) - ); - } - catch (Exception e) - { - return new ErrorResponse($"Error getting info for asset '{fullPath}': {e.Message}"); - } - } - - /// - /// Retrieves components attached to a GameObject asset (like a Prefab). - /// - /// The asset path of the GameObject or Prefab. - /// A response object containing a list of component type names or an error. - private static object GetComponentsFromAsset(string path) - { - // 1. Validate input path - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for get_components."); - - // 2. Sanitize and check existence - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - if (!AssetExists(fullPath)) - return new ErrorResponse($"Asset not found at path: {fullPath}"); - - try - { - // 3. Load the asset - UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath( - fullPath - ); - if (asset == null) - return new ErrorResponse($"Failed to load asset at path: {fullPath}"); - - // 4. Check if it's a GameObject (Prefabs load as GameObjects) - GameObject gameObject = asset as GameObject; - if (gameObject == null) - { - // Also check if it's *directly* a Component type (less common for primary assets) - Component componentAsset = asset as Component; - if (componentAsset != null) - { - // If the asset itself *is* a component, maybe return just its info? - // This is an edge case. Let's stick to GameObjects for now. - return new ErrorResponse( - $"Asset at '{fullPath}' is a Component ({asset.GetType().FullName}), not a GameObject. Components are typically retrieved *from* a GameObject." - ); - } - return new ErrorResponse( - $"Asset at '{fullPath}' is not a GameObject (Type: {asset.GetType().FullName}). Cannot get components from this asset type." - ); - } - - // 5. Get components - Component[] components = gameObject.GetComponents(); - - // 6. Format component data - List componentList = components - .Select(comp => new - { - typeName = comp.GetType().FullName, - instanceID = comp.GetInstanceID(), - // TODO: Add more component-specific details here if needed in the future? - // Requires reflection or specific handling per component type. - }) - .ToList(); // Explicit cast for clarity if needed - - // 7. Return success response - return new SuccessResponse( - $"Found {componentList.Count} component(s) on asset '{fullPath}'.", - componentList - ); - } - catch (Exception e) - { - McpLog.Error( - $"[ManageAsset.GetComponentsFromAsset] Error getting components for '{fullPath}': {e}" - ); - return new ErrorResponse( - $"Error getting components for asset '{fullPath}': {e.Message}" - ); - } - } - - // --- Internal Helpers --- - - /// - /// Ensures the asset path starts with "Assets/". - /// - /// - /// Checks if an asset exists at the given path (file or folder). - /// - private static bool AssetExists(string sanitizedPath) - { - // AssetDatabase APIs are generally preferred over raw File/Directory checks for assets. - // Check if it's a known asset GUID. - if (!string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath))) - { - return true; - } - // AssetPathToGUID might not work for newly created folders not yet refreshed. - // Check directory explicitly for folders. - if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))) - { - // Check if it's considered a *valid* folder by Unity - return AssetDatabase.IsValidFolder(sanitizedPath); - } - // Check file existence for non-folder assets. - if (File.Exists(Path.Combine(Directory.GetCurrentDirectory(), sanitizedPath))) - { - return true; // Assume if file exists, it's an asset or will be imported - } - - return false; - // Alternative: return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(sanitizedPath)); - } - - /// - /// Ensures the directory for a given asset path exists, creating it if necessary. - /// - private static void EnsureDirectoryExists(string directoryPath) - { - if (string.IsNullOrEmpty(directoryPath)) - return; - string fullDirPath = Path.Combine(Directory.GetCurrentDirectory(), directoryPath); - if (!Directory.Exists(fullDirPath)) - { - Directory.CreateDirectory(fullDirPath); - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Let Unity know about the new folder - } - } - - - - /// - /// Applies properties from JObject to a PhysicsMaterial. - /// - 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(); - pmat.dynamicFriction = dynamicFriction; - modified = true; - } - - // Example: Set static friction - if (properties["staticFriction"]?.Type == JTokenType.Float) - { - float staticFriction = properties["staticFriction"].ToObject(); - pmat.staticFriction = staticFriction; - modified = true; - } - - // Example: Set bounciness - if (properties["bounciness"]?.Type == JTokenType.Float) - { - float bounciness = properties["bounciness"].ToObject(); - pmat.bounciness = bounciness; - modified = true; - } - - List averageList = new List { "ave", "Ave", "average", "Average" }; - List multiplyList = new List { "mul", "Mul", "mult", "Mult", "multiply", "Multiply" }; - List minimumList = new List { "min", "Min", "minimum", "Minimum" }; - List maximumList = new List { "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; - } - - /// - /// Generic helper to set properties on any UnityEngine.Object using reflection. - /// - private static bool ApplyObjectProperties(UnityEngine.Object target, JObject properties) - { - if (target == null || properties == null) - return false; - bool modified = false; - Type type = target.GetType(); - - foreach (var prop in properties.Properties()) - { - string propName = prop.Name; - JToken propValue = prop.Value; - if (SetPropertyOrField(target, propName, propValue, type)) - { - modified = true; - } - } - return modified; - } - - /// - /// Helper to set a property or field via reflection, handling basic types and Unity objects. - /// - private static bool SetPropertyOrField( - object target, - string memberName, - JToken value, - Type type = null - ) - { - type = type ?? target.GetType(); - System.Reflection.BindingFlags flags = - System.Reflection.BindingFlags.Public - | System.Reflection.BindingFlags.Instance - | System.Reflection.BindingFlags.IgnoreCase; - - try - { - System.Reflection.PropertyInfo propInfo = type.GetProperty(memberName, flags); - if (propInfo != null && propInfo.CanWrite) - { - object convertedValue = Helpers.PropertyConversion.TryConvertToType(value, propInfo.PropertyType); - if ( - convertedValue != null - && !object.Equals(propInfo.GetValue(target), convertedValue) - ) - { - propInfo.SetValue(target, convertedValue); - return true; - } - } - else - { - System.Reflection.FieldInfo fieldInfo = type.GetField(memberName, flags); - if (fieldInfo != null) - { - object convertedValue = Helpers.PropertyConversion.TryConvertToType(value, fieldInfo.FieldType); - if ( - convertedValue != null - && !object.Equals(fieldInfo.GetValue(target), convertedValue) - ) - { - fieldInfo.SetValue(target, convertedValue); - return true; - } - } - } - } - catch (Exception ex) - { - McpLog.Warn( - $"[SetPropertyOrField] Failed to set '{memberName}' on {type.Name}: {ex.Message}" - ); - } - return false; - } - - // --- Data Serialization --- - - /// - /// Creates a serializable representation of an asset. - /// - private static object GetAssetData(string path, bool generatePreview = false) - { - if (string.IsNullOrEmpty(path) || !AssetExists(path)) - return null; - - string guid = AssetDatabase.AssetPathToGUID(path); - Type assetType = AssetDatabase.GetMainAssetTypeAtPath(path); - UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath(path); - string previewBase64 = null; - int previewWidth = 0; - int previewHeight = 0; - - if (generatePreview && asset != null) - { - Texture2D preview = AssetPreview.GetAssetPreview(asset); - - if (preview != null) - { - try - { - // Ensure texture is readable for EncodeToPNG - // Creating a temporary readable copy is safer - RenderTexture rt = null; - Texture2D readablePreview = null; - RenderTexture previous = RenderTexture.active; - try - { - rt = RenderTexture.GetTemporary(preview.width, preview.height); - Graphics.Blit(preview, rt); - RenderTexture.active = rt; - readablePreview = new Texture2D(preview.width, preview.height, TextureFormat.RGB24, false); - readablePreview.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); - readablePreview.Apply(); - - var pngData = readablePreview.EncodeToPNG(); - if (pngData != null && pngData.Length > 0) - { - previewBase64 = Convert.ToBase64String(pngData); - previewWidth = readablePreview.width; - previewHeight = readablePreview.height; - } - } - finally - { - RenderTexture.active = previous; - if (rt != null) RenderTexture.ReleaseTemporary(rt); - if (readablePreview != null) UnityEngine.Object.DestroyImmediate(readablePreview); - } - } - catch (Exception ex) - { - McpLog.Warn( - $"Failed to generate readable preview for '{path}': {ex.Message}. Preview might not be readable." - ); - // Fallback: Try getting static preview if available? - // Texture2D staticPreview = AssetPreview.GetMiniThumbnail(asset); - } - } - else - { - McpLog.Warn( - $"Could not get asset preview for {path} (Type: {assetType?.Name}). Is it supported?" - ); - } - } - - return new - { - path = path, - guid = guid, - assetType = assetType?.FullName ?? "Unknown", - name = Path.GetFileNameWithoutExtension(path), - fileName = Path.GetFileName(path), - isFolder = AssetDatabase.IsValidFolder(path), - instanceID = asset?.GetInstanceID() ?? 0, - lastWriteTimeUtc = File.GetLastWriteTimeUtc( - Path.Combine(Directory.GetCurrentDirectory(), path) - ) - .ToString("o"), // ISO 8601 - // --- Preview Data --- - previewBase64 = previewBase64, // PNG data as Base64 string - previewWidth = previewWidth, - previewHeight = previewHeight, - // TODO: Add more metadata? Importer settings? Dependencies? - }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/ManageAsset.cs.meta b/Assets/MCPForUnity/Editor/Tools/ManageAsset.cs.meta deleted file mode 100644 index c6cc94f..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageAsset.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: de90a1d9743a2874cb235cf0b83444b1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/ManageAsset.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/ManageComponents.cs b/Assets/MCPForUnity/Editor/Tools/ManageComponents.cs deleted file mode 100644 index 596e5b8..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageComponents.cs +++ /dev/null @@ -1,351 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Tool for managing components on GameObjects. - /// Actions: add, remove, set_property - /// - /// This is a focused tool for component lifecycle operations. - /// For reading component data, use the unity://scene/gameobject/{id}/components resource. - /// - [McpForUnityTool("manage_components")] - public static class ManageComponents - { - /// - /// Handles the manage_components command. - /// - /// Command parameters - /// Result of the component operation - public static object HandleCommand(JObject @params) - { - if (@params == null) - { - return new ErrorResponse("Parameters cannot be null."); - } - - string action = ParamCoercion.CoerceString(@params["action"], null)?.ToLowerInvariant(); - if (string.IsNullOrEmpty(action)) - { - return new ErrorResponse("'action' parameter is required (add, remove, set_property)."); - } - - // Target resolution - JToken targetToken = @params["target"]; - string searchMethod = ParamCoercion.CoerceString(@params["searchMethod"] ?? @params["search_method"], null); - - if (targetToken == null) - { - return new ErrorResponse("'target' parameter is required."); - } - - try - { - return action switch - { - "add" => AddComponent(@params, targetToken, searchMethod), - "remove" => RemoveComponent(@params, targetToken, searchMethod), - "set_property" => SetProperty(@params, targetToken, searchMethod), - _ => new ErrorResponse($"Unknown action: '{action}'. Supported actions: add, remove, set_property") - }; - } - catch (Exception e) - { - McpLog.Error($"[ManageComponents] Action '{action}' failed: {e}"); - return new ErrorResponse($"Internal error processing action '{action}': {e.Message}"); - } - } - - #region Action Implementations - - private static object AddComponent(JObject @params, JToken targetToken, string searchMethod) - { - GameObject targetGo = FindTarget(targetToken, searchMethod); - if (targetGo == null) - { - return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'."); - } - - string componentTypeName = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null); - if (string.IsNullOrEmpty(componentTypeName)) - { - return new ErrorResponse("'componentType' parameter is required for 'add' action."); - } - - // Resolve component type using unified type resolver - Type type = UnityTypeResolver.ResolveComponent(componentTypeName); - if (type == null) - { - return new ErrorResponse($"Component type '{componentTypeName}' not found. Use a fully-qualified name if needed."); - } - - // Use ComponentOps for the actual operation - Component newComponent = ComponentOps.AddComponent(targetGo, type, out string error); - if (newComponent == null) - { - return new ErrorResponse(error ?? $"Failed to add component '{componentTypeName}'."); - } - - // Set properties if provided - JObject properties = @params["properties"] as JObject ?? @params["componentProperties"] as JObject; - if (properties != null && properties.HasValues) - { - // Record for undo before modifying properties - Undo.RecordObject(newComponent, "Modify Component Properties"); - SetPropertiesOnComponent(newComponent, properties); - } - - EditorUtility.SetDirty(targetGo); - MarkOwningSceneDirty(targetGo); - - return new - { - success = true, - message = $"Component '{componentTypeName}' added to '{targetGo.name}'.", - data = new - { - instanceID = targetGo.GetInstanceID(), - componentType = type.FullName, - componentInstanceID = newComponent.GetInstanceID() - } - }; - } - - private static object RemoveComponent(JObject @params, JToken targetToken, string searchMethod) - { - GameObject targetGo = FindTarget(targetToken, searchMethod); - if (targetGo == null) - { - return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'."); - } - - string componentTypeName = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null); - if (string.IsNullOrEmpty(componentTypeName)) - { - return new ErrorResponse("'componentType' parameter is required for 'remove' action."); - } - - // Resolve component type using unified type resolver - Type type = UnityTypeResolver.ResolveComponent(componentTypeName); - if (type == null) - { - return new ErrorResponse($"Component type '{componentTypeName}' not found."); - } - - // Use ComponentOps for the actual operation - bool removed = ComponentOps.RemoveComponent(targetGo, type, out string error); - if (!removed) - { - return new ErrorResponse(error ?? $"Failed to remove component '{componentTypeName}'."); - } - - EditorUtility.SetDirty(targetGo); - MarkOwningSceneDirty(targetGo); - - return new - { - success = true, - message = $"Component '{componentTypeName}' removed from '{targetGo.name}'.", - data = new - { - instanceID = targetGo.GetInstanceID() - } - }; - } - - private static object SetProperty(JObject @params, JToken targetToken, string searchMethod) - { - GameObject targetGo = FindTarget(targetToken, searchMethod); - if (targetGo == null) - { - return new ErrorResponse($"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'."); - } - - string componentType = ParamCoercion.CoerceString(@params["componentType"] ?? @params["component_type"], null); - if (string.IsNullOrEmpty(componentType)) - { - return new ErrorResponse("'componentType' parameter is required for 'set_property' action."); - } - - // Resolve component type using unified type resolver - Type type = UnityTypeResolver.ResolveComponent(componentType); - if (type == null) - { - return new ErrorResponse($"Component type '{componentType}' not found."); - } - - Component component = targetGo.GetComponent(type); - if (component == null) - { - return new ErrorResponse($"Component '{componentType}' not found on '{targetGo.name}'."); - } - - // Get property and value - string propertyName = ParamCoercion.CoerceString(@params["property"], null); - JToken valueToken = @params["value"]; - - // Support both single property or properties object - JObject properties = @params["properties"] as JObject; - - if (string.IsNullOrEmpty(propertyName) && (properties == null || !properties.HasValues)) - { - return new ErrorResponse("Either 'property'+'value' or 'properties' object is required for 'set_property' action."); - } - - var errors = new List(); - - try - { - Undo.RecordObject(component, $"Set property on {componentType}"); - - if (!string.IsNullOrEmpty(propertyName) && valueToken != null) - { - // Single property mode - var error = TrySetProperty(component, propertyName, valueToken); - if (error != null) - { - errors.Add(error); - } - } - - if (properties != null && properties.HasValues) - { - // Multiple properties mode - foreach (var prop in properties.Properties()) - { - var error = TrySetProperty(component, prop.Name, prop.Value); - if (error != null) - { - errors.Add(error); - } - } - } - - EditorUtility.SetDirty(component); - MarkOwningSceneDirty(targetGo); - - if (errors.Count > 0) - { - return new - { - success = false, - message = $"Some properties failed to set on '{componentType}'.", - data = new - { - instanceID = targetGo.GetInstanceID(), - errors = errors - } - }; - } - - return new - { - success = true, - message = $"Properties set on component '{componentType}' on '{targetGo.name}'.", - data = new - { - instanceID = targetGo.GetInstanceID() - } - }; - } - catch (Exception e) - { - return new ErrorResponse($"Error setting properties on component '{componentType}': {e.Message}"); - } - } - - #endregion - - #region Helpers - - /// - /// Marks the appropriate scene as dirty for the given GameObject. - /// Handles both regular scenes and prefab stages. - /// - private static void MarkOwningSceneDirty(GameObject targetGo) - { - var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); - if (prefabStage != null) - { - EditorSceneManager.MarkSceneDirty(prefabStage.scene); - } - else - { - EditorSceneManager.MarkSceneDirty(targetGo.scene); - } - } - - private static GameObject FindTarget(JToken targetToken, string searchMethod) - { - if (targetToken == null) - return null; - - // Try instance ID first - if (targetToken.Type == JTokenType.Integer) - { - int instanceId = targetToken.Value(); - return GameObjectLookup.FindById(instanceId); - } - - string targetStr = targetToken.ToString(); - - // Try parsing as instance ID - if (int.TryParse(targetStr, out int parsedId)) - { - var byId = GameObjectLookup.FindById(parsedId); - if (byId != null) - return byId; - } - - // Use GameObjectLookup for search - return GameObjectLookup.FindByTarget(targetToken, searchMethod ?? "by_name", true); - } - - private static void SetPropertiesOnComponent(Component component, JObject properties) - { - if (component == null || properties == null) - return; - - var errors = new List(); - foreach (var prop in properties.Properties()) - { - var error = TrySetProperty(component, prop.Name, prop.Value); - if (error != null) - errors.Add(error); - } - - if (errors.Count > 0) - { - McpLog.Warn($"[ManageComponents] Some properties failed to set on {component.GetType().Name}: {string.Join(", ", errors)}"); - } - } - - /// - /// Attempts to set a property or field on a component. - /// Delegates to ComponentOps.SetProperty for unified implementation. - /// - private static string TrySetProperty(Component component, string propertyName, JToken value) - { - if (component == null || string.IsNullOrEmpty(propertyName)) - return "Invalid component or property name"; - - if (ComponentOps.SetProperty(component, propertyName, value, out string error)) - { - return null; // Success - } - - McpLog.Warn($"[ManageComponents] {error}"); - return error; - } - - #endregion - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/ManageComponents.cs.meta b/Assets/MCPForUnity/Editor/Tools/ManageComponents.cs.meta deleted file mode 100644 index d0a14fc..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageComponents.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: c6f476359563842c79eda2c180566c98 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/ManageComponents.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/ManageEditor.cs b/Assets/MCPForUnity/Editor/Tools/ManageEditor.cs deleted file mode 100644 index 2704803..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageEditor.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditorInternal; // Required for tag management - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Handles editor control actions including play mode control, tool selection, - /// and tag/layer management. For reading editor state, use MCP resources instead. - /// - [McpForUnityTool("manage_editor", AutoRegister = false)] - public static class ManageEditor - { - // Constant for starting user layer index - private const int FirstUserLayerIndex = 8; - - // Constant for total layer count - private const int TotalLayerCount = 32; - - /// - /// Main handler for editor management actions. - /// - public static object HandleCommand(JObject @params) - { - // Step 1: Null parameter guard (consistent across all tools) - if (@params == null) - { - return new ErrorResponse("Parameters cannot be null."); - } - - // Step 2: Wrap parameters - var p = new ToolParams(@params); - - // Step 3: Extract and validate required parameters - var actionResult = p.GetRequired("action"); - if (!actionResult.IsSuccess) - { - return new ErrorResponse(actionResult.ErrorMessage); - } - string action = actionResult.Value.ToLowerInvariant(); - - // Parameters for specific actions - string tagName = p.Get("tagName"); - string layerName = p.Get("layerName"); - bool waitForCompletion = p.GetBool("waitForCompletion", false); - - // Route action - switch (action) - { - // Play Mode Control - case "play": - try - { - if (!EditorApplication.isPlaying) - { - EditorApplication.isPlaying = true; - return new SuccessResponse("Entered play mode."); - } - return new SuccessResponse("Already in play mode."); - } - catch (Exception e) - { - return new ErrorResponse($"Error entering play mode: {e.Message}"); - } - case "pause": - try - { - if (EditorApplication.isPlaying) - { - EditorApplication.isPaused = !EditorApplication.isPaused; - return new SuccessResponse( - EditorApplication.isPaused ? "Game paused." : "Game resumed." - ); - } - return new ErrorResponse("Cannot pause/resume: Not in play mode."); - } - catch (Exception e) - { - return new ErrorResponse($"Error pausing/resuming game: {e.Message}"); - } - case "stop": - try - { - if (EditorApplication.isPlaying) - { - EditorApplication.isPlaying = false; - return new SuccessResponse("Exited play mode."); - } - return new SuccessResponse("Already stopped (not in play mode)."); - } - catch (Exception e) - { - return new ErrorResponse($"Error stopping play mode: {e.Message}"); - } - - // Tool Control - case "set_active_tool": - var toolNameResult = p.GetRequired("toolName", "'toolName' parameter required for set_active_tool."); - if (!toolNameResult.IsSuccess) - return new ErrorResponse(toolNameResult.ErrorMessage); - return SetActiveTool(toolNameResult.Value); - - // Tag Management - case "add_tag": - var addTagResult = p.GetRequired("tagName", "'tagName' parameter required for add_tag."); - if (!addTagResult.IsSuccess) - return new ErrorResponse(addTagResult.ErrorMessage); - return AddTag(addTagResult.Value); - case "remove_tag": - var removeTagResult = p.GetRequired("tagName", "'tagName' parameter required for remove_tag."); - if (!removeTagResult.IsSuccess) - return new ErrorResponse(removeTagResult.ErrorMessage); - return RemoveTag(removeTagResult.Value); - // Layer Management - case "add_layer": - var addLayerResult = p.GetRequired("layerName", "'layerName' parameter required for add_layer."); - if (!addLayerResult.IsSuccess) - return new ErrorResponse(addLayerResult.ErrorMessage); - return AddLayer(addLayerResult.Value); - case "remove_layer": - var removeLayerResult = p.GetRequired("layerName", "'layerName' parameter required for remove_layer."); - if (!removeLayerResult.IsSuccess) - return new ErrorResponse(removeLayerResult.ErrorMessage); - return RemoveLayer(removeLayerResult.Value); - // --- Settings (Example) --- - // case "set_resolution": - // int? width = @params["width"]?.ToObject(); - // int? height = @params["height"]?.ToObject(); - // if (!width.HasValue || !height.HasValue) return new ErrorResponse("'width' and 'height' parameters required."); - // return SetGameViewResolution(width.Value, height.Value); - // case "set_quality": - // // Handle string name or int index - // return SetQualityLevel(@params["qualityLevel"]); - - default: - return new ErrorResponse( - $"Unknown action: '{action}'. Supported actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer. Use MCP resources for reading editor state, project info, tags, layers, selection, windows, prefab stage, and active tool." - ); - } - } - - // --- Tool Control Methods --- - - private static object SetActiveTool(string toolName) - { - try - { - Tool targetTool; - if (Enum.TryParse(toolName, true, out targetTool)) // Case-insensitive parse - { - // Check if it's a valid built-in tool - if (targetTool != Tool.None && targetTool <= Tool.Custom) // Tool.Custom is the last standard tool - { - UnityEditor.Tools.current = targetTool; - return new SuccessResponse($"Set active tool to '{targetTool}'."); - } - else - { - return new ErrorResponse( - $"Cannot directly set tool to '{toolName}'. It might be None, Custom, or invalid." - ); - } - } - else - { - // Potentially try activating a custom tool by name here if needed - // This often requires specific editor scripting knowledge for that tool. - return new ErrorResponse( - $"Could not parse '{toolName}' as a standard Unity Tool (View, Move, Rotate, Scale, Rect, Transform, Custom)." - ); - } - } - catch (Exception e) - { - return new ErrorResponse($"Error setting active tool: {e.Message}"); - } - } - - // --- Tag Management Methods --- - - private static object AddTag(string tagName) - { - if (string.IsNullOrWhiteSpace(tagName)) - return new ErrorResponse("Tag name cannot be empty or whitespace."); - - // Check if tag already exists - if (System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tagName)) - { - return new ErrorResponse($"Tag '{tagName}' already exists."); - } - - try - { - // Add the tag using the internal utility - InternalEditorUtility.AddTag(tagName); - // Force save assets to ensure the change persists in the TagManager asset - AssetDatabase.SaveAssets(); - return new SuccessResponse($"Tag '{tagName}' added successfully."); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to add tag '{tagName}': {e.Message}"); - } - } - - private static object RemoveTag(string tagName) - { - if (string.IsNullOrWhiteSpace(tagName)) - return new ErrorResponse("Tag name cannot be empty or whitespace."); - if (tagName.Equals("Untagged", StringComparison.OrdinalIgnoreCase)) - return new ErrorResponse("Cannot remove the built-in 'Untagged' tag."); - - // Check if tag exists before attempting removal - if (!System.Linq.Enumerable.Contains(InternalEditorUtility.tags, tagName)) - { - return new ErrorResponse($"Tag '{tagName}' does not exist."); - } - - try - { - // Remove the tag using the internal utility - InternalEditorUtility.RemoveTag(tagName); - // Force save assets - AssetDatabase.SaveAssets(); - return new SuccessResponse($"Tag '{tagName}' removed successfully."); - } - catch (Exception e) - { - // Catch potential issues if the tag is somehow in use or removal fails - return new ErrorResponse($"Failed to remove tag '{tagName}': {e.Message}"); - } - } - - // --- Layer Management Methods --- - - private static object AddLayer(string layerName) - { - if (string.IsNullOrWhiteSpace(layerName)) - return new ErrorResponse("Layer name cannot be empty or whitespace."); - - // Access the TagManager asset - SerializedObject tagManager = GetTagManager(); - if (tagManager == null) - return new ErrorResponse("Could not access TagManager asset."); - - SerializedProperty layersProp = tagManager.FindProperty("layers"); - if (layersProp == null || !layersProp.isArray) - return new ErrorResponse("Could not find 'layers' property in TagManager."); - - // Check if layer name already exists (case-insensitive check recommended) - for (int i = 0; i < TotalLayerCount; i++) - { - SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); - if ( - layerSP != null - && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase) - ) - { - return new ErrorResponse($"Layer '{layerName}' already exists at index {i}."); - } - } - - // Find the first empty user layer slot (indices 8 to 31) - int firstEmptyUserLayer = -1; - for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) - { - SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); - if (layerSP != null && string.IsNullOrEmpty(layerSP.stringValue)) - { - firstEmptyUserLayer = i; - break; - } - } - - if (firstEmptyUserLayer == -1) - { - return new ErrorResponse("No empty User Layer slots available (8-31 are full)."); - } - - // Assign the name to the found slot - try - { - SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex( - firstEmptyUserLayer - ); - targetLayerSP.stringValue = layerName; - // Apply the changes to the TagManager asset - tagManager.ApplyModifiedProperties(); - // Save assets to make sure it's written to disk - AssetDatabase.SaveAssets(); - return new SuccessResponse( - $"Layer '{layerName}' added successfully to slot {firstEmptyUserLayer}." - ); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to add layer '{layerName}': {e.Message}"); - } - } - - private static object RemoveLayer(string layerName) - { - if (string.IsNullOrWhiteSpace(layerName)) - return new ErrorResponse("Layer name cannot be empty or whitespace."); - - // Access the TagManager asset - SerializedObject tagManager = GetTagManager(); - if (tagManager == null) - return new ErrorResponse("Could not access TagManager asset."); - - SerializedProperty layersProp = tagManager.FindProperty("layers"); - if (layersProp == null || !layersProp.isArray) - return new ErrorResponse("Could not find 'layers' property in TagManager."); - - // Find the layer by name (must be user layer) - int layerIndexToRemove = -1; - for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) // Start from user layers - { - SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i); - // Case-insensitive comparison is safer - if ( - layerSP != null - && layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase) - ) - { - layerIndexToRemove = i; - break; - } - } - - if (layerIndexToRemove == -1) - { - return new ErrorResponse($"User layer '{layerName}' not found."); - } - - // Clear the name for that index - try - { - SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex( - layerIndexToRemove - ); - targetLayerSP.stringValue = string.Empty; // Set to empty string to remove - // Apply the changes - tagManager.ApplyModifiedProperties(); - // Save assets - AssetDatabase.SaveAssets(); - return new SuccessResponse( - $"Layer '{layerName}' (slot {layerIndexToRemove}) removed successfully." - ); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to remove layer '{layerName}': {e.Message}"); - } - } - - // --- Helper Methods --- - - /// - /// Gets the SerializedObject for the TagManager asset. - /// - private static SerializedObject GetTagManager() - { - try - { - // Load the TagManager asset from the ProjectSettings folder - UnityEngine.Object[] tagManagerAssets = AssetDatabase.LoadAllAssetsAtPath( - "ProjectSettings/TagManager.asset" - ); - if (tagManagerAssets == null || tagManagerAssets.Length == 0) - { - McpLog.Error("[ManageEditor] TagManager.asset not found in ProjectSettings."); - return null; - } - // The first object in the asset file should be the TagManager - return new SerializedObject(tagManagerAssets[0]); - } - catch (Exception e) - { - McpLog.Error($"[ManageEditor] Error accessing TagManager.asset: {e.Message}"); - return null; - } - } - - // --- Example Implementations for Settings --- - /* - private static object SetGameViewResolution(int width, int height) { ... } - private static object SetQualityLevel(JToken qualityLevelToken) { ... } - */ - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/ManageEditor.cs.meta b/Assets/MCPForUnity/Editor/Tools/ManageEditor.cs.meta deleted file mode 100644 index e60635d..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageEditor.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 43ac60aa36b361b4dbe4a038ae9f35c8 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/ManageEditor.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/ManageMaterial.cs b/Assets/MCPForUnity/Editor/Tools/ManageMaterial.cs deleted file mode 100644 index e6d7cd2..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageMaterial.cs +++ /dev/null @@ -1,596 +0,0 @@ -using System; -using System.Collections.Generic; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools -{ - [McpForUnityTool("manage_material", AutoRegister = false)] - public static class ManageMaterial - { - public static object HandleCommand(JObject @params) - { - string action = @params["action"]?.ToString()?.ToLowerInvariant(); - if (string.IsNullOrEmpty(action)) - { - return new ErrorResponse("Action is required"); - } - - try - { - switch (action) - { - case "ping": - return new SuccessResponse("pong", new { tool = "manage_material" }); - - case "create": - return CreateMaterial(@params); - - case "set_material_shader_property": - return SetMaterialShaderProperty(@params); - - case "set_material_color": - return SetMaterialColor(@params); - - case "assign_material_to_renderer": - return AssignMaterialToRenderer(@params); - - case "set_renderer_color": - return SetRendererColor(@params); - - case "get_material_info": - return GetMaterialInfo(@params); - - default: - return new ErrorResponse($"Unknown action: {action}"); - } - } - catch (Exception ex) - { - return new ErrorResponse(ex.Message, new { stackTrace = ex.StackTrace }); - } - } - - private static string NormalizePath(string path) - { - if (string.IsNullOrEmpty(path)) return path; - - // Normalize separators and ensure Assets/ root - path = AssetPathUtility.SanitizeAssetPath(path); - - // Ensure .mat extension - if (!path.EndsWith(".mat", StringComparison.OrdinalIgnoreCase)) - { - path += ".mat"; - } - - return path; - } - - private static object SetMaterialShaderProperty(JObject @params) - { - string materialPath = NormalizePath(@params["materialPath"]?.ToString()); - string property = @params["property"]?.ToString(); - JToken value = @params["value"]; - - if (string.IsNullOrEmpty(materialPath) || string.IsNullOrEmpty(property) || value == null) - { - return new ErrorResponse("materialPath, property, and value are required"); - } - - // Find material - var findInstruction = new JObject { ["find"] = materialPath }; - Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material; - - if (mat == null) - { - return new ErrorResponse($"Could not find material at path: {materialPath}"); - } - - Undo.RecordObject(mat, "Set Material Property"); - - // Normalize alias/casing once for all code paths - property = MaterialOps.ResolvePropertyName(mat, property); - - // 1. Try handling Texture instruction explicitly (ManageMaterial special feature) - if (value.Type == JTokenType.Object) - { - // Check if it looks like an instruction - if (value is JObject obj && (obj.ContainsKey("find") || obj.ContainsKey("method"))) - { - Texture tex = ObjectResolver.Resolve(obj, typeof(Texture)) as Texture; - if (tex != null && mat.HasProperty(property)) - { - mat.SetTexture(property, tex); - EditorUtility.SetDirty(mat); - return new SuccessResponse($"Set texture property {property} on {mat.name}"); - } - } - } - - // 2. Fallback to standard logic via MaterialOps (handles Colors, Floats, Strings->Path) - bool success = MaterialOps.TrySetShaderProperty(mat, property, value, UnityJsonSerializer.Instance); - - if (success) - { - EditorUtility.SetDirty(mat); - return new SuccessResponse($"Set property {property} on {mat.name}"); - } - else - { - return new ErrorResponse($"Failed to set property {property}. Value format might be unsupported or texture not found."); - } - } - - private static object SetMaterialColor(JObject @params) - { - string materialPath = NormalizePath(@params["materialPath"]?.ToString()); - JToken colorToken = @params["color"]; - string property = @params["property"]?.ToString(); - - if (string.IsNullOrEmpty(materialPath) || colorToken == null) - { - return new ErrorResponse("materialPath and color are required"); - } - - var findInstruction = new JObject { ["find"] = materialPath }; - Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material; - - if (mat == null) - { - return new ErrorResponse($"Could not find material at path: {materialPath}"); - } - - Color color; - try - { - color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance); - } - catch (Exception e) - { - return new ErrorResponse($"Invalid color format: {e.Message}"); - } - - Undo.RecordObject(mat, "Set Material Color"); - - bool foundProp = false; - if (!string.IsNullOrEmpty(property)) - { - if (mat.HasProperty(property)) - { - mat.SetColor(property, color); - foundProp = true; - } - } - else - { - // Fallback logic: _BaseColor (URP/HDRP) then _Color (Built-in) - if (mat.HasProperty("_BaseColor")) - { - mat.SetColor("_BaseColor", color); - foundProp = true; - property = "_BaseColor"; - } - else if (mat.HasProperty("_Color")) - { - mat.SetColor("_Color", color); - foundProp = true; - property = "_Color"; - } - } - - if (foundProp) - { - EditorUtility.SetDirty(mat); - return new SuccessResponse($"Set color on {property}"); - } - else - { - return new ErrorResponse("Could not find suitable color property (_BaseColor or _Color) or specified property does not exist."); - } - } - - private static object AssignMaterialToRenderer(JObject @params) - { - string target = @params["target"]?.ToString(); - string searchMethod = @params["searchMethod"]?.ToString(); - string materialPath = NormalizePath(@params["materialPath"]?.ToString()); - int slot = @params["slot"]?.ToObject() ?? 0; - - if (string.IsNullOrEmpty(target) || string.IsNullOrEmpty(materialPath)) - { - return new ErrorResponse("target and materialPath are required"); - } - - var goInstruction = new JObject { ["find"] = target }; - if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod; - - GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject; - if (go == null) - { - return new ErrorResponse($"Could not find target GameObject: {target}"); - } - - Renderer renderer = go.GetComponent(); - if (renderer == null) - { - return new ErrorResponse($"GameObject {go.name} has no Renderer component"); - } - - var matInstruction = new JObject { ["find"] = materialPath }; - Material mat = ObjectResolver.Resolve(matInstruction, typeof(Material)) as Material; - if (mat == null) - { - return new ErrorResponse($"Could not find material: {materialPath}"); - } - - Undo.RecordObject(renderer, "Assign Material"); - - Material[] sharedMats = renderer.sharedMaterials; - if (slot < 0 || slot >= sharedMats.Length) - { - return new ErrorResponse($"Slot {slot} out of bounds (count: {sharedMats.Length})"); - } - - sharedMats[slot] = mat; - renderer.sharedMaterials = sharedMats; - - EditorUtility.SetDirty(renderer); - return new SuccessResponse($"Assigned material {mat.name} to {go.name} slot {slot}"); - } - - private static object SetRendererColor(JObject @params) - { - string target = @params["target"]?.ToString(); - string searchMethod = @params["searchMethod"]?.ToString(); - JToken colorToken = @params["color"]; - int slot = @params["slot"]?.ToObject() ?? 0; - string mode = @params["mode"]?.ToString() ?? "property_block"; - - if (string.IsNullOrEmpty(target) || colorToken == null) - { - return new ErrorResponse("target and color are required"); - } - - Color color; - try - { - color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance); - } - catch (Exception e) - { - return new ErrorResponse($"Invalid color format: {e.Message}"); - } - - var goInstruction = new JObject { ["find"] = target }; - if (!string.IsNullOrEmpty(searchMethod)) goInstruction["method"] = searchMethod; - - GameObject go = ObjectResolver.Resolve(goInstruction, typeof(GameObject)) as GameObject; - if (go == null) - { - return new ErrorResponse($"Could not find target GameObject: {target}"); - } - - Renderer renderer = go.GetComponent(); - if (renderer == null) - { - return new ErrorResponse($"GameObject {go.name} has no Renderer component"); - } - - if (mode == "property_block") - { - if (slot < 0 || slot >= renderer.sharedMaterials.Length) - { - return new ErrorResponse($"Slot {slot} out of bounds (count: {renderer.sharedMaterials.Length})"); - } - - MaterialPropertyBlock block = new MaterialPropertyBlock(); - renderer.GetPropertyBlock(block, slot); - - if (renderer.sharedMaterials[slot] != null) - { - Material mat = renderer.sharedMaterials[slot]; - if (mat.HasProperty("_BaseColor")) block.SetColor("_BaseColor", color); - else if (mat.HasProperty("_Color")) block.SetColor("_Color", color); - else block.SetColor("_Color", color); - } - else - { - block.SetColor("_Color", color); - } - - renderer.SetPropertyBlock(block, slot); - EditorUtility.SetDirty(renderer); - return new SuccessResponse($"Set renderer color (PropertyBlock) on slot {slot}"); - } - else if (mode == "shared") - { - if (slot >= 0 && slot < renderer.sharedMaterials.Length) - { - Material mat = renderer.sharedMaterials[slot]; - if (mat == null) - { - return new ErrorResponse($"No material in slot {slot}"); - } - Undo.RecordObject(mat, "Set Material Color"); - if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color); - else mat.SetColor("_Color", color); - EditorUtility.SetDirty(mat); - return new SuccessResponse("Set shared material color"); - } - return new ErrorResponse("Invalid slot"); - } - else if (mode == "instance") - { - if (slot >= 0 && slot < renderer.materials.Length) - { - Material mat = renderer.materials[slot]; - if (mat == null) - { - return new ErrorResponse($"No material in slot {slot}"); - } - // Note: Undo cannot fully revert material instantiation - Undo.RecordObject(mat, "Set Instance Material Color"); - if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color); - else mat.SetColor("_Color", color); - return new SuccessResponse("Set instance material color", new { warning = "Material instance created; Undo cannot fully revert instantiation." }); - } - return new ErrorResponse("Invalid slot"); - } - - return new ErrorResponse($"Unknown mode: {mode}"); - } - - private static object GetMaterialInfo(JObject @params) - { - string materialPath = NormalizePath(@params["materialPath"]?.ToString()); - if (string.IsNullOrEmpty(materialPath)) - { - return new ErrorResponse("materialPath is required"); - } - - var findInstruction = new JObject { ["find"] = materialPath }; - Material mat = ObjectResolver.Resolve(findInstruction, typeof(Material)) as Material; - - if (mat == null) - { - return new ErrorResponse($"Could not find material at path: {materialPath}"); - } - - Shader shader = mat.shader; - var properties = new List(); - -#if UNITY_6000_0_OR_NEWER - int propertyCount = shader.GetPropertyCount(); - for (int i = 0; i < propertyCount; i++) - { - string name = shader.GetPropertyName(i); - var type = shader.GetPropertyType(i); - string description = shader.GetPropertyDescription(i); - - object currentValue = null; - try - { - if (mat.HasProperty(name)) - { - switch (type) - { - case UnityEngine.Rendering.ShaderPropertyType.Color: - var c = mat.GetColor(name); - currentValue = new { r = c.r, g = c.g, b = c.b, a = c.a }; - break; - case UnityEngine.Rendering.ShaderPropertyType.Vector: - var v = mat.GetVector(name); - currentValue = new { x = v.x, y = v.y, z = v.z, w = v.w }; - break; - case UnityEngine.Rendering.ShaderPropertyType.Float: - case UnityEngine.Rendering.ShaderPropertyType.Range: - currentValue = mat.GetFloat(name); - break; - case UnityEngine.Rendering.ShaderPropertyType.Texture: - currentValue = mat.GetTexture(name)?.name ?? "null"; - break; - } - } - } - catch (Exception ex) - { - currentValue = $""; - } - - properties.Add(new - { - name = name, - type = type.ToString(), - description = description, - value = currentValue - }); - } -#else - int propertyCount = ShaderUtil.GetPropertyCount(shader); - for (int i = 0; i < propertyCount; i++) - { - string name = ShaderUtil.GetPropertyName(shader, i); - ShaderUtil.ShaderPropertyType type = ShaderUtil.GetPropertyType(shader, i); - string description = ShaderUtil.GetPropertyDescription(shader, i); - - object currentValue = null; - try - { - if (mat.HasProperty(name)) - { - switch (type) - { - case ShaderUtil.ShaderPropertyType.Color: - var c = mat.GetColor(name); - currentValue = new { r = c.r, g = c.g, b = c.b, a = c.a }; - break; - case ShaderUtil.ShaderPropertyType.Vector: - var v = mat.GetVector(name); - currentValue = new { x = v.x, y = v.y, z = v.z, w = v.w }; - break; - case ShaderUtil.ShaderPropertyType.Float: currentValue = mat.GetFloat(name); break; - case ShaderUtil.ShaderPropertyType.Range: currentValue = mat.GetFloat(name); break; - case ShaderUtil.ShaderPropertyType.TexEnv: currentValue = mat.GetTexture(name)?.name ?? "null"; break; - } - } - } - catch (Exception ex) - { - currentValue = $""; - } - - properties.Add(new - { - name = name, - type = type.ToString(), - description = description, - value = currentValue - }); - } -#endif - - return new SuccessResponse($"Retrieved material info for {mat.name}", new - { - material = mat.name, - shader = shader.name, - properties = properties - }); - } - - private static object CreateMaterial(JObject @params) - { - string materialPath = NormalizePath(@params["materialPath"]?.ToString()); - string shaderName = @params["shader"]?.ToString() ?? "Standard"; - JToken colorToken = @params["color"]; - string colorProperty = @params["property"]?.ToString(); - - JObject properties = null; - JToken propsToken = @params["properties"]; - if (propsToken != null) - { - if (propsToken.Type == JTokenType.String) - { - try { properties = JObject.Parse(propsToken.ToString()); } - catch (Exception ex) { return new ErrorResponse($"Invalid JSON in properties: {ex.Message}"); } - } - else if (propsToken is JObject obj) - { - properties = obj; - } - } - - if (string.IsNullOrEmpty(materialPath)) - { - return new ErrorResponse("materialPath is required"); - } - - // Safety check: SanitizeAssetPath should guarantee Assets/ prefix - // This check catches edge cases where normalization might fail - if (!materialPath.StartsWith("Assets/")) - { - return new ErrorResponse($"Invalid path '{materialPath}'. Path must be within Assets/ folder."); - } - - Shader shader = RenderPipelineUtility.ResolveShader(shaderName); - if (shader == null) - { - return new ErrorResponse($"Could not find shader: {shaderName}"); - } - - // Check for existing asset to avoid silent overwrite - if (AssetDatabase.LoadAssetAtPath(materialPath) != null) - { - return new ErrorResponse($"Material already exists at {materialPath}"); - } - - Material material = null; - var shouldDestroyMaterial = true; - try - { - material = new Material(shader); - - // Apply color param during creation (keeps Python tool signature and C# implementation consistent). - // If "properties" already contains a color property, let properties win. - bool shouldApplyColor = false; - if (colorToken != null) - { - if (properties == null) - { - shouldApplyColor = true; - } - else if (!string.IsNullOrEmpty(colorProperty)) - { - // If colorProperty is specified, only check that specific property. - shouldApplyColor = !properties.ContainsKey(colorProperty); - } - else - { - // If colorProperty is not specified, check fallback properties. - shouldApplyColor = !properties.ContainsKey("_BaseColor") && !properties.ContainsKey("_Color"); - } - } - - if (shouldApplyColor) - { - Color color; - try - { - color = MaterialOps.ParseColor(colorToken, UnityJsonSerializer.Instance); - } - catch (Exception e) - { - return new ErrorResponse($"Invalid color format: {e.Message}"); - } - - if (!string.IsNullOrEmpty(colorProperty)) - { - if (material.HasProperty(colorProperty)) - { - material.SetColor(colorProperty, color); - } - else - { - return new ErrorResponse($"Specified color property '{colorProperty}' does not exist on this material."); - } - } - else if (material.HasProperty("_BaseColor")) - { - material.SetColor("_BaseColor", color); - } - else if (material.HasProperty("_Color")) - { - material.SetColor("_Color", color); - } - else - { - return new ErrorResponse("Could not find suitable color property (_BaseColor or _Color) on this material's shader."); - } - } - - AssetDatabase.CreateAsset(material, materialPath); - shouldDestroyMaterial = false; // material is now owned by the AssetDatabase - - if (properties != null) - { - MaterialOps.ApplyProperties(material, properties, UnityJsonSerializer.Instance); - } - - EditorUtility.SetDirty(material); - AssetDatabase.SaveAssets(); - - return new SuccessResponse($"Created material at {materialPath} with shader {shaderName}"); - } - finally - { - if (shouldDestroyMaterial && material != null) - { - UnityEngine.Object.DestroyImmediate(material); - } - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/ManageMaterial.cs.meta b/Assets/MCPForUnity/Editor/Tools/ManageMaterial.cs.meta deleted file mode 100644 index f5cd95f..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageMaterial.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: e55741e2b00794a049a0ed5e63278a56 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/ManageMaterial.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/ManageScene.cs b/Assets/MCPForUnity/Editor/Tools/ManageScene.cs deleted file mode 100644 index 2ceecd9..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageScene.cs +++ /dev/null @@ -1,838 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using MCPForUnity.Editor.Helpers; // For Response class -using MCPForUnity.Runtime.Helpers; // For ScreenshotUtility -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; -using UnityEngine.SceneManagement; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Handles scene management operations like loading, saving, creating, and querying hierarchy. - /// - [McpForUnityTool("manage_scene", AutoRegister = false)] - public static class ManageScene - { - private sealed class SceneCommand - { - public string action { get; set; } = string.Empty; - public string name { get; set; } = string.Empty; - public string path { get; set; } = string.Empty; - public int? buildIndex { get; set; } - public string fileName { get; set; } = string.Empty; - public int? superSize { get; set; } - - // get_hierarchy paging + safety (summary-first) - public JToken parent { get; set; } - public int? pageSize { get; set; } - public int? cursor { get; set; } - public int? maxNodes { get; set; } - public int? maxDepth { get; set; } - public int? maxChildrenPerNode { get; set; } - public bool? includeTransform { get; set; } - } - - private static SceneCommand ToSceneCommand(JObject p) - { - if (p == null) return new SceneCommand(); - return new SceneCommand - { - action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(), - name = p["name"]?.ToString() ?? string.Empty, - path = p["path"]?.ToString() ?? string.Empty, - buildIndex = ParamCoercion.CoerceIntNullable(p["buildIndex"] ?? p["build_index"]), - fileName = (p["fileName"] ?? p["filename"])?.ToString() ?? string.Empty, - superSize = ParamCoercion.CoerceIntNullable(p["superSize"] ?? p["super_size"] ?? p["supersize"]), - - // get_hierarchy paging + safety - parent = p["parent"], - pageSize = ParamCoercion.CoerceIntNullable(p["pageSize"] ?? p["page_size"]), - cursor = ParamCoercion.CoerceIntNullable(p["cursor"]), - maxNodes = ParamCoercion.CoerceIntNullable(p["maxNodes"] ?? p["max_nodes"]), - maxDepth = ParamCoercion.CoerceIntNullable(p["maxDepth"] ?? p["max_depth"]), - maxChildrenPerNode = ParamCoercion.CoerceIntNullable(p["maxChildrenPerNode"] ?? p["max_children_per_node"]), - includeTransform = ParamCoercion.CoerceBoolNullable(p["includeTransform"] ?? p["include_transform"]), - }; - } - - /// - /// Main handler for scene management actions. - /// - public static object HandleCommand(JObject @params) - { - try { McpLog.Info("[ManageScene] HandleCommand: start", always: false); } catch { } - var cmd = ToSceneCommand(@params); - string action = cmd.action; - string name = string.IsNullOrEmpty(cmd.name) ? null : cmd.name; - string path = string.IsNullOrEmpty(cmd.path) ? null : cmd.path; // Relative to Assets/ - int? buildIndex = cmd.buildIndex; - // bool loadAdditive = @params["loadAdditive"]?.ToObject() ?? false; // Example for future extension - - // Ensure path is relative to Assets/, removing any leading "Assets/" - string relativeDir = path ?? string.Empty; - if (!string.IsNullOrEmpty(relativeDir)) - { - relativeDir = AssetPathUtility.NormalizeSeparators(relativeDir).Trim('/'); - if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) - { - relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/'); - } - } - - // Apply default *after* sanitizing, using the original path variable for the check - if (string.IsNullOrEmpty(path) && action == "create") // Check original path for emptiness - { - relativeDir = "Scenes"; // Default relative directory - } - - if (string.IsNullOrEmpty(action)) - { - return new ErrorResponse("Action parameter is required."); - } - - string sceneFileName = string.IsNullOrEmpty(name) ? null : $"{name}.unity"; - // Construct full system path correctly: ProjectRoot/Assets/relativeDir/sceneFileName - string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Combine with Assets path (Application.dataPath ends in Assets) - string fullPath = string.IsNullOrEmpty(sceneFileName) - ? null - : Path.Combine(fullPathDir, sceneFileName); - // Ensure relativePath always starts with "Assets/" and uses forward slashes - string relativePath = string.IsNullOrEmpty(sceneFileName) - ? null - : AssetPathUtility.NormalizeSeparators(Path.Combine("Assets", relativeDir, sceneFileName)); - - // Ensure directory exists for 'create' - if (action == "create" && !string.IsNullOrEmpty(fullPathDir)) - { - try - { - Directory.CreateDirectory(fullPathDir); - } - catch (Exception e) - { - return new ErrorResponse( - $"Could not create directory '{fullPathDir}': {e.Message}" - ); - } - } - - // Route action - try { McpLog.Info($"[ManageScene] Route action='{action}' name='{name}' path='{path}' buildIndex={(buildIndex.HasValue ? buildIndex.Value.ToString() : "null")}", always: false); } catch { } - switch (action) - { - case "create": - if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(relativePath)) - return new ErrorResponse( - "'name' and 'path' parameters are required for 'create' action." - ); - return CreateScene(fullPath, relativePath); - case "load": - // Loading can be done by path/name or build index - if (!string.IsNullOrEmpty(relativePath)) - return LoadScene(relativePath); - else if (buildIndex.HasValue) - return LoadScene(buildIndex.Value); - else - return new ErrorResponse( - "Either 'name'/'path' or 'buildIndex' must be provided for 'load' action." - ); - case "save": - // Save current scene, optionally to a new path - return SaveScene(fullPath, relativePath); - case "get_hierarchy": - try { McpLog.Info("[ManageScene] get_hierarchy: entering", always: false); } catch { } - var gh = GetSceneHierarchyPaged(cmd); - try { McpLog.Info("[ManageScene] get_hierarchy: exiting", always: false); } catch { } - return gh; - case "get_active": - try { McpLog.Info("[ManageScene] get_active: entering", always: false); } catch { } - var ga = GetActiveSceneInfo(); - try { McpLog.Info("[ManageScene] get_active: exiting", always: false); } catch { } - return ga; - case "get_build_settings": - return GetBuildSettingsScenes(); - case "screenshot": - return CaptureScreenshot(cmd.fileName, cmd.superSize); - // Add cases for modifying build settings, additive loading, unloading etc. - default: - return new ErrorResponse( - $"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings, screenshot." - ); - } - } - - /// - /// Captures a screenshot to Assets/Screenshots and returns a response payload. - /// Public so the tools UI can reuse the same logic without duplicating parameters. - /// Available in both Edit Mode and Play Mode. - /// - public static object ExecuteScreenshot(string fileName = null, int? superSize = null) - { - return CaptureScreenshot(fileName, superSize); - } - - private static object CreateScene(string fullPath, string relativePath) - { - if (File.Exists(fullPath)) - { - return new ErrorResponse($"Scene already exists at '{relativePath}'."); - } - - try - { - // Create a new empty scene - Scene newScene = EditorSceneManager.NewScene( - NewSceneSetup.EmptyScene, - NewSceneMode.Single - ); - // Save it to the specified path - bool saved = EditorSceneManager.SaveScene(newScene, relativePath); - - if (saved) - { - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Ensure Unity sees the new scene file - return new SuccessResponse( - $"Scene '{Path.GetFileName(relativePath)}' created successfully at '{relativePath}'.", - new { path = relativePath } - ); - } - else - { - // If SaveScene fails, it might leave an untitled scene open. - // Optionally try to close it, but be cautious. - return new ErrorResponse($"Failed to save new scene to '{relativePath}'."); - } - } - catch (Exception e) - { - return new ErrorResponse($"Error creating scene '{relativePath}': {e.Message}"); - } - } - - private static object LoadScene(string relativePath) - { - if ( - !File.Exists( - Path.Combine( - Application.dataPath.Substring( - 0, - Application.dataPath.Length - "Assets".Length - ), - relativePath - ) - ) - ) - { - return new ErrorResponse($"Scene file not found at '{relativePath}'."); - } - - // Check for unsaved changes in the current scene - if (EditorSceneManager.GetActiveScene().isDirty) - { - // Optionally prompt the user or save automatically before loading - return new ErrorResponse( - "Current scene has unsaved changes. Please save or discard changes before loading a new scene." - ); - // Example: bool saveOK = EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo(); - // if (!saveOK) return new ErrorResponse("Load cancelled by user."); - } - - try - { - EditorSceneManager.OpenScene(relativePath, OpenSceneMode.Single); - return new SuccessResponse( - $"Scene '{relativePath}' loaded successfully.", - new - { - path = relativePath, - name = Path.GetFileNameWithoutExtension(relativePath), - } - ); - } - catch (Exception e) - { - return new ErrorResponse($"Error loading scene '{relativePath}': {e.Message}"); - } - } - - private static object LoadScene(int buildIndex) - { - if (buildIndex < 0 || buildIndex >= SceneManager.sceneCountInBuildSettings) - { - return new ErrorResponse( - $"Invalid build index: {buildIndex}. Must be between 0 and {SceneManager.sceneCountInBuildSettings - 1}." - ); - } - - // Check for unsaved changes - if (EditorSceneManager.GetActiveScene().isDirty) - { - return new ErrorResponse( - "Current scene has unsaved changes. Please save or discard changes before loading a new scene." - ); - } - - try - { - string scenePath = SceneUtility.GetScenePathByBuildIndex(buildIndex); - EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); - return new SuccessResponse( - $"Scene at build index {buildIndex} ('{scenePath}') loaded successfully.", - new - { - path = scenePath, - name = Path.GetFileNameWithoutExtension(scenePath), - buildIndex = buildIndex, - } - ); - } - catch (Exception e) - { - return new ErrorResponse( - $"Error loading scene with build index {buildIndex}: {e.Message}" - ); - } - } - - private static object SaveScene(string fullPath, string relativePath) - { - try - { - Scene currentScene = EditorSceneManager.GetActiveScene(); - if (!currentScene.IsValid()) - { - return new ErrorResponse("No valid scene is currently active to save."); - } - - bool saved; - string finalPath = currentScene.path; // Path where it was last saved or will be saved - - if (!string.IsNullOrEmpty(relativePath) && currentScene.path != relativePath) - { - // Save As... - // Ensure directory exists - string dir = Path.GetDirectoryName(fullPath); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - saved = EditorSceneManager.SaveScene(currentScene, relativePath); - finalPath = relativePath; - } - else - { - // Save (overwrite existing or save untitled) - if (string.IsNullOrEmpty(currentScene.path)) - { - // Scene is untitled, needs a path - return new ErrorResponse( - "Cannot save an untitled scene without providing a 'name' and 'path'. Use Save As functionality." - ); - } - saved = EditorSceneManager.SaveScene(currentScene); - } - - if (saved) - { - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); - return new SuccessResponse( - $"Scene '{currentScene.name}' saved successfully to '{finalPath}'.", - new { path = finalPath, name = currentScene.name } - ); - } - else - { - return new ErrorResponse($"Failed to save scene '{currentScene.name}'."); - } - } - catch (Exception e) - { - return new ErrorResponse($"Error saving scene: {e.Message}"); - } - } - - private static object CaptureScreenshot(string fileName, int? superSize) - { - try - { - int resolvedSuperSize = (superSize.HasValue && superSize.Value > 0) ? superSize.Value : 1; - - // Batch mode warning - if (Application.isBatchMode) - { - McpLog.Warn("[ManageScene] Screenshot capture in batch mode uses camera-based fallback. Results may vary."); - } - - // Check Screen Capture module availability and warn if not available - bool screenCaptureAvailable = ScreenshotUtility.IsScreenCaptureModuleAvailable; - bool hasCameraFallback = Camera.main != null || UnityEngine.Object.FindObjectsOfType().Length > 0; - -#if UNITY_2022_1_OR_NEWER - if (!screenCaptureAvailable && !hasCameraFallback) - { - return new ErrorResponse( - "Cannot capture screenshot. The Screen Capture module is not enabled and no Camera was found in the scene. " + - "Please either: (1) Enable the Screen Capture module: Window > Package Manager > Built-in > Screen Capture > Enable, " + - "or (2) Add a Camera to your scene for camera-based fallback capture." - ); - } - - if (!screenCaptureAvailable) - { - McpLog.Warn("[ManageScene] Screen Capture module not enabled. Using camera-based fallback. " + - "For best results, enable it: Window > Package Manager > Built-in > Screen Capture > Enable."); - } -#else - if (!hasCameraFallback) - { - return new ErrorResponse( - "No camera found in the scene. Screenshot capture on Unity versions before 2022.1 requires a Camera in the scene. " + - "Please add a Camera to your scene or upgrade to Unity 2022.1+ for ScreenCapture API support." - ); - } -#endif - - // Best-effort: ensure Game View exists and repaints before capture. - if (!Application.isBatchMode) - { - EnsureGameView(); - } - - ScreenshotCaptureResult result = ScreenshotUtility.CaptureToAssetsFolder(fileName, resolvedSuperSize, ensureUniqueFileName: true); - - // ScreenCapture.CaptureScreenshot is async. Import after the file actually hits disk. - if (result.IsAsync) - { - ScheduleAssetImportWhenFileExists(result.AssetsRelativePath, result.FullPath, timeoutSeconds: 30.0); - } - else - { - AssetDatabase.ImportAsset(result.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport); - } - - string verb = result.IsAsync ? "Screenshot requested" : "Screenshot captured"; - string message = $"{verb} to '{result.AssetsRelativePath}' (full: {result.FullPath})."; - - return new SuccessResponse( - message, - new - { - path = result.AssetsRelativePath, - fullPath = result.FullPath, - superSize = result.SuperSize, - isAsync = result.IsAsync, - } - ); - } - catch (Exception e) - { - return new ErrorResponse($"Error capturing screenshot: {e.Message}"); - } - } - - private static void EnsureGameView() - { - try - { - // Ensure a Game View exists and has a chance to repaint before capture. - try - { - if (!EditorApplication.ExecuteMenuItem("Window/General/Game")) - { - // Some Unity versions expose hotkey suffixes in menu paths. - EditorApplication.ExecuteMenuItem("Window/General/Game %2"); - } - } - catch (Exception e) - { - try { McpLog.Debug($"[ManageScene] screenshot: failed to open Game View via menu item: {e.Message}"); } catch { } - } - - try - { - var gameViewType = Type.GetType("UnityEditor.GameView,UnityEditor"); - if (gameViewType != null) - { - var window = EditorWindow.GetWindow(gameViewType); - window?.Repaint(); - } - } - catch (Exception e) - { - try { McpLog.Debug($"[ManageScene] screenshot: failed to repaint Game View: {e.Message}"); } catch { } - } - - try { SceneView.RepaintAll(); } - catch (Exception e) - { - try { McpLog.Debug($"[ManageScene] screenshot: failed to repaint Scene View: {e.Message}"); } catch { } - } - - try { EditorApplication.QueuePlayerLoopUpdate(); } - catch (Exception e) - { - try { McpLog.Debug($"[ManageScene] screenshot: failed to queue player loop update: {e.Message}"); } catch { } - } - } - catch (Exception e) - { - try { McpLog.Debug($"[ManageScene] screenshot: EnsureGameView failed: {e.Message}"); } catch { } - } - } - - private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, string fullPath, double timeoutSeconds) - { - if (string.IsNullOrWhiteSpace(assetsRelativePath) || string.IsNullOrWhiteSpace(fullPath)) - { - McpLog.Warn("[ManageScene] ScheduleAssetImportWhenFileExists: invalid paths provided, skipping import scheduling."); - return; - } - - double start = EditorApplication.timeSinceStartup; - int failureCount = 0; - bool hasSeenFile = false; - const int maxLoggedFailures = 3; - EditorApplication.CallbackFunction tick = null; - tick = () => - { - try - { - if (File.Exists(fullPath)) - { - hasSeenFile = true; - - AssetDatabase.ImportAsset(assetsRelativePath, ImportAssetOptions.ForceSynchronousImport); - McpLog.Debug($"[ManageScene] Imported asset at '{assetsRelativePath}'."); - EditorApplication.update -= tick; - return; - } - } - catch (Exception e) - { - failureCount++; - - if (failureCount <= maxLoggedFailures) - { - McpLog.Warn($"[ManageScene] Exception while importing asset '{assetsRelativePath}' from '{fullPath}' (attempt {failureCount}): {e}"); - } - } - - if (EditorApplication.timeSinceStartup - start > timeoutSeconds) - { - if (!hasSeenFile) - { - McpLog.Warn($"[ManageScene] Timed out waiting for file '{fullPath}' (asset: '{assetsRelativePath}') after {timeoutSeconds:F1} seconds. The asset was not imported."); - } - else - { - McpLog.Warn($"[ManageScene] Timed out importing asset '{assetsRelativePath}' from '{fullPath}' after {timeoutSeconds:F1} seconds. The file existed but the asset was not imported."); - } - - EditorApplication.update -= tick; - } - }; - - EditorApplication.update += tick; - } - - private static object GetActiveSceneInfo() - { - try - { - try { McpLog.Info("[ManageScene] get_active: querying EditorSceneManager.GetActiveScene", always: false); } catch { } - Scene activeScene = EditorSceneManager.GetActiveScene(); - try { McpLog.Info($"[ManageScene] get_active: got scene valid={activeScene.IsValid()} loaded={activeScene.isLoaded} name='{activeScene.name}'", always: false); } catch { } - if (!activeScene.IsValid()) - { - return new ErrorResponse("No active scene found."); - } - - var sceneInfo = new - { - name = activeScene.name, - path = activeScene.path, - buildIndex = activeScene.buildIndex, // -1 if not in build settings - isDirty = activeScene.isDirty, - isLoaded = activeScene.isLoaded, - rootCount = activeScene.rootCount, - }; - - return new SuccessResponse("Retrieved active scene information.", sceneInfo); - } - catch (Exception e) - { - try { McpLog.Error($"[ManageScene] get_active: exception {e.Message}"); } catch { } - return new ErrorResponse($"Error getting active scene info: {e.Message}"); - } - } - - private static object GetBuildSettingsScenes() - { - try - { - var scenes = new List(); - for (int i = 0; i < EditorBuildSettings.scenes.Length; i++) - { - var scene = EditorBuildSettings.scenes[i]; - scenes.Add( - new - { - path = scene.path, - guid = scene.guid.ToString(), - enabled = scene.enabled, - buildIndex = i, // Actual build index considering only enabled scenes might differ - } - ); - } - return new SuccessResponse("Retrieved scenes from Build Settings.", scenes); - } - catch (Exception e) - { - return new ErrorResponse($"Error getting scenes from Build Settings: {e.Message}"); - } - } - - private static object GetSceneHierarchyPaged(SceneCommand cmd) - { - try - { - // Check Prefab Stage first - var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); - Scene activeScene; - - if (prefabStage != null) - { - activeScene = prefabStage.scene; - try { McpLog.Info("[ManageScene] get_hierarchy: using Prefab Stage scene", always: false); } catch { } - } - else - { - try { McpLog.Info("[ManageScene] get_hierarchy: querying EditorSceneManager.GetActiveScene", always: false); } catch { } - activeScene = EditorSceneManager.GetActiveScene(); - } - - try { McpLog.Info($"[ManageScene] get_hierarchy: got scene valid={activeScene.IsValid()} loaded={activeScene.isLoaded} name='{activeScene.name}'", always: false); } catch { } - if (!activeScene.IsValid() || !activeScene.isLoaded) - { - return new ErrorResponse( - "No valid and loaded scene is active to get hierarchy from." - ); - } - - // Defaults tuned for safety; callers can override but we clamp to sane maxes. - // NOTE: pageSize is "items per page", not "number of pages". - // Keep this conservative to reduce peak response sizes when callers omit page_size. - int resolvedPageSize = Mathf.Clamp(cmd.pageSize ?? 50, 1, 500); - int resolvedCursor = Mathf.Max(0, cmd.cursor ?? 0); - int resolvedMaxNodes = Mathf.Clamp(cmd.maxNodes ?? 1000, 1, 5000); - int effectiveTake = Mathf.Min(resolvedPageSize, resolvedMaxNodes); - int resolvedMaxChildrenPerNode = Mathf.Clamp(cmd.maxChildrenPerNode ?? 200, 0, 2000); - bool includeTransform = cmd.includeTransform ?? false; - - // NOTE: maxDepth is accepted for forward-compatibility, but current paging mode - // returns a single level (roots or direct children). This keeps payloads bounded. - - List nodes; - string scope; - - GameObject parentGo = ResolveGameObject(cmd.parent, activeScene); - if (cmd.parent == null || cmd.parent.Type == JTokenType.Null) - { - try { McpLog.Info("[ManageScene] get_hierarchy: listing root objects (paged summary)", always: false); } catch { } - nodes = activeScene.GetRootGameObjects().Where(go => go != null).ToList(); - scope = "roots"; - } - else - { - if (parentGo == null) - { - return new ErrorResponse($"Parent GameObject ('{cmd.parent}') not found."); - } - try { McpLog.Info($"[ManageScene] get_hierarchy: listing children of '{parentGo.name}' (paged summary)", always: false); } catch { } - nodes = new List(parentGo.transform.childCount); - foreach (Transform child in parentGo.transform) - { - if (child != null) nodes.Add(child.gameObject); - } - scope = "children"; - } - - int total = nodes.Count; - if (resolvedCursor > total) resolvedCursor = total; - int end = Mathf.Min(total, resolvedCursor + effectiveTake); - - var items = new List(Mathf.Max(0, end - resolvedCursor)); - for (int i = resolvedCursor; i < end; i++) - { - var go = nodes[i]; - if (go == null) continue; - items.Add(BuildGameObjectSummary(go, includeTransform, resolvedMaxChildrenPerNode)); - } - - bool truncated = end < total; - string nextCursor = truncated ? end.ToString() : null; - - var payload = new - { - scope = scope, - cursor = resolvedCursor, - pageSize = effectiveTake, - next_cursor = nextCursor, - truncated = truncated, - total = total, - items = items, - }; - - var resp = new SuccessResponse($"Retrieved hierarchy page for scene '{activeScene.name}'.", payload); - try { McpLog.Info("[ManageScene] get_hierarchy: success", always: false); } catch { } - return resp; - } - catch (Exception e) - { - try { McpLog.Error($"[ManageScene] get_hierarchy: exception {e.Message}"); } catch { } - return new ErrorResponse($"Error getting scene hierarchy: {e.Message}"); - } - } - - private static GameObject ResolveGameObject(JToken targetToken, Scene activeScene) - { - if (targetToken == null || targetToken.Type == JTokenType.Null) return null; - - try - { - if (targetToken.Type == JTokenType.Integer || int.TryParse(targetToken.ToString(), out _)) - { - if (int.TryParse(targetToken.ToString(), out int id)) - { - var obj = EditorUtility.InstanceIDToObject(id); - if (obj is GameObject go) return go; - if (obj is Component c) return c.gameObject; - } - } - } - catch { } - - string s = targetToken.ToString(); - if (string.IsNullOrEmpty(s)) return null; - - // Path-based find (e.g., "Root/Child/GrandChild") - if (s.Contains("/")) - { - try - { - var ids = GameObjectLookup.SearchGameObjects("by_path", s, includeInactive: true, maxResults: 1); - if (ids.Count > 0) - { - var byPath = GameObjectLookup.FindById(ids[0]); - if (byPath != null) return byPath; - } - } - catch { } - } - - // Name-based find (first match, includes inactive) - try - { - var all = activeScene.GetRootGameObjects(); - foreach (var root in all) - { - if (root == null) continue; - if (root.name == s) return root; - var trs = root.GetComponentsInChildren(includeInactive: true); - foreach (var t in trs) - { - if (t != null && t.gameObject != null && t.gameObject.name == s) return t.gameObject; - } - } - } - catch { } - - return null; - } - - private static object BuildGameObjectSummary(GameObject go, bool includeTransform, int maxChildrenPerNode) - { - if (go == null) return null; - - int childCount = 0; - try { childCount = go.transform != null ? go.transform.childCount : 0; } catch { } - bool childrenTruncated = childCount > 0; // We do not inline children in summary mode. - - // Get component type names (lightweight - no full serialization) - var componentTypes = new List(); - try - { - var components = go.GetComponents(); - if (components != null) - { - foreach (var c in components) - { - if (c != null) - { - componentTypes.Add(c.GetType().Name); - } - } - } - } - catch (Exception ex) - { - McpLog.Debug($"[ManageScene] Failed to enumerate components for '{go.name}': {ex.Message}"); - } - - var d = new Dictionary - { - { "name", go.name }, - { "instanceID", go.GetInstanceID() }, - { "activeSelf", go.activeSelf }, - { "activeInHierarchy", go.activeInHierarchy }, - { "tag", go.tag }, - { "layer", go.layer }, - { "isStatic", go.isStatic }, - { "path", GetGameObjectPath(go) }, - { "childCount", childCount }, - { "childrenTruncated", childrenTruncated }, - { "childrenCursor", childCount > 0 ? "0" : null }, - { "childrenPageSizeDefault", maxChildrenPerNode }, - { "componentTypes", componentTypes }, - }; - - if (includeTransform && go.transform != null) - { - var t = go.transform; - d["transform"] = new - { - position = new[] { t.localPosition.x, t.localPosition.y, t.localPosition.z }, - rotation = new[] { t.localRotation.eulerAngles.x, t.localRotation.eulerAngles.y, t.localRotation.eulerAngles.z }, - scale = new[] { t.localScale.x, t.localScale.y, t.localScale.z }, - }; - } - - return d; - } - - private static string GetGameObjectPath(GameObject go) - { - if (go == null) return string.Empty; - try - { - var names = new Stack(); - Transform t = go.transform; - while (t != null) - { - names.Push(t.name); - t = t.parent; - } - return string.Join("/", names); - } - catch - { - return go.name; - } - } - - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/ManageScene.cs.meta b/Assets/MCPForUnity/Editor/Tools/ManageScene.cs.meta deleted file mode 100644 index 51823bd..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageScene.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: b6ddda47f4077e74fbb5092388cefcc2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/ManageScene.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/ManageScript.cs b/Assets/MCPForUnity/Editor/Tools/ManageScript.cs deleted file mode 100644 index 3e98172..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageScript.cs +++ /dev/null @@ -1,2672 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using System.Threading; -using System.Security.Cryptography; - -#if USE_ROSLYN -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Formatting; -#endif - -#if UNITY_EDITOR -using UnityEditor.Compilation; -#endif - - -namespace MCPForUnity.Editor.Tools -{ - /// - /// 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. - /// - [McpForUnityTool("manage_script", AutoRegister = false)] - public static class ManageScript - { - /// - /// Resolves a directory under Assets/, preventing traversal and escaping. - /// Returns fullPathDir on disk and canonical 'Assets/...' relative path. - /// - private static bool TryResolveUnderAssets(string relDir, out string fullPathDir, out string relPathSafe) - { - string assets = AssetPathUtility.NormalizeSeparators(Application.dataPath); - - // Normalize caller path: allow both "Scripts/..." and "Assets/Scripts/..." - string rel = AssetPathUtility.NormalizeSeparators(relDir ?? "Scripts").Trim(); - if (string.IsNullOrEmpty(rel)) rel = "Scripts"; - - // Handle both "Assets" and "Assets/" prefixes - if (rel.Equals("Assets", StringComparison.OrdinalIgnoreCase)) - { - rel = string.Empty; - } - else if (rel.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) - { - rel = rel.Substring(7); - } - - rel = rel.TrimStart('/'); - - string targetDir = AssetPathUtility.NormalizeSeparators(Path.Combine(assets, rel)); - string full = AssetPathUtility.NormalizeSeparators(Path.GetFullPath(targetDir)); - - bool underAssets = full.StartsWith(assets + "/", StringComparison.OrdinalIgnoreCase) - || string.Equals(full, assets, StringComparison.OrdinalIgnoreCase); - if (!underAssets) - { - fullPathDir = null; - relPathSafe = null; - return false; - } - - // Best-effort symlink guard: if the directory OR ANY ANCESTOR (up to Assets/) is a reparse point/symlink, reject - try - { - var di = new DirectoryInfo(full); - while (di != null) - { - if (di.Exists && (di.Attributes & FileAttributes.ReparsePoint) != 0) - { - fullPathDir = null; - relPathSafe = null; - return false; - } - var atAssets = string.Equals( - di.FullName.Replace('\\', '/'), - assets, - StringComparison.OrdinalIgnoreCase - ); - if (atAssets) break; - di = di.Parent; - } - } - catch { /* best effort; proceed */ } - - fullPathDir = full; - string tail = full.Length > assets.Length ? full.Substring(assets.Length).TrimStart('/') : string.Empty; - relPathSafe = ("Assets/" + tail).TrimEnd('/'); - return true; - } - /// - /// Main handler for script management actions. - /// - public static object HandleCommand(JObject @params) - { - // Handle null parameters - if (@params == null) - { - return new ErrorResponse("invalid_params", "Parameters cannot be null."); - } - - var p = new ToolParams(@params); - - // Extract and validate required parameters - var actionResult = p.GetRequired("action"); - if (!actionResult.IsSuccess) - { - return new ErrorResponse(actionResult.ErrorMessage); - } - string action = actionResult.Value.ToLowerInvariant(); - - var nameResult = p.GetRequired("name"); - if (!nameResult.IsSuccess) - { - return new ErrorResponse(nameResult.ErrorMessage); - } - string name = nameResult.Value; - - // Optional parameters - string path = p.Get("path"); // Relative to Assets/ - string contents = null; - - // Check if we have base64 encoded contents - bool contentsEncoded = p.GetBool("contentsEncoded", false); - if (contentsEncoded && p.Has("encodedContents")) - { - try - { - contents = DecodeBase64(p.Get("encodedContents")); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to decode script contents: {e.Message}"); - } - } - else - { - contents = p.Get("contents"); - } - - string scriptType = p.Get("scriptType"); // For templates/validation - string namespaceName = p.Get("namespace"); // For organizing code - // Basic name validation (alphanumeric, underscores, cannot start with number) - if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(2))) - { - return new ErrorResponse( - $"Invalid script name: '{name}'. Use only letters, numbers, underscores, and don't start with a number." - ); - } - - // Resolve and harden target directory under Assets/ - if (!TryResolveUnderAssets(path, out string fullPathDir, out string relPathSafeDir)) - { - return new ErrorResponse($"Invalid path. Target directory must be within 'Assets/'. Provided: '{(path ?? "(null)")}'"); - } - - // Construct file paths - string scriptFileName = $"{name}.cs"; - string fullPath = Path.Combine(fullPathDir, scriptFileName); - string relativePath = AssetPathUtility.NormalizeSeparators(Path.Combine(relPathSafeDir, scriptFileName)); - - // Ensure the target directory exists for create/update - if (action == "create" || action == "update") - { - try - { - Directory.CreateDirectory(fullPathDir); - } - catch (Exception e) - { - return new ErrorResponse( - $"Could not create directory '{fullPathDir}': {e.Message}" - ); - } - } - - // Route to specific action handlers - switch (action) - { - case "create": - return CreateScript( - fullPath, - relativePath, - name, - contents, - scriptType, - namespaceName - ); - case "read": - McpLog.Warn("manage_script.read is deprecated; prefer resources/read. Serving read for backward compatibility."); - return ReadScript(fullPath, relativePath); - case "update": - McpLog.Warn("manage_script.update is deprecated; prefer apply_text_edits. Serving update for backward compatibility."); - return UpdateScript(fullPath, relativePath, name, contents); - case "delete": - return DeleteScript(fullPath, relativePath); - case "apply_text_edits": - { - var textEdits = p.GetRaw("edits") as JArray; - string precondition = p.Get("precondition_sha256"); - // Respect optional options (guard type before indexing) - var optionsObj = p.GetRaw("options") as JObject; - string refreshOpt = optionsObj?["refresh"]?.ToString()?.ToLowerInvariant(); - string validateOpt = optionsObj?["validate"]?.ToString()?.ToLowerInvariant(); - return ApplyTextEdits(fullPath, relativePath, name, textEdits, precondition, refreshOpt, validateOpt); - } - case "validate": - { - string level = p.Get("level", "standard").ToLowerInvariant(); - var chosen = level switch - { - "basic" => ValidationLevel.Basic, - "standard" => ValidationLevel.Standard, - "strict" => ValidationLevel.Strict, - "comprehensive" => ValidationLevel.Comprehensive, - _ => ValidationLevel.Standard - }; - string fileText; - try { fileText = File.ReadAllText(fullPath); } - catch (Exception ex) { return new ErrorResponse($"Failed to read script: {ex.Message}"); } - - bool ok = ValidateScriptSyntax(fileText, chosen, out string[] diagsRaw); - var diags = (diagsRaw ?? Array.Empty()).Select(s => - { - var m = Regex.Match( - s, - @"^(ERROR|WARNING|INFO): (.*?)(?: \(Line (\d+)\))?$", - RegexOptions.CultureInvariant | RegexOptions.Multiline, - TimeSpan.FromMilliseconds(250) - ); - string severity = m.Success ? m.Groups[1].Value.ToLowerInvariant() : "info"; - string message = m.Success ? m.Groups[2].Value : s; - int lineNum = m.Success && int.TryParse(m.Groups[3].Value, out var l) ? l : 0; - return new { line = lineNum, col = 0, severity, message }; - }).ToArray(); - - var result = new { diagnostics = diags }; - return ok ? new SuccessResponse("Validation completed.", result) - : new ErrorResponse("Validation failed.", result); - } - case "edit": - McpLog.Warn("manage_script.edit is deprecated; prefer apply_text_edits. Serving structured edit for backward compatibility."); - var structEdits = @params["edits"] as JArray; - var options = @params["options"] as JObject; - return EditScript(fullPath, relativePath, name, structEdits, options); - case "get_sha": - { - try - { - if (!File.Exists(fullPath)) - return new ErrorResponse($"Script not found at '{relativePath}'."); - - string text = File.ReadAllText(fullPath); - string sha = ComputeSha256(text); - var fi = new FileInfo(fullPath); - long lengthBytes; - try { lengthBytes = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetByteCount(text); } - catch { lengthBytes = fi.Exists ? fi.Length : 0; } - var data = new - { - uri = $"mcpforunity://path/{relativePath}", - path = relativePath, - sha256 = sha, - lengthBytes, - lastModifiedUtc = fi.Exists ? fi.LastWriteTimeUtc.ToString("o") : string.Empty - }; - return new SuccessResponse($"SHA computed for '{relativePath}'.", data); - } - catch (Exception ex) - { - return new ErrorResponse($"Failed to compute SHA: {ex.Message}"); - } - } - default: - return new ErrorResponse( - $"Unknown action: '{action}'. Valid actions are: create, delete, apply_text_edits, validate, read (deprecated), update (deprecated), edit (deprecated)." - ); - } - } - - /// - /// Decode base64 string to normal text - /// - private static string DecodeBase64(string encoded) - { - byte[] data = Convert.FromBase64String(encoded); - return System.Text.Encoding.UTF8.GetString(data); - } - - /// - /// Encode text to base64 string - /// - private static string EncodeBase64(string text) - { - byte[] data = System.Text.Encoding.UTF8.GetBytes(text); - return Convert.ToBase64String(data); - } - - private static object CreateScript( - string fullPath, - string relativePath, - string name, - string contents, - string scriptType, - string namespaceName - ) - { - // Check if script already exists - if (File.Exists(fullPath)) - { - return new ErrorResponse( - $"Script already exists at '{relativePath}'. Use 'update' action to modify." - ); - } - - // Generate default content if none provided - if (string.IsNullOrEmpty(contents)) - { - contents = GenerateDefaultScriptContent(name, scriptType, namespaceName); - } - - // Validate syntax with detailed error reporting using GUI setting - ValidationLevel validationLevel = GetValidationLevelFromGUI(); - bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors); - if (!isValid) - { - return new ErrorResponse("validation_failed", new { status = "validation_failed", diagnostics = validationErrors ?? Array.Empty() }); - } - else if (validationErrors != null && validationErrors.Length > 0) - { - // Log warnings but don't block creation - McpLog.Warn($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors)); - } - - try - { - // Atomic create without BOM; schedule refresh after reply - var enc = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - var tmp = fullPath + ".tmp"; - File.WriteAllText(tmp, contents, enc); - try - { - File.Move(tmp, fullPath); - } - catch (IOException) - { - File.Copy(tmp, fullPath, overwrite: true); - try { File.Delete(tmp); } catch { } - } - - var uri = $"mcpforunity://path/{relativePath}"; - var ok = new SuccessResponse( - $"Script '{name}.cs' created successfully at '{relativePath}'.", - new { uri, scheduledRefresh = false } - ); - - ManageScriptRefreshHelpers.ImportAndRequestCompile(relativePath); - - return ok; - } - catch (Exception e) - { - return new ErrorResponse($"Failed to create script '{relativePath}': {e.Message}"); - } - } - - private static object ReadScript(string fullPath, string relativePath) - { - if (!File.Exists(fullPath)) - { - return new ErrorResponse($"Script not found at '{relativePath}'."); - } - - try - { - string contents = File.ReadAllText(fullPath); - - // Return both normal and encoded contents for larger files - bool isLarge = contents.Length > 10000; // If content is large, include encoded version - var uri = $"mcpforunity://path/{relativePath}"; - var responseData = new - { - uri, - path = relativePath, - contents = contents, - // For large files, also include base64-encoded version - encodedContents = isLarge ? EncodeBase64(contents) : null, - contentsEncoded = isLarge, - }; - - return new SuccessResponse( - $"Script '{Path.GetFileName(relativePath)}' read successfully.", - responseData - ); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to read script '{relativePath}': {e.Message}"); - } - } - - private static object UpdateScript( - string fullPath, - string relativePath, - string name, - string contents - ) - { - if (!File.Exists(fullPath)) - { - return new ErrorResponse( - $"Script not found at '{relativePath}'. Use 'create' action to add a new script." - ); - } - if (string.IsNullOrEmpty(contents)) - { - return new ErrorResponse("Content is required for the 'update' action."); - } - - // Validate syntax with detailed error reporting using GUI setting - ValidationLevel validationLevel = GetValidationLevelFromGUI(); - bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors); - if (!isValid) - { - return new ErrorResponse("validation_failed", new { status = "validation_failed", diagnostics = validationErrors ?? Array.Empty() }); - } - else if (validationErrors != null && validationErrors.Length > 0) - { - // Log warnings but don't block update - McpLog.Warn($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors)); - } - - try - { - // Safe write with atomic replace when available, without BOM - var encoding = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - string tempPath = fullPath + ".tmp"; - File.WriteAllText(tempPath, contents, encoding); - - string backupPath = fullPath + ".bak"; - try - { - File.Replace(tempPath, fullPath, backupPath); - try { if (File.Exists(backupPath)) File.Delete(backupPath); } catch { } - } - catch (PlatformNotSupportedException) - { - File.Copy(tempPath, fullPath, true); - try { File.Delete(tempPath); } catch { } - try { if (File.Exists(backupPath)) File.Delete(backupPath); } catch { } - } - catch (IOException) - { - File.Copy(tempPath, fullPath, true); - try { File.Delete(tempPath); } catch { } - try { if (File.Exists(backupPath)) File.Delete(backupPath); } catch { } - } - - // Prepare success response BEFORE any operation that can trigger a domain reload - var uri = $"mcpforunity://path/{relativePath}"; - var ok = new SuccessResponse( - $"Script '{name}.cs' updated successfully at '{relativePath}'.", - new { uri, path = relativePath, scheduledRefresh = true } - ); - - // Schedule a debounced import/compile on next editor tick to avoid stalling the reply - ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath); - - return ok; - } - catch (Exception e) - { - return new ErrorResponse($"Failed to update script '{relativePath}': {e.Message}"); - } - } - - /// - /// Apply simple text edits specified by line/column ranges. Applies transactionally and validates result. - /// - private const int MaxEditPayloadBytes = 64 * 1024; - - private static object ApplyTextEdits( - string fullPath, - string relativePath, - string name, - JArray edits, - string preconditionSha256, - string refreshModeFromCaller = null, - string validateMode = null) - { - if (!File.Exists(fullPath)) - return new ErrorResponse($"Script not found at '{relativePath}'."); - // Refuse edits if the target or any ancestor is a symlink - try - { - var di = new DirectoryInfo(Path.GetDirectoryName(fullPath) ?? ""); - while (di != null && !string.Equals(di.FullName.Replace('\\', '/'), Application.dataPath.Replace('\\', '/'), StringComparison.OrdinalIgnoreCase)) - { - if (di.Exists && (di.Attributes & FileAttributes.ReparsePoint) != 0) - return new ErrorResponse("Refusing to edit a symlinked script path."); - di = di.Parent; - } - } - catch - { - // If checking attributes fails, proceed without the symlink guard - } - if (edits == null || edits.Count == 0) - return new ErrorResponse("No edits provided."); - - string original; - try { original = File.ReadAllText(fullPath); } - catch (Exception ex) { return new ErrorResponse($"Failed to read script: {ex.Message}"); } - - // Require precondition to avoid drift on large files - string currentSha = ComputeSha256(original); - if (string.IsNullOrEmpty(preconditionSha256)) - return new ErrorResponse("precondition_required", new { status = "precondition_required", current_sha256 = currentSha }); - if (!preconditionSha256.Equals(currentSha, StringComparison.OrdinalIgnoreCase)) - return new ErrorResponse("stale_file", new { status = "stale_file", expected_sha256 = preconditionSha256, current_sha256 = currentSha }); - - // Convert edits to absolute index ranges - var spans = new List<(int start, int end, string text)>(); - long totalBytes = 0; - foreach (var e in edits) - { - try - { - int sl = Math.Max(1, e.Value("startLine")); - int sc = Math.Max(1, e.Value("startCol")); - int el = Math.Max(1, e.Value("endLine")); - int ec = Math.Max(1, e.Value("endCol")); - string newText = e.Value("newText") ?? string.Empty; - - if (!TryIndexFromLineCol(original, sl, sc, out int sidx)) - return new ErrorResponse($"apply_text_edits: start out of range (line {sl}, col {sc})"); - if (!TryIndexFromLineCol(original, el, ec, out int eidx)) - return new ErrorResponse($"apply_text_edits: end out of range (line {el}, col {ec})"); - if (eidx < sidx) (sidx, eidx) = (eidx, sidx); - - spans.Add((sidx, eidx, newText)); - checked - { - totalBytes += System.Text.Encoding.UTF8.GetByteCount(newText); - } - } - catch (Exception ex) - { - return new ErrorResponse($"Invalid edit payload: {ex.Message}"); - } - } - - // Header guard: refuse edits that touch before the first 'using ' directive (after optional BOM) to prevent file corruption - int headerBoundary = (original.Length > 0 && original[0] == '\uFEFF') ? 1 : 0; // skip BOM once if present - // Find first top-level using (supports alias, static, and dotted namespaces) - var mUsing = System.Text.RegularExpressions.Regex.Match( - original, - @"(?m)^\s*using\s+(?:static\s+)?(?:[A-Za-z_]\w*\s*=\s*)?[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\s*;", - System.Text.RegularExpressions.RegexOptions.CultureInvariant, - TimeSpan.FromSeconds(2) - ); - if (mUsing.Success) - { - headerBoundary = Math.Min(Math.Max(headerBoundary, mUsing.Index), original.Length); - } - foreach (var sp in spans) - { - if (sp.start < headerBoundary) - { - return new ErrorResponse("using_guard", new { status = "using_guard", hint = "Refusing to edit before the first 'using'. Use anchor_insert near a method or a structured edit." }); - } - } - - // Attempt auto-upgrade: if a single edit targets a method header/body, re-route as structured replace_method - if (spans.Count == 1) - { - var sp = spans[0]; - // Heuristic: around the start of the edit, try to match a method header in original - int searchStart = Math.Max(0, sp.start - 200); - int searchEnd = Math.Min(original.Length, sp.start + 200); - string slice = original.Substring(searchStart, searchEnd - searchStart); - var rx = new System.Text.RegularExpressions.Regex(@"(?m)^[\t ]*(?:\[[^\]]+\][\t ]*)*[\t ]*(?:public|private|protected|internal|static|virtual|override|sealed|async|extern|unsafe|new|partial)[\s\S]*?\b([A-Za-z_][A-Za-z0-9_]*)\s*\("); - var mh = rx.Match(slice); - if (mh.Success) - { - string methodName = mh.Groups[1].Value; - // Find class span containing the edit - if (TryComputeClassSpan(original, name, null, out var clsStart, out var clsLen, out _)) - { - if (TryComputeMethodSpan(original, clsStart, clsLen, methodName, null, null, null, out var mStart, out var mLen, out _)) - { - // If the edit overlaps the method span significantly, treat as replace_method - if (sp.start <= mStart + 2 && sp.end >= mStart + 1) - { - var structEdits = new JArray(); - - // Apply the edit to get a candidate string, then recompute method span on the edited text - string candidate = original.Remove(sp.start, sp.end - sp.start).Insert(sp.start, sp.text ?? string.Empty); - string replacementText; - if (TryComputeClassSpan(candidate, name, null, out var cls2Start, out var cls2Len, out _) - && TryComputeMethodSpan(candidate, cls2Start, cls2Len, methodName, null, null, null, out var m2Start, out var m2Len, out _)) - { - replacementText = candidate.Substring(m2Start, m2Len); - } - else - { - // Fallback: adjust method start by the net delta if the edit was before the method - int delta = (sp.text?.Length ?? 0) - (sp.end - sp.start); - int adjustedStart = mStart + (sp.start <= mStart ? delta : 0); - adjustedStart = Math.Max(0, Math.Min(adjustedStart, candidate.Length)); - - // If the edit was within the original method span, adjust the length by the delta within-method - int withinMethodDelta = 0; - if (sp.start >= mStart && sp.start <= mStart + mLen) - { - withinMethodDelta = delta; - } - int adjustedLen = mLen + withinMethodDelta; - adjustedLen = Math.Max(0, Math.Min(candidate.Length - adjustedStart, adjustedLen)); - replacementText = candidate.Substring(adjustedStart, adjustedLen); - } - - var op = new JObject - { - ["mode"] = "replace_method", - ["className"] = name, - ["methodName"] = methodName, - ["replacement"] = replacementText - }; - structEdits.Add(op); - // Reuse structured path - return EditScript(fullPath, relativePath, name, structEdits, new JObject { ["refresh"] = "immediate", ["validate"] = "standard" }); - } - } - } - } - } - - if (totalBytes > MaxEditPayloadBytes) - { - return new ErrorResponse("too_large", new { status = "too_large", limitBytes = MaxEditPayloadBytes, hint = "split into smaller edits" }); - } - - // Ensure non-overlap and apply from back to front - spans = spans.OrderByDescending(t => t.start).ToList(); - for (int i = 1; i < spans.Count; i++) - { - if (spans[i].end > spans[i - 1].start) - { - var conflict = new[] { new { startA = spans[i].start, endA = spans[i].end, startB = spans[i - 1].start, endB = spans[i - 1].end } }; - return new ErrorResponse("overlap", new { status = "overlap", conflicts = conflict, hint = "Sort ranges descending by start and compute from the same snapshot." }); - } - } - - string working = original; - bool relaxed = string.Equals(validateMode, "relaxed", StringComparison.OrdinalIgnoreCase); - bool syntaxOnly = string.Equals(validateMode, "syntax", StringComparison.OrdinalIgnoreCase); - foreach (var sp in spans) - { - string next = working.Remove(sp.start, sp.end - sp.start).Insert(sp.start, sp.text ?? string.Empty); - if (relaxed) - { - // Scoped balance check: validate just around the changed region to avoid false positives - int originalLength = sp.end - sp.start; - int newLength = sp.text?.Length ?? 0; - int endPos = sp.start + newLength; - if (!CheckScopedBalance(next, Math.Max(0, sp.start - 500), Math.Min(next.Length, endPos + 500))) - { - return new ErrorResponse("unbalanced_braces", new { status = "unbalanced_braces", line = 0, expected = "{}()[] (scoped)", hint = "Use standard validation or shrink the edit range." }); - } - } - working = next; - } - - // No-op guard: if resulting text is identical, avoid writes and return explicit no-op - if (string.Equals(working, original, StringComparison.Ordinal)) - { - string noChangeSha = ComputeSha256(original); - return new SuccessResponse( - $"No-op: contents unchanged for '{relativePath}'.", - new - { - uri = $"mcpforunity://path/{relativePath}", - path = relativePath, - editsApplied = 0, - no_op = true, - sha256 = noChangeSha, - evidence = new { reason = "identical_content" } - } - ); - } - - // Always check final structural balance regardless of relaxed mode - if (!CheckBalancedDelimiters(working, out int line, out char expected)) - { - int startLine = Math.Max(1, line - 5); - int endLine = line + 5; - string hint = $"unbalanced_braces at line {line}. Call resources/read for lines {startLine}-{endLine} and resend a smaller apply_text_edits that restores balance."; - return new ErrorResponse(hint, new { status = "unbalanced_braces", line, expected = expected.ToString(), evidenceWindow = new { startLine, endLine } }); - } - -#if USE_ROSLYN - if (!syntaxOnly) - { - var tree = CSharpSyntaxTree.ParseText(working); - var diagnostics = tree.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).Take(3) - .Select(d => new { - line = d.Location.GetLineSpan().StartLinePosition.Line + 1, - col = d.Location.GetLineSpan().StartLinePosition.Character + 1, - code = d.Id, - message = d.GetMessage() - }).ToArray(); - if (diagnostics.Length > 0) - { - int firstLine = diagnostics[0].line; - int startLineRos = Math.Max(1, firstLine - 5); - int endLineRos = firstLine + 5; - return new ErrorResponse("syntax_error", new { status = "syntax_error", diagnostics, evidenceWindow = new { startLine = startLineRos, endLine = endLineRos } }); - } - - // Optional formatting - try - { - var root = tree.GetRoot(); - var workspace = new AdhocWorkspace(); - root = Microsoft.CodeAnalysis.Formatting.Formatter.Format(root, workspace); - working = root.ToFullString(); - } - catch { } - } -#endif - - string newSha = ComputeSha256(working); - - // Atomic write and schedule refresh - try - { - var enc = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - var tmp = fullPath + ".tmp"; - File.WriteAllText(tmp, working, enc); - string backup = fullPath + ".bak"; - try - { - File.Replace(tmp, fullPath, backup); - try { if (File.Exists(backup)) File.Delete(backup); } catch { /* ignore */ } - } - catch (PlatformNotSupportedException) - { - File.Copy(tmp, fullPath, true); - try { File.Delete(tmp); } catch { } - try { if (File.Exists(backup)) File.Delete(backup); } catch { } - } - catch (IOException) - { - File.Copy(tmp, fullPath, true); - try { File.Delete(tmp); } catch { } - try { if (File.Exists(backup)) File.Delete(backup); } catch { } - } - - // Respect refresh mode: immediate vs debounced - bool immediate = string.Equals(refreshModeFromCaller, "immediate", StringComparison.OrdinalIgnoreCase) || - string.Equals(refreshModeFromCaller, "sync", StringComparison.OrdinalIgnoreCase); - if (immediate) - { - McpLog.Info($"[ManageScript] ApplyTextEdits: immediate refresh for '{relativePath}'"); - AssetDatabase.ImportAsset( - relativePath, - ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate - ); -#if UNITY_EDITOR - UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); -#endif - } - else - { - McpLog.Info($"[ManageScript] ApplyTextEdits: debounced refresh scheduled for '{relativePath}'"); - ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath); - } - - return new SuccessResponse( - $"Applied {spans.Count} text edit(s) to '{relativePath}'.", - new - { - uri = $"mcpforunity://path/{relativePath}", - path = relativePath, - editsApplied = spans.Count, - sha256 = newSha, - scheduledRefresh = !immediate - } - ); - } - catch (Exception ex) - { - return new ErrorResponse($"Failed to write edits: {ex.Message}"); - } - } - - private static bool TryIndexFromLineCol(string text, int line1, int col1, out int index) - { - // 1-based line/col to absolute index (0-based), col positions are counted in code points - int line = 1, col = 1; - for (int i = 0; i <= text.Length; i++) - { - if (line == line1 && col == col1) - { - index = i; - return true; - } - if (i == text.Length) break; - char c = text[i]; - if (c == '\r') - { - // Treat CRLF as a single newline; skip the LF if present - if (i + 1 < text.Length && text[i + 1] == '\n') - i++; - line++; - col = 1; - } - else if (c == '\n') - { - line++; - col = 1; - } - else - { - col++; - } - } - index = -1; - return false; - } - - private static string ComputeSha256(string contents) - { - using (var sha = SHA256.Create()) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(contents); - var hash = sha.ComputeHash(bytes); - return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); - } - } - - private static bool CheckBalancedDelimiters(string text, out int line, out char expected) - { - var braceStack = new Stack(); - var parenStack = new Stack(); - var bracketStack = new Stack(); - bool inString = false, inChar = false, inSingle = false, inMulti = false, escape = false; - line = 1; expected = '\0'; - - for (int i = 0; i < text.Length; i++) - { - char c = text[i]; - char next = i + 1 < text.Length ? text[i + 1] : '\0'; - - if (c == '\n') { line++; if (inSingle) inSingle = false; } - - if (escape) { escape = false; continue; } - - if (inString) - { - if (c == '\\') { escape = true; } - else if (c == '"') inString = false; - continue; - } - if (inChar) - { - if (c == '\\') { escape = true; } - else if (c == '\'') inChar = false; - continue; - } - if (inSingle) continue; - if (inMulti) - { - if (c == '*' && next == '/') { inMulti = false; i++; } - continue; - } - - if (c == '"') { inString = true; continue; } - if (c == '\'') { inChar = true; continue; } - if (c == '/' && next == '/') { inSingle = true; i++; continue; } - if (c == '/' && next == '*') { inMulti = true; i++; continue; } - - switch (c) - { - case '{': braceStack.Push(line); break; - case '}': - if (braceStack.Count == 0) { expected = '{'; return false; } - braceStack.Pop(); - break; - case '(': parenStack.Push(line); break; - case ')': - if (parenStack.Count == 0) { expected = '('; return false; } - parenStack.Pop(); - break; - case '[': bracketStack.Push(line); break; - case ']': - if (bracketStack.Count == 0) { expected = '['; return false; } - bracketStack.Pop(); - break; - } - } - - if (braceStack.Count > 0) { line = braceStack.Peek(); expected = '}'; return false; } - if (parenStack.Count > 0) { line = parenStack.Peek(); expected = ')'; return false; } - if (bracketStack.Count > 0) { line = bracketStack.Peek(); expected = ']'; return false; } - - return true; - } - - // Lightweight scoped balance: checks delimiters within a substring, ignoring outer context - private static bool CheckScopedBalance(string text, int start, int end) - { - start = Math.Max(0, Math.Min(text.Length, start)); - end = Math.Max(start, Math.Min(text.Length, end)); - int brace = 0, paren = 0, bracket = 0; - bool inStr = false, inChr = false, esc = false; - for (int i = start; i < end; i++) - { - char c = text[i]; - char n = (i + 1 < end) ? text[i + 1] : '\0'; - if (inStr) - { - if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; - } - if (inChr) - { - if (!esc && c == '\'') inChr = false; esc = (!esc && c == '\\'); continue; - } - if (c == '"') { inStr = true; esc = false; continue; } - if (c == '\'') { inChr = true; esc = false; continue; } - if (c == '/' && n == '/') { while (i < end && text[i] != '\n') i++; continue; } - if (c == '/' && n == '*') { i += 2; while (i + 1 < end && !(text[i] == '*' && text[i + 1] == '/')) i++; i++; continue; } - if (c == '{') brace++; - else if (c == '}') brace--; - else if (c == '(') paren++; - else if (c == ')') paren--; - else if (c == '[') bracket++; else if (c == ']') bracket--; - // Allow temporary negative balance - will check tolerance at end - } - return brace >= -3 && paren >= -3 && bracket >= -3; // tolerate more context from outside region - } - - private static object DeleteScript(string fullPath, string relativePath) - { - if (!File.Exists(fullPath)) - { - return new ErrorResponse($"Script not found at '{relativePath}'. Cannot delete."); - } - - try - { - // Use AssetDatabase.MoveAssetToTrash for safer deletion (allows undo) - bool deleted = AssetDatabase.MoveAssetToTrash(relativePath); - if (deleted) - { - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); - return new SuccessResponse( - $"Script '{Path.GetFileName(relativePath)}' moved to trash successfully.", - new { deleted = true } - ); - } - else - { - // Fallback or error if MoveAssetToTrash fails - return new ErrorResponse( - $"Failed to move script '{relativePath}' to trash. It might be locked or in use." - ); - } - } - catch (Exception e) - { - return new ErrorResponse($"Error deleting script '{relativePath}': {e.Message}"); - } - } - - /// - /// Structured edits (AST-backed where available) on existing scripts. - /// Supports class-level replace/delete with Roslyn span computation if USE_ROSLYN is defined, - /// otherwise falls back to a conservative balanced-brace scan. - /// - private static object EditScript( - string fullPath, - string relativePath, - string name, - JArray edits, - JObject options) - { - if (!File.Exists(fullPath)) - return new ErrorResponse($"Script not found at '{relativePath}'."); - // Refuse edits if the target is a symlink - try - { - var attrs = File.GetAttributes(fullPath); - if ((attrs & FileAttributes.ReparsePoint) != 0) - return new ErrorResponse("Refusing to edit a symlinked script path."); - } - catch - { - // ignore failures checking attributes and proceed - } - if (edits == null || edits.Count == 0) - return new ErrorResponse("No edits provided."); - - string original; - try { original = File.ReadAllText(fullPath); } - catch (Exception ex) { return new ErrorResponse($"Failed to read script: {ex.Message}"); } - - string working = original; - - try - { - var replacements = new List<(int start, int length, string text)>(); - int appliedCount = 0; - - // Apply mode: atomic (default) computes all spans against original and applies together. - // Sequential applies each edit immediately to the current working text (useful for dependent edits). - string applyMode = options?["applyMode"]?.ToString()?.ToLowerInvariant(); - bool applySequentially = applyMode == "sequential"; - - foreach (var e in edits) - { - var op = (JObject)e; - var mode = (op.Value("mode") ?? op.Value("op") ?? string.Empty).ToLowerInvariant(); - - switch (mode) - { - case "replace_class": - { - string className = op.Value("className"); - string ns = op.Value("namespace"); - string replacement = ExtractReplacement(op); - - if (string.IsNullOrWhiteSpace(className)) - return new ErrorResponse("replace_class requires 'className'."); - if (replacement == null) - return new ErrorResponse("replace_class requires 'replacement' (inline or base64)."); - - if (!TryComputeClassSpan(working, className, ns, out var spanStart, out var spanLength, out var why)) - return new ErrorResponse($"replace_class failed: {why}"); - - if (!ValidateClassSnippet(replacement, className, out var vErr)) - return new ErrorResponse($"Replacement snippet invalid: {vErr}"); - - if (applySequentially) - { - working = working.Remove(spanStart, spanLength).Insert(spanStart, NormalizeNewlines(replacement)); - appliedCount++; - } - else - { - replacements.Add((spanStart, spanLength, NormalizeNewlines(replacement))); - } - break; - } - - case "delete_class": - { - string className = op.Value("className"); - string ns = op.Value("namespace"); - if (string.IsNullOrWhiteSpace(className)) - return new ErrorResponse("delete_class requires 'className'."); - - if (!TryComputeClassSpan(working, className, ns, out var s, out var l, out var why)) - return new ErrorResponse($"delete_class failed: {why}"); - - if (applySequentially) - { - working = working.Remove(s, l); - appliedCount++; - } - else - { - replacements.Add((s, l, string.Empty)); - } - break; - } - - case "replace_method": - { - string className = op.Value("className"); - string ns = op.Value("namespace"); - string methodName = op.Value("methodName"); - string replacement = ExtractReplacement(op); - string returnType = op.Value("returnType"); - string parametersSignature = op.Value("parametersSignature"); - string attributesContains = op.Value("attributesContains"); - - if (string.IsNullOrWhiteSpace(className)) return new ErrorResponse("replace_method requires 'className'."); - if (string.IsNullOrWhiteSpace(methodName)) return new ErrorResponse("replace_method requires 'methodName'."); - if (replacement == null) return new ErrorResponse("replace_method requires 'replacement' (inline or base64)."); - - if (!TryComputeClassSpan(working, className, ns, out var clsStart, out var clsLen, out var whyClass)) - return new ErrorResponse($"replace_method failed to locate class: {whyClass}"); - - if (!TryComputeMethodSpan(working, clsStart, clsLen, methodName, returnType, parametersSignature, attributesContains, out var mStart, out var mLen, out var whyMethod)) - { - bool hasDependentInsert = edits.Any(j => j is JObject jo && - string.Equals(jo.Value("className"), className, StringComparison.Ordinal) && - string.Equals(jo.Value("methodName"), methodName, StringComparison.Ordinal) && - ((jo.Value("mode") ?? jo.Value("op") ?? string.Empty).ToLowerInvariant() == "insert_method")); - string hint = hasDependentInsert && !applySequentially ? " Hint: This batch inserts this method. Use options.applyMode='sequential' or split into separate calls." : string.Empty; - return new ErrorResponse($"replace_method failed: {whyMethod}.{hint}"); - } - - if (applySequentially) - { - working = working.Remove(mStart, mLen).Insert(mStart, NormalizeNewlines(replacement)); - appliedCount++; - } - else - { - replacements.Add((mStart, mLen, NormalizeNewlines(replacement))); - } - break; - } - - case "delete_method": - { - string className = op.Value("className"); - string ns = op.Value("namespace"); - string methodName = op.Value("methodName"); - string returnType = op.Value("returnType"); - string parametersSignature = op.Value("parametersSignature"); - string attributesContains = op.Value("attributesContains"); - - if (string.IsNullOrWhiteSpace(className)) return new ErrorResponse("delete_method requires 'className'."); - if (string.IsNullOrWhiteSpace(methodName)) return new ErrorResponse("delete_method requires 'methodName'."); - - if (!TryComputeClassSpan(working, className, ns, out var clsStart, out var clsLen, out var whyClass)) - return new ErrorResponse($"delete_method failed to locate class: {whyClass}"); - - if (!TryComputeMethodSpan(working, clsStart, clsLen, methodName, returnType, parametersSignature, attributesContains, out var mStart, out var mLen, out var whyMethod)) - { - bool hasDependentInsert = edits.Any(j => j is JObject jo && - string.Equals(jo.Value("className"), className, StringComparison.Ordinal) && - string.Equals(jo.Value("methodName"), methodName, StringComparison.Ordinal) && - ((jo.Value("mode") ?? jo.Value("op") ?? string.Empty).ToLowerInvariant() == "insert_method")); - string hint = hasDependentInsert && !applySequentially ? " Hint: This batch inserts this method. Use options.applyMode='sequential' or split into separate calls." : string.Empty; - return new ErrorResponse($"delete_method failed: {whyMethod}.{hint}"); - } - - if (applySequentially) - { - working = working.Remove(mStart, mLen); - appliedCount++; - } - else - { - replacements.Add((mStart, mLen, string.Empty)); - } - break; - } - - case "insert_method": - { - string className = op.Value("className"); - string ns = op.Value("namespace"); - string position = (op.Value("position") ?? "end").ToLowerInvariant(); - string afterMethodName = op.Value("afterMethodName"); - string afterReturnType = op.Value("afterReturnType"); - string afterParameters = op.Value("afterParametersSignature"); - string afterAttributesContains = op.Value("afterAttributesContains"); - string snippet = ExtractReplacement(op); - // Harden: refuse empty replacement for inserts - if (snippet == null || snippet.Trim().Length == 0) - return new ErrorResponse("insert_method requires a non-empty 'replacement' text."); - - if (string.IsNullOrWhiteSpace(className)) return new ErrorResponse("insert_method requires 'className'."); - if (snippet == null) return new ErrorResponse("insert_method requires 'replacement' (inline or base64) containing a full method declaration."); - - if (!TryComputeClassSpan(working, className, ns, out var clsStart, out var clsLen, out var whyClass)) - return new ErrorResponse($"insert_method failed to locate class: {whyClass}"); - - if (position == "after") - { - if (string.IsNullOrEmpty(afterMethodName)) return new ErrorResponse("insert_method with position='after' requires 'afterMethodName'."); - if (!TryComputeMethodSpan(working, clsStart, clsLen, afterMethodName, afterReturnType, afterParameters, afterAttributesContains, out var aStart, out var aLen, out var whyAfter)) - return new ErrorResponse($"insert_method(after) failed to locate anchor method: {whyAfter}"); - int insAt = aStart + aLen; - string text = NormalizeNewlines("\n\n" + snippet.TrimEnd() + "\n"); - if (applySequentially) - { - working = working.Insert(insAt, text); - appliedCount++; - } - else - { - replacements.Add((insAt, 0, text)); - } - } - else if (!TryFindClassInsertionPoint(working, clsStart, clsLen, position, out var insAt, out var whyIns)) - return new ErrorResponse($"insert_method failed: {whyIns}"); - else - { - string text = NormalizeNewlines("\n\n" + snippet.TrimEnd() + "\n"); - if (applySequentially) - { - working = working.Insert(insAt, text); - appliedCount++; - } - else - { - replacements.Add((insAt, 0, text)); - } - } - break; - } - - case "anchor_insert": - { - string anchor = op.Value("anchor"); - string position = (op.Value("position") ?? "before").ToLowerInvariant(); - string text = op.Value("text") ?? ExtractReplacement(op); - if (string.IsNullOrWhiteSpace(anchor)) return new ErrorResponse("anchor_insert requires 'anchor' (regex)."); - if (string.IsNullOrEmpty(text)) return new ErrorResponse("anchor_insert requires non-empty 'text'."); - - try - { - var rx = new Regex(anchor, RegexOptions.Multiline, TimeSpan.FromSeconds(2)); - var m = rx.Match(working); - if (!m.Success) return new ErrorResponse($"anchor_insert: anchor not found: {anchor}"); - int insAt = position == "after" ? m.Index + m.Length : m.Index; - string norm = NormalizeNewlines(text); - if (!norm.EndsWith("\n")) - { - norm += "\n"; - } - - // Duplicate guard: if identical snippet already exists within this class, skip insert - if (TryComputeClassSpan(working, name, null, out var clsStartDG, out var clsLenDG, out _)) - { - string classSlice = working.Substring(clsStartDG, Math.Min(clsLenDG, working.Length - clsStartDG)); - if (classSlice.IndexOf(norm, StringComparison.Ordinal) >= 0) - { - // Do not insert duplicate; treat as no-op - break; - } - } - if (applySequentially) - { - working = working.Insert(insAt, norm); - appliedCount++; - } - else - { - replacements.Add((insAt, 0, norm)); - } - } - catch (Exception ex) - { - return new ErrorResponse($"anchor_insert failed: {ex.Message}"); - } - break; - } - - case "anchor_delete": - { - string anchor = op.Value("anchor"); - if (string.IsNullOrWhiteSpace(anchor)) return new ErrorResponse("anchor_delete requires 'anchor' (regex)."); - try - { - var rx = new Regex(anchor, RegexOptions.Multiline, TimeSpan.FromSeconds(2)); - var m = rx.Match(working); - if (!m.Success) return new ErrorResponse($"anchor_delete: anchor not found: {anchor}"); - int delAt = m.Index; - int delLen = m.Length; - if (applySequentially) - { - working = working.Remove(delAt, delLen); - appliedCount++; - } - else - { - replacements.Add((delAt, delLen, string.Empty)); - } - } - catch (Exception ex) - { - return new ErrorResponse($"anchor_delete failed: {ex.Message}"); - } - break; - } - - case "anchor_replace": - { - string anchor = op.Value("anchor"); - string replacement = op.Value("text") ?? op.Value("replacement") ?? ExtractReplacement(op) ?? string.Empty; - if (string.IsNullOrWhiteSpace(anchor)) return new ErrorResponse("anchor_replace requires 'anchor' (regex)."); - try - { - var rx = new Regex(anchor, RegexOptions.Multiline, TimeSpan.FromSeconds(2)); - var m = rx.Match(working); - if (!m.Success) return new ErrorResponse($"anchor_replace: anchor not found: {anchor}"); - int at = m.Index; - int len = m.Length; - string norm = NormalizeNewlines(replacement); - if (applySequentially) - { - working = working.Remove(at, len).Insert(at, norm); - appliedCount++; - } - else - { - replacements.Add((at, len, norm)); - } - } - catch (Exception ex) - { - return new ErrorResponse($"anchor_replace failed: {ex.Message}"); - } - break; - } - - default: - return new ErrorResponse($"Unknown edit mode: '{mode}'. Allowed: replace_class, delete_class, replace_method, delete_method, insert_method, anchor_insert, anchor_delete, anchor_replace."); - } - } - - if (!applySequentially) - { - if (HasOverlaps(replacements)) - { - var ordered = replacements.OrderByDescending(r => r.start).ToList(); - for (int i = 1; i < ordered.Count; i++) - { - if (ordered[i].start + ordered[i].length > ordered[i - 1].start) - { - var conflict = new[] { new { startA = ordered[i].start, endA = ordered[i].start + ordered[i].length, startB = ordered[i - 1].start, endB = ordered[i - 1].start + ordered[i - 1].length } }; - return new ErrorResponse("overlap", new { status = "overlap", conflicts = conflict, hint = "Sort ranges descending by start and compute from the same snapshot." }); - } - } - return new ErrorResponse("overlap", new { status = "overlap" }); - } - - foreach (var r in replacements.OrderByDescending(r => r.start)) - working = working.Remove(r.start, r.length).Insert(r.start, r.text); - appliedCount = replacements.Count; - } - - // Guard against structural imbalance before validation - if (!CheckBalancedDelimiters(working, out int lineBal, out char expectedBal)) - return new ErrorResponse("unbalanced_braces", new { status = "unbalanced_braces", line = lineBal, expected = expectedBal.ToString() }); - - // No-op guard for structured edits: if text unchanged, return explicit no-op - if (string.Equals(working, original, StringComparison.Ordinal)) - { - var sameSha = ComputeSha256(original); - return new SuccessResponse( - $"No-op: contents unchanged for '{relativePath}'.", - new - { - path = relativePath, - uri = $"mcpforunity://path/{relativePath}", - editsApplied = 0, - no_op = true, - sha256 = sameSha, - evidence = new { reason = "identical_content" } - } - ); - } - - // Validate result using override from options if provided; otherwise GUI strictness - var level = GetValidationLevelFromGUI(); - try - { - var validateOpt = options?["validate"]?.ToString()?.ToLowerInvariant(); - if (!string.IsNullOrEmpty(validateOpt)) - { - level = validateOpt switch - { - "basic" => ValidationLevel.Basic, - "standard" => ValidationLevel.Standard, - "comprehensive" => ValidationLevel.Comprehensive, - "strict" => ValidationLevel.Strict, - _ => level - }; - } - } - catch { /* ignore option parsing issues */ } - if (!ValidateScriptSyntax(working, level, out var errors)) - return new ErrorResponse("validation_failed", new { status = "validation_failed", diagnostics = errors ?? Array.Empty() }); - else if (errors != null && errors.Length > 0) - McpLog.Warn($"Script validation warnings for {name}:\n" + string.Join("\n", errors)); - - // Atomic write with backup; schedule refresh - // Decide refresh behavior - string refreshMode = options?["refresh"]?.ToString()?.ToLowerInvariant(); - bool immediate = refreshMode == "immediate" || refreshMode == "sync"; - - // Persist changes atomically (no BOM), then compute/return new file SHA - var enc = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - var tmp = fullPath + ".tmp"; - File.WriteAllText(tmp, working, enc); - var backup = fullPath + ".bak"; - try - { - File.Replace(tmp, fullPath, backup); - try { if (File.Exists(backup)) File.Delete(backup); } catch { } - } - catch (PlatformNotSupportedException) - { - File.Copy(tmp, fullPath, true); - try { File.Delete(tmp); } catch { } - try { if (File.Exists(backup)) File.Delete(backup); } catch { } - } - catch (IOException) - { - File.Copy(tmp, fullPath, true); - try { File.Delete(tmp); } catch { } - try { if (File.Exists(backup)) File.Delete(backup); } catch { } - } - - var newSha = ComputeSha256(working); - var ok = new SuccessResponse( - $"Applied {appliedCount} structured edit(s) to '{relativePath}'.", - new - { - path = relativePath, - uri = $"mcpforunity://path/{relativePath}", - editsApplied = appliedCount, - scheduledRefresh = !immediate, - sha256 = newSha - } - ); - - if (immediate) - { - McpLog.Info($"[ManageScript] EditScript: immediate refresh for '{relativePath}'", always: false); - ManageScriptRefreshHelpers.ImportAndRequestCompile(relativePath); - } - else - { - ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath); - } - return ok; - } - catch (Exception ex) - { - return new ErrorResponse($"Edit failed: {ex.Message}"); - } - } - - private static bool HasOverlaps(IEnumerable<(int start, int length, string text)> list) - { - var arr = list.OrderBy(x => x.start).ToArray(); - for (int i = 1; i < arr.Length; i++) - { - if (arr[i - 1].start + arr[i - 1].length > arr[i].start) - return true; - } - return false; - } - - private static string ExtractReplacement(JObject op) - { - var inline = op.Value("replacement"); - if (!string.IsNullOrEmpty(inline)) return inline; - - var b64 = op.Value("replacementBase64"); - if (!string.IsNullOrEmpty(b64)) - { - try { return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(b64)); } - catch { return null; } - } - return null; - } - - private static string NormalizeNewlines(string t) - { - if (string.IsNullOrEmpty(t)) return t; - return t.Replace("\r\n", "\n").Replace("\r", "\n"); - } - - private static bool ValidateClassSnippet(string snippet, string expectedName, out string err) - { -#if USE_ROSLYN - try - { - var tree = CSharpSyntaxTree.ParseText(snippet); - var root = tree.GetRoot(); - var classes = root.DescendantNodes().OfType().ToList(); - if (classes.Count != 1) { err = "snippet must contain exactly one class declaration"; return false; } - // Optional: enforce expected name - // if (classes[0].Identifier.ValueText != expectedName) { err = $"snippet declares '{classes[0].Identifier.ValueText}', expected '{expectedName}'"; return false; } - err = null; return true; - } - catch (Exception ex) { err = ex.Message; return false; } -#else - if (string.IsNullOrWhiteSpace(snippet) || !snippet.Contains("class ")) { err = "no 'class' keyword found in snippet"; return false; } - err = null; return true; -#endif - } - - private static bool TryComputeClassSpan(string source, string className, string ns, out int start, out int length, out string why) - { -#if USE_ROSLYN - try - { - var tree = CSharpSyntaxTree.ParseText(source); - var root = tree.GetRoot(); - var classes = root.DescendantNodes() - .OfType() - .Where(c => c.Identifier.ValueText == className); - - if (!string.IsNullOrEmpty(ns)) - { - classes = classes.Where(c => - (c.FirstAncestorOrSelf()?.Name?.ToString() ?? "") == ns - || (c.FirstAncestorOrSelf()?.Name?.ToString() ?? "") == ns); - } - - var list = classes.ToList(); - if (list.Count == 0) { start = length = 0; why = $"class '{className}' not found" + (ns != null ? $" in namespace '{ns}'" : ""); return false; } - if (list.Count > 1) { start = length = 0; why = $"class '{className}' matched {list.Count} declarations (partial/nested?). Disambiguate."; return false; } - - var cls = list[0]; - var span = cls.FullSpan; // includes attributes & leading trivia - start = span.Start; length = span.Length; why = null; return true; - } - catch - { - // fall back below - } -#endif - return TryComputeClassSpanBalanced(source, className, ns, out start, out length, out why); - } - - private static bool TryComputeClassSpanBalanced(string source, string className, string ns, out int start, out int length, out string why) - { - start = length = 0; why = null; - var idx = IndexOfClassToken(source, className); - if (idx < 0) { why = $"class '{className}' not found (balanced scan)"; return false; } - - if (!string.IsNullOrEmpty(ns) && !AppearsWithinNamespaceHeader(source, idx, ns)) - { why = $"class '{className}' not under namespace '{ns}' (balanced scan)"; return false; } - - // Include modifiers/attributes on the same line: back up to the start of line - int lineStart = idx; - while (lineStart > 0 && source[lineStart - 1] != '\n' && source[lineStart - 1] != '\r') lineStart--; - - int i = idx; - while (i < source.Length && source[i] != '{') i++; - if (i >= source.Length) { why = "no opening brace after class header"; return false; } - - int depth = 0; bool inStr = false, inChar = false, inSL = false, inML = false, esc = false; - int startSpan = lineStart; - for (; i < source.Length; i++) - { - char c = source[i]; - char n = i + 1 < source.Length ? source[i + 1] : '\0'; - - if (inSL) { if (c == '\n') inSL = false; continue; } - if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; } - if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; } - if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; } - - if (c == '/' && n == '/') { inSL = true; i++; continue; } - if (c == '/' && n == '*') { inML = true; i++; continue; } - if (c == '"') { inStr = true; continue; } - if (c == '\'') { inChar = true; continue; } - - if (c == '{') { depth++; } - else if (c == '}') - { - depth--; - if (depth == 0) { start = startSpan; length = (i - startSpan) + 1; return true; } - if (depth < 0) { why = "brace underflow"; return false; } - } - } - why = "unterminated class block"; return false; - } - - private static bool TryComputeMethodSpan( - string source, - int classStart, - int classLength, - string methodName, - string returnType, - string parametersSignature, - string attributesContains, - out int start, - out int length, - out string why) - { - start = length = 0; why = null; - int searchStart = classStart; - int searchEnd = Math.Min(source.Length, classStart + classLength); - - // 1) Find the method header using a stricter regex (allows optional attributes above) - string rtPattern = string.IsNullOrEmpty(returnType) ? @"[^\s]+" : Regex.Escape(returnType).Replace("\\ ", "\\s+"); - string namePattern = Regex.Escape(methodName); - // If a parametersSignature is provided, it may include surrounding parentheses. Strip them so - // we can safely embed the signature inside our own parenthesis group without duplicating. - string paramsPattern; - if (string.IsNullOrEmpty(parametersSignature)) - { - paramsPattern = @"[\s\S]*?"; // permissive when not specified - } - else - { - string ps = parametersSignature.Trim(); - if (ps.StartsWith("(") && ps.EndsWith(")") && ps.Length >= 2) - { - ps = ps.Substring(1, ps.Length - 2); - } - // Escape literal text of the signature - paramsPattern = Regex.Escape(ps); - } - string pattern = - @"(?m)^[\t ]*(?:\[[^\]]+\][\t ]*)*[\t ]*" + - @"(?:(?:public|private|protected|internal|static|virtual|override|sealed|async|extern|unsafe|new|partial|readonly|volatile|event|abstract|ref|in|out)\s+)*" + - rtPattern + @"[\t ]+" + namePattern + @"\s*(?:<[^>]+>)?\s*\(" + paramsPattern + @"\)"; - - string slice = source.Substring(searchStart, searchEnd - searchStart); - var headerMatch = Regex.Match(slice, pattern, RegexOptions.Multiline, TimeSpan.FromSeconds(2)); - if (!headerMatch.Success) - { - why = $"method '{methodName}' header not found in class"; return false; - } - int headerIndex = searchStart + headerMatch.Index; - - // Optional attributes filter: look upward from headerIndex for contiguous attribute lines - if (!string.IsNullOrEmpty(attributesContains)) - { - int attrScanStart = headerIndex; - while (attrScanStart > searchStart) - { - int prevNl = source.LastIndexOf('\n', attrScanStart - 1); - if (prevNl < 0 || prevNl < searchStart) break; - string prevLine = source.Substring(prevNl + 1, attrScanStart - (prevNl + 1)); - if (prevLine.TrimStart().StartsWith("[")) { attrScanStart = prevNl; continue; } - break; - } - string attrBlock = source.Substring(attrScanStart, headerIndex - attrScanStart); - if (attrBlock.IndexOf(attributesContains, StringComparison.Ordinal) < 0) - { - why = $"method '{methodName}' found but attributes filter did not match"; return false; - } - } - - // backtrack to the very start of header/attributes to include in span - int lineStart = headerIndex; - while (lineStart > searchStart && source[lineStart - 1] != '\n' && source[lineStart - 1] != '\r') lineStart--; - // If previous lines are attributes, include them - int attrStart = lineStart; - int probe = lineStart - 1; - while (probe > searchStart) - { - int prevNl = source.LastIndexOf('\n', probe); - if (prevNl < 0 || prevNl < searchStart) break; - string prev = source.Substring(prevNl + 1, attrStart - (prevNl + 1)); - if (prev.TrimStart().StartsWith("[")) { attrStart = prevNl + 1; probe = prevNl - 1; } - else break; - } - - // 2) Walk from the end of signature to detect body style ('{' or '=> ...;') and compute end - // Find the '(' that belongs to the method signature, not attributes - int nameTokenIdx = IndexOfTokenWithin(source, methodName, headerIndex, searchEnd); - if (nameTokenIdx < 0) { why = $"method '{methodName}' token not found after header"; return false; } - int sigOpenParen = IndexOfTokenWithin(source, "(", nameTokenIdx, searchEnd); - if (sigOpenParen < 0) { why = "method parameter list '(' not found"; return false; } - - int i = sigOpenParen; - int parenDepth = 0; bool inStr = false, inChar = false, inSL = false, inML = false, esc = false; - for (; i < searchEnd; i++) - { - char c = source[i]; - char n = i + 1 < searchEnd ? source[i + 1] : '\0'; - if (inSL) { if (c == '\n') inSL = false; continue; } - if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; } - if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; } - if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; } - - if (c == '/' && n == '/') { inSL = true; i++; continue; } - if (c == '/' && n == '*') { inML = true; i++; continue; } - if (c == '"') { inStr = true; continue; } - if (c == '\'') { inChar = true; continue; } - - if (c == '(') parenDepth++; - if (c == ')') { parenDepth--; if (parenDepth == 0) { i++; break; } } - } - - // After params: detect expression-bodied or block-bodied - // Skip whitespace/comments - for (; i < searchEnd; i++) - { - char c = source[i]; - char n = i + 1 < searchEnd ? source[i + 1] : '\0'; - if (char.IsWhiteSpace(c)) continue; - if (c == '/' && n == '/') { while (i < searchEnd && source[i] != '\n') i++; continue; } - if (c == '/' && n == '*') { i += 2; while (i + 1 < searchEnd && !(source[i] == '*' && source[i + 1] == '/')) i++; i++; continue; } - break; - } - - // Tolerate generic constraints between params and body: multiple 'where T : ...' - for (; ; ) - { - // Skip whitespace/comments before checking for 'where' - for (; i < searchEnd; i++) - { - char c = source[i]; - char n = i + 1 < searchEnd ? source[i + 1] : '\0'; - if (char.IsWhiteSpace(c)) continue; - if (c == '/' && n == '/') { while (i < searchEnd && source[i] != '\n') i++; continue; } - if (c == '/' && n == '*') { i += 2; while (i + 1 < searchEnd && !(source[i] == '*' && source[i + 1] == '/')) i++; i++; continue; } - break; - } - - // Check word-boundary 'where' - bool hasWhere = false; - if (i + 5 <= searchEnd) - { - hasWhere = source[i] == 'w' && source[i + 1] == 'h' && source[i + 2] == 'e' && source[i + 3] == 'r' && source[i + 4] == 'e'; - if (hasWhere) - { - // Left boundary - if (i - 1 >= 0) - { - char lb = source[i - 1]; - if (char.IsLetterOrDigit(lb) || lb == '_') hasWhere = false; - } - // Right boundary - if (hasWhere && i + 5 < searchEnd) - { - char rb = source[i + 5]; - if (char.IsLetterOrDigit(rb) || rb == '_') hasWhere = false; - } - } - } - if (!hasWhere) break; - - // Advance past the entire where-constraint clause until we hit '{' or '=>' or ';' - i += 5; // past 'where' - while (i < searchEnd) - { - char c = source[i]; - char n = i + 1 < searchEnd ? source[i + 1] : '\0'; - if (c == '{' || c == ';' || (c == '=' && n == '>')) break; - // Skip comments inline - if (c == '/' && n == '/') { while (i < searchEnd && source[i] != '\n') i++; continue; } - if (c == '/' && n == '*') { i += 2; while (i + 1 < searchEnd && !(source[i] == '*' && source[i + 1] == '/')) i++; i++; continue; } - i++; - } - } - - // Re-check for expression-bodied after constraints - if (i < searchEnd - 1 && source[i] == '=' && source[i + 1] == '>') - { - // expression-bodied method: seek to terminating semicolon - int j = i; - bool done = false; - while (j < searchEnd) - { - char c = source[j]; - if (c == ';') { done = true; break; } - j++; - } - if (!done) { why = "unterminated expression-bodied method"; return false; } - start = attrStart; length = (j - attrStart) + 1; return true; - } - - if (i >= searchEnd || source[i] != '{') { why = "no opening brace after method signature"; return false; } - - int depth = 0; inStr = false; inChar = false; inSL = false; inML = false; esc = false; - int startSpan = attrStart; - for (; i < searchEnd; i++) - { - char c = source[i]; - char n = i + 1 < searchEnd ? source[i + 1] : '\0'; - if (inSL) { if (c == '\n') inSL = false; continue; } - if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; } - if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; } - if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; } - - if (c == '/' && n == '/') { inSL = true; i++; continue; } - if (c == '/' && n == '*') { inML = true; i++; continue; } - if (c == '"') { inStr = true; continue; } - if (c == '\'') { inChar = true; continue; } - - if (c == '{') depth++; - else if (c == '}') - { - depth--; - if (depth == 0) { start = startSpan; length = (i - startSpan) + 1; return true; } - if (depth < 0) { why = "brace underflow in method"; return false; } - } - } - why = "unterminated method block"; return false; - } - - private static int IndexOfTokenWithin(string s, string token, int start, int end) - { - int idx = s.IndexOf(token, start, StringComparison.Ordinal); - return (idx >= 0 && idx < end) ? idx : -1; - } - - private static bool TryFindClassInsertionPoint(string source, int classStart, int classLength, string position, out int insertAt, out string why) - { - insertAt = 0; why = null; - int searchStart = classStart; - int searchEnd = Math.Min(source.Length, classStart + classLength); - - if (position == "start") - { - // find first '{' after class header, insert just after with a newline - int i = IndexOfTokenWithin(source, "{", searchStart, searchEnd); - if (i < 0) { why = "could not find class opening brace"; return false; } - insertAt = i + 1; return true; - } - else // end - { - // walk to matching closing brace of class and insert just before it - int i = IndexOfTokenWithin(source, "{", searchStart, searchEnd); - if (i < 0) { why = "could not find class opening brace"; return false; } - int depth = 0; bool inStr = false, inChar = false, inSL = false, inML = false, esc = false; - for (; i < searchEnd; i++) - { - char c = source[i]; - char n = i + 1 < searchEnd ? source[i + 1] : '\0'; - if (inSL) { if (c == '\n') inSL = false; continue; } - if (inML) { if (c == '*' && n == '/') { inML = false; i++; } continue; } - if (inStr) { if (!esc && c == '"') inStr = false; esc = (!esc && c == '\\'); continue; } - if (inChar) { if (!esc && c == '\'') inChar = false; esc = (!esc && c == '\\'); continue; } - - if (c == '/' && n == '/') { inSL = true; i++; continue; } - if (c == '/' && n == '*') { inML = true; i++; continue; } - if (c == '"') { inStr = true; continue; } - if (c == '\'') { inChar = true; continue; } - - if (c == '{') depth++; - else if (c == '}') - { - depth--; - if (depth == 0) { insertAt = i; return true; } - if (depth < 0) { why = "brace underflow while scanning class"; return false; } - } - } - why = "could not find class closing brace"; return false; - } - } - - private static int IndexOfClassToken(string s, string className) - { - // simple token search; could be tightened with Regex for word boundaries - var pattern = "class " + className; - return s.IndexOf(pattern, StringComparison.Ordinal); - } - - private static bool AppearsWithinNamespaceHeader(string s, int pos, string ns) - { - int from = Math.Max(0, pos - 2000); - var slice = s.Substring(from, pos - from); - return slice.Contains("namespace " + ns); - } - - /// - /// Generates basic C# script content based on name and type. - /// - private static string GenerateDefaultScriptContent( - string name, - string scriptType, - string namespaceName - ) - { - string usingStatements = "using UnityEngine;\nusing System.Collections;\n"; - string classDeclaration; - string body = - "\n // Use this for initialization\n void Start() {\n\n }\n\n // Update is called once per frame\n void Update() {\n\n }\n"; - - string baseClass = ""; - if (!string.IsNullOrEmpty(scriptType)) - { - if (scriptType.Equals("MonoBehaviour", StringComparison.OrdinalIgnoreCase)) - baseClass = " : MonoBehaviour"; - else if (scriptType.Equals("ScriptableObject", StringComparison.OrdinalIgnoreCase)) - { - baseClass = " : ScriptableObject"; - body = ""; // ScriptableObjects don't usually need Start/Update - } - else if ( - scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase) - || scriptType.Equals("EditorWindow", StringComparison.OrdinalIgnoreCase) - ) - { - usingStatements += "using UnityEditor;\n"; - if (scriptType.Equals("Editor", StringComparison.OrdinalIgnoreCase)) - baseClass = " : Editor"; - else - baseClass = " : EditorWindow"; - body = ""; // Editor scripts have different structures - } - // Add more types as needed - } - - classDeclaration = $"public class {name}{baseClass}"; - - string fullContent = $"{usingStatements}\n"; - bool useNamespace = !string.IsNullOrEmpty(namespaceName); - - if (useNamespace) - { - fullContent += $"namespace {namespaceName}\n{{\n"; - // Indent class and body if using namespace - classDeclaration = " " + classDeclaration; - body = string.Join("\n", body.Split('\n').Select(line => " " + line)); - } - - fullContent += $"{classDeclaration}\n{{\n{body}\n}}"; - - if (useNamespace) - { - fullContent += "\n}"; // Close namespace - } - - return fullContent.Trim() + "\n"; // Ensure a trailing newline - } - - /// - /// Gets the validation level from the GUI settings - /// - private static ValidationLevel GetValidationLevelFromGUI() - { - int savedLevel = EditorPrefs.GetInt(EditorPrefKeys.ValidationLevel, (int)ValidationLevel.Standard); - return (ValidationLevel)Mathf.Clamp(savedLevel, 0, 3); - } - - /// - /// Validates C# script syntax using multiple validation layers. - /// - private static bool ValidateScriptSyntax(string contents) - { - return ValidateScriptSyntax(contents, ValidationLevel.Standard, out _); - } - - /// - /// Advanced syntax validation with detailed diagnostics and configurable strictness. - /// - private static bool ValidateScriptSyntax(string contents, ValidationLevel level, out string[] errors) - { - var errorList = new System.Collections.Generic.List(); - errors = null; - - if (string.IsNullOrEmpty(contents)) - { - return true; // Empty content is valid - } - - // Basic structural validation - if (!ValidateBasicStructure(contents, errorList)) - { - errors = errorList.ToArray(); - return false; - } - -#if USE_ROSLYN - // Advanced Roslyn-based validation: only run for Standard+; fail on Roslyn errors - if (level >= ValidationLevel.Standard) - { - if (!ValidateScriptSyntaxRoslyn(contents, level, errorList)) - { - errors = errorList.ToArray(); - return false; - } - } -#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:"))); - } - - /// - /// Validation strictness levels - /// - private enum ValidationLevel - { - Basic, // Only syntax errors - Standard, // Syntax + Unity best practices - Comprehensive, // All checks + semantic analysis - Strict // Treat all issues as errors - } - - /// - /// Validates basic code structure (braces, quotes, comments) - /// - private static bool ValidateBasicStructure(string contents, System.Collections.Generic.List 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 - /// - /// Cached compilation references for performance - /// - private static System.Collections.Generic.List _cachedReferences = null; - private static DateTime _cacheTime = DateTime.MinValue; - private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(5); - - /// - /// Validates syntax using Roslyn compiler services - /// - private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List 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; - } - } - - /// - /// Validates script semantics using full compilation context to catch namespace, type, and method resolution errors - /// - private static bool ValidateScriptSemantics(string contents, System.Collections.Generic.List 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; - } - } - - /// - /// Gets compilation references with caching for performance - /// - private static System.Collections.Generic.List GetCompilationReferences() - { - // Check cache validity - if (_cachedReferences != null && DateTime.Now - _cacheTime < CacheExpiry) - { - return _cachedReferences; - } - - try - { - var references = new System.Collections.Generic.List(); - - // 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) - { - McpLog.Warn($"Could not load UnityEngine assembly: {ex.Message}"); - } - -#if UNITY_EDITOR - try - { - references.Add(MetadataReference.CreateFromFile(typeof(UnityEditor.Editor).Assembly.Location)); // UnityEditor - } - catch (Exception ex) - { - McpLog.Warn($"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) - { - McpLog.Warn($"Could not load Unity project assemblies: {ex.Message}"); - } -#endif - - // Cache the results - _cachedReferences = references; - _cacheTime = DateTime.Now; - - return references; - } - catch (Exception ex) - { - McpLog.Error($"Failed to get compilation references: {ex.Message}"); - return new System.Collections.Generic.List(); - } - } -#else - private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List errors) - { - // Fallback when Roslyn is not available - return true; - } -#endif - - /// - /// Validates Unity-specific coding rules and best practices - /// //TODO: Naive Unity Checks and not really yield any results, need to be improved - /// - private static void ValidateScriptSyntaxUnity(string contents, System.Collections.Generic.List 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"); - } - } - - /// - /// Validates semantic rules and common coding issues - /// - private static void ValidateSemanticRules(string contents, System.Collections.Generic.List 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*[;})\]])", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(2)); - 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*{", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(2)); - 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) - /// - /// Public method to validate script syntax with configurable validation level - /// Returns detailed validation results including errors and warnings - /// - // public static object ValidateScript(JObject @params) - // { - // string contents = @params["contents"]?.ToString(); - // string validationLevel = @params["validationLevel"]?.ToString() ?? "standard"; - - // if (string.IsNullOrEmpty(contents)) - // { - // return new ErrorResponse("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 new ErrorResponse($"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 new SuccessResponse("Script validation completed successfully.", result); - // } - // else - // { - // return new ErrorResponse("Script validation failed.", result); - // } - // } - } - - // Debounced refresh/compile scheduler to coalesce bursts of edits - static class RefreshDebounce - { - private static int _pending; - private static readonly object _lock = new object(); - private static readonly HashSet _paths = new HashSet(StringComparer.OrdinalIgnoreCase); - - // The timestamp of the most recent schedule request. - private static DateTime _lastRequest; - - // Guard to ensure we only have a single ticking callback running. - private static bool _scheduled; - - public static void Schedule(string relPath, TimeSpan window) - { - // Record that work is pending and track the path in a threadsafe way. - Interlocked.Exchange(ref _pending, 1); - lock (_lock) - { - _paths.Add(relPath); - _lastRequest = DateTime.UtcNow; - - // If a debounce timer is already scheduled it will pick up the new request. - if (_scheduled) - return; - - _scheduled = true; - } - - // Kick off a ticking callback that waits until the window has elapsed - // from the last request before performing the refresh. - EditorApplication.delayCall += () => Tick(window); - // Nudge the editor loop so ticks run even if the window is unfocused - EditorApplication.QueuePlayerLoopUpdate(); - } - - private static void Tick(TimeSpan window) - { - bool ready; - lock (_lock) - { - // Only proceed once the debounce window has fully elapsed. - ready = (DateTime.UtcNow - _lastRequest) >= window; - if (ready) - { - _scheduled = false; - } - } - - if (!ready) - { - // Window has not yet elapsed; check again on the next editor tick. - EditorApplication.delayCall += () => Tick(window); - return; - } - - if (Interlocked.Exchange(ref _pending, 0) == 1) - { - string[] toImport; - lock (_lock) { toImport = _paths.ToArray(); _paths.Clear(); } - foreach (var p in toImport) - { - var sp = ManageScriptRefreshHelpers.SanitizeAssetsPath(p); - AssetDatabase.ImportAsset(sp, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport); - } -#if UNITY_EDITOR - UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); -#endif - // Fallback if needed: - // AssetDatabase.Refresh(); - } - } - } - - static class ManageScriptRefreshHelpers - { - public static string SanitizeAssetsPath(string p) - { - if (string.IsNullOrEmpty(p)) return p; - p = AssetPathUtility.NormalizeSeparators(p).Trim(); - if (p.StartsWith("mcpforunity://path/", StringComparison.OrdinalIgnoreCase)) - p = p.Substring("mcpforunity://path/".Length); - while (p.StartsWith("Assets/Assets/", StringComparison.OrdinalIgnoreCase)) - p = p.Substring("Assets/".Length); - if (!p.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) - p = "Assets/" + p.TrimStart('/'); - return p; - } - - public static void ScheduleScriptRefresh(string relPath) - { - var sp = SanitizeAssetsPath(relPath); - RefreshDebounce.Schedule(sp, TimeSpan.FromMilliseconds(200)); - } - - public static void ImportAndRequestCompile(string relPath, bool synchronous = true) - { - var sp = SanitizeAssetsPath(relPath); - var opts = ImportAssetOptions.ForceUpdate; - if (synchronous) opts |= ImportAssetOptions.ForceSynchronousImport; - AssetDatabase.ImportAsset(sp, opts); -#if UNITY_EDITOR - UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); -#endif - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/ManageScript.cs.meta b/Assets/MCPForUnity/Editor/Tools/ManageScript.cs.meta deleted file mode 100644 index ef94cc1..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageScript.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 626d2d44668019a45ae52e9ee066b7ec -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/ManageScript.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/ManageScriptableObject.cs b/Assets/MCPForUnity/Editor/Tools/ManageScriptableObject.cs deleted file mode 100644 index 62d1c19..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageScriptableObject.cs +++ /dev/null @@ -1,1522 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Single tool for ScriptableObject workflows: - /// - action=create: create a ScriptableObject asset (and optionally apply patches) - /// - action=modify: apply serialized property patches to an existing asset - /// - /// Patching is performed via SerializedObject/SerializedProperty paths (Unity-native), not reflection. - /// - [McpForUnityTool("manage_scriptable_object", AutoRegister = false)] - public static class ManageScriptableObject - { - private const string CodeCompilingOrReloading = "compiling_or_reloading"; - private const string CodeInvalidParams = "invalid_params"; - private const string CodeTypeNotFound = "type_not_found"; - private const string CodeInvalidFolderPath = "invalid_folder_path"; - private const string CodeTargetNotFound = "target_not_found"; - private const string CodeAssetCreateFailed = "asset_create_failed"; - - private static readonly HashSet ValidActions = new(StringComparer.OrdinalIgnoreCase) - { - // NOTE: Action strings are normalized by NormalizeAction() (lowercased, '_'/'-' removed), - // so we only need the canonical normalized forms here. - "create", - "createso", - "modify", - "modifyso", - }; - - public static object HandleCommand(JObject @params) - { - if (@params == null) - { - return new ErrorResponse(CodeInvalidParams); - } - - if (EditorApplication.isCompiling || EditorApplication.isUpdating) - { - // Unity is transient; treat as retryable on the client side. - return new ErrorResponse(CodeCompilingOrReloading, new { hint = "retry" }); - } - - // Allow JSON-string parameters for objects/arrays. - JsonUtil.CoerceJsonStringParameter(@params, "target"); - CoerceJsonStringArrayParameter(@params, "patches"); - - string actionRaw = @params["action"]?.ToString(); - if (string.IsNullOrWhiteSpace(actionRaw)) - { - return new ErrorResponse(CodeInvalidParams, new { message = "'action' is required.", validActions = ValidActions.ToArray() }); - } - - string action = NormalizeAction(actionRaw); - if (!ValidActions.Contains(action)) - { - return new ErrorResponse(CodeInvalidParams, new { message = $"Unknown action: '{actionRaw}'.", validActions = ValidActions.ToArray() }); - } - - if (IsCreateAction(action)) - { - return HandleCreate(@params); - } - - return HandleModify(@params); - } - - private static object HandleCreate(JObject @params) - { - string typeName = @params["typeName"]?.ToString() ?? @params["type_name"]?.ToString(); - string folderPath = @params["folderPath"]?.ToString() ?? @params["folder_path"]?.ToString(); - string assetName = @params["assetName"]?.ToString() ?? @params["asset_name"]?.ToString(); - bool overwrite = @params["overwrite"]?.ToObject() ?? false; - - if (string.IsNullOrWhiteSpace(typeName)) - { - return new ErrorResponse(CodeInvalidParams, new { message = "'typeName' is required." }); - } - - if (string.IsNullOrWhiteSpace(folderPath)) - { - return new ErrorResponse(CodeInvalidParams, new { message = "'folderPath' is required." }); - } - - if (string.IsNullOrWhiteSpace(assetName)) - { - return new ErrorResponse(CodeInvalidParams, new { message = "'assetName' is required." }); - } - - if (assetName.Contains("/") || assetName.Contains("\\")) - { - return new ErrorResponse(CodeInvalidParams, new { message = "'assetName' must not contain path separators." }); - } - - if (!TryNormalizeFolderPath(folderPath, out var normalizedFolder, out var folderNormalizeError)) - { - return new ErrorResponse(CodeInvalidFolderPath, new { message = folderNormalizeError, folderPath }); - } - - if (!EnsureFolderExists(normalizedFolder, out var folderError)) - { - return new ErrorResponse(CodeInvalidFolderPath, new { message = folderError, folderPath = normalizedFolder }); - } - - var resolvedType = ResolveType(typeName); - if (resolvedType == null || !typeof(ScriptableObject).IsAssignableFrom(resolvedType)) - { - return new ErrorResponse(CodeTypeNotFound, new { message = $"ScriptableObject type not found: '{typeName}'", typeName }); - } - - string fileName = assetName.EndsWith(".asset", StringComparison.OrdinalIgnoreCase) - ? assetName - : assetName + ".asset"; - string desiredPath = $"{normalizedFolder.TrimEnd('/')}/{fileName}"; - string finalPath = overwrite ? desiredPath : AssetDatabase.GenerateUniqueAssetPath(desiredPath); - - ScriptableObject instance; - try - { - instance = ScriptableObject.CreateInstance(resolvedType); - if (instance == null) - { - return new ErrorResponse(CodeAssetCreateFailed, new { message = "CreateInstance returned null.", typeName = resolvedType.FullName }); - } - } - catch (Exception ex) - { - return new ErrorResponse(CodeAssetCreateFailed, new { message = ex.Message, typeName = resolvedType.FullName }); - } - - // GUID-preserving overwrite logic - bool isNewAsset = true; - try - { - if (overwrite) - { - var existingAsset = AssetDatabase.LoadAssetAtPath(finalPath); - if (existingAsset != null && existingAsset.GetType() == resolvedType) - { - // Preserve GUID by overwriting existing asset data in-place - EditorUtility.CopySerialized(instance, existingAsset); - - // Fix for "Main Object Name does not match filename" warning: - // CopySerialized overwrites the name with the (empty) name of the new instance. - // We must restore the correct name to match the filename. - existingAsset.name = Path.GetFileNameWithoutExtension(finalPath); - - UnityEngine.Object.DestroyImmediate(instance); // Destroy temporary instance - instance = existingAsset; // Proceed with patching the existing asset - isNewAsset = false; - - // Mark dirty to ensure changes are picked up - EditorUtility.SetDirty(instance); - } - else if (existingAsset != null) - { - // Type mismatch or not a ScriptableObject - must delete and recreate to change type, losing GUID - // (Or we could warn, but overwrite usually implies replacing) - AssetDatabase.DeleteAsset(finalPath); - } - } - - if (isNewAsset) - { - // Ensure the new instance has the correct name before creating asset to avoid warnings - instance.name = Path.GetFileNameWithoutExtension(finalPath); - AssetDatabase.CreateAsset(instance, finalPath); - } - } - catch (Exception ex) - { - return new ErrorResponse(CodeAssetCreateFailed, new { message = ex.Message, path = finalPath }); - } - - string guid = AssetDatabase.AssetPathToGUID(finalPath); - var patchesToken = @params["patches"]; - object patchResults = null; - var warnings = new List(); - - if (patchesToken is JArray patches && patches.Count > 0) - { - var patchApply = ApplyPatches(instance, patches); - patchResults = patchApply.results; - warnings.AddRange(patchApply.warnings); - } - - EditorUtility.SetDirty(instance); - AssetDatabase.SaveAssets(); - - return new SuccessResponse( - "ScriptableObject created.", - new - { - guid, - path = finalPath, - typeNameResolved = resolvedType.FullName, - patchResults, - warnings = warnings.Count > 0 ? warnings : null - } - ); - } - - private static object HandleModify(JObject @params) - { - if (!TryResolveTarget(@params["target"], out var target, out var targetPath, out var targetGuid, out var err)) - { - return err; - } - - var patchesToken = @params["patches"]; - if (patchesToken == null || patchesToken.Type == JTokenType.Null) - { - return new ErrorResponse(CodeInvalidParams, new { message = "'patches' is required.", targetPath, targetGuid }); - } - - if (patchesToken is not JArray patches) - { - return new ErrorResponse(CodeInvalidParams, new { message = "'patches' must be an array.", targetPath, targetGuid }); - } - - // Phase 5: Dry-run mode - validate patches without applying - bool dryRun = @params["dryRun"]?.ToObject() ?? @params["dry_run"]?.ToObject() ?? false; - - if (dryRun) - { - var validationResults = ValidatePatches(target, patches); - return new SuccessResponse( - "Dry-run validation complete.", - new - { - targetGuid, - targetPath, - targetTypeName = target.GetType().FullName, - dryRun = true, - valid = validationResults.All(r => (bool)r.GetType().GetProperty("ok")?.GetValue(r)), - validationResults - } - ); - } - - var (results, warnings) = ApplyPatches(target, patches); - - return new SuccessResponse( - "Serialized properties patched.", - new - { - targetGuid, - targetPath, - targetTypeName = target.GetType().FullName, - results, - warnings = warnings.Count > 0 ? warnings : null - } - ); - } - - /// - /// Validates patches without applying them (for dry-run mode). - /// Checks that property paths exist and that value types are compatible. - /// - private static List ValidatePatches(UnityEngine.Object target, JArray patches) - { - var results = new List(patches.Count); - var so = new SerializedObject(target); - so.Update(); - - for (int i = 0; i < patches.Count; i++) - { - if (patches[i] is not JObject patchObj) - { - results.Add(new { index = i, propertyPath = "", op = "", ok = false, message = $"Patch at index {i} must be an object." }); - continue; - } - - string propertyPath = patchObj["propertyPath"]?.ToString() - ?? patchObj["property_path"]?.ToString() - ?? patchObj["path"]?.ToString(); - string op = (patchObj["op"]?.ToString() ?? "set").Trim(); - - if (string.IsNullOrWhiteSpace(propertyPath)) - { - results.Add(new { index = i, propertyPath = propertyPath ?? "", op, ok = false, message = "Missing required field: propertyPath" }); - continue; - } - - // Normalize the path - string normalizedPath = NormalizePropertyPath(propertyPath); - string normalizedOp = op.ToLowerInvariant(); - - // For array_resize, check if the array exists - if (normalizedOp == "array_resize") - { - var valueToken = patchObj["value"]; - if (valueToken == null || valueToken.Type == JTokenType.Null) - { - results.Add(new { index = i, propertyPath = normalizedPath, op, ok = false, message = "array_resize requires integer 'value'." }); - continue; - } - - int size = ParamCoercion.CoerceInt(valueToken, -1); - if (size < 0) - { - results.Add(new { index = i, propertyPath = normalizedPath, op, ok = false, message = "array_resize requires non-negative integer 'value'." }); - continue; - } - - // Check if the array path exists - string arrayPath = normalizedPath; - if (arrayPath.EndsWith(".Array.size", StringComparison.Ordinal)) - { - arrayPath = arrayPath.Substring(0, arrayPath.Length - ".Array.size".Length); - } - - var arrayProp = so.FindProperty(arrayPath); - if (arrayProp == null) - { - results.Add(new { index = i, propertyPath = normalizedPath, op, ok = false, message = $"Array not found: {arrayPath}" }); - continue; - } - - if (!arrayProp.isArray) - { - results.Add(new { index = i, propertyPath = normalizedPath, op, ok = false, message = $"Property is not an array: {arrayPath}" }); - continue; - } - - results.Add(new { index = i, propertyPath = normalizedPath, op, ok = true, message = $"Will resize to {size}.", currentSize = arrayProp.arraySize }); - continue; - } - - // For set operations, check if the property exists (or can be auto-grown) - var prop = so.FindProperty(normalizedPath); - - // Check if it's an auto-growable array element path - bool isAutoGrowable = false; - if (prop == null) - { - var match = Regex.Match(normalizedPath, @"^(.+?)\.Array\.data\[(\d+)\]"); - if (match.Success) - { - string arrayPath = match.Groups[1].Value; - var arrayProp = so.FindProperty(arrayPath); - if (arrayProp != null && arrayProp.isArray) - { - isAutoGrowable = true; - // Get the element type info from existing elements or report as growable - int targetIndex = int.Parse(match.Groups[2].Value); - if (arrayProp.arraySize > 0) - { - var sampleElement = arrayProp.GetArrayElementAtIndex(0); - results.Add(new { - index = i, - propertyPath = normalizedPath, - op, - ok = true, - message = $"Will auto-grow array from {arrayProp.arraySize} to {targetIndex + 1}.", - elementType = sampleElement?.propertyType.ToString() ?? "unknown" - }); - } - else - { - results.Add(new { - index = i, - propertyPath = normalizedPath, - op, - ok = true, - message = $"Will auto-grow empty array to size {targetIndex + 1}." - }); - } - continue; - } - } - } - - if (prop == null && !isAutoGrowable) - { - results.Add(new { index = i, propertyPath = normalizedPath, op, ok = false, message = $"Property not found: {normalizedPath}" }); - continue; - } - - if (prop != null) - { - // Property exists - validate value format for supported complex types - var valueToken = patchObj["value"]; - string valueValidationMsg = null; - bool valueFormatOk = true; - - // Enhanced dry-run: validate value format for AnimationCurve and Quaternion - // Uses shared validators from VectorParsing - if (valueToken != null && valueToken.Type != JTokenType.Null) - { - switch (prop.propertyType) - { - case SerializedPropertyType.AnimationCurve: - valueFormatOk = VectorParsing.ValidateAnimationCurveFormat(valueToken, out valueValidationMsg); - break; - case SerializedPropertyType.Quaternion: - valueFormatOk = VectorParsing.ValidateQuaternionFormat(valueToken, out valueValidationMsg); - break; - } - } - - if (valueFormatOk) - { - results.Add(new { - index = i, - propertyPath = normalizedPath, - op, - ok = true, - message = valueValidationMsg ?? "Property found.", - propertyType = prop.propertyType.ToString(), - isArray = prop.isArray - }); - } - else - { - results.Add(new { - index = i, - propertyPath = normalizedPath, - op, - ok = false, - message = valueValidationMsg, - propertyType = prop.propertyType.ToString(), - isArray = prop.isArray - }); - } - } - } - - return results; - } - - private static (List results, List warnings) ApplyPatches(UnityEngine.Object target, JArray patches) - { - var warnings = new List(); - var results = new List(patches.Count); - bool anyChanged = false; - - var so = new SerializedObject(target); - so.Update(); - - for (int i = 0; i < patches.Count; i++) - { - if (patches[i] is not JObject patchObj) - { - results.Add(new { propertyPath = "", op = "", ok = false, message = $"Patch at index {i} must be an object." }); - continue; - } - - string propertyPath = patchObj["propertyPath"]?.ToString() - ?? patchObj["property_path"]?.ToString() - ?? patchObj["path"]?.ToString(); - string op = (patchObj["op"]?.ToString() ?? "set").Trim(); - if (string.IsNullOrWhiteSpace(propertyPath)) - { - results.Add(new { propertyPath = propertyPath ?? "", op, ok = false, message = "Missing required field: propertyPath" }); - continue; - } - - if (string.IsNullOrWhiteSpace(op)) - { - op = "set"; - } - - var patchResult = ApplyPatch(so, propertyPath, op, patchObj, out bool changed); - anyChanged |= changed; - results.Add(patchResult); - - // Array resize should be applied immediately so later paths resolve. - if (string.Equals(op, "array_resize", StringComparison.OrdinalIgnoreCase) && changed) - { - so.ApplyModifiedProperties(); - so.Update(); - } - } - - if (anyChanged) - { - so.ApplyModifiedProperties(); - EditorUtility.SetDirty(target); - AssetDatabase.SaveAssets(); - } - - return (results, warnings); - } - - private static object ApplyPatch(SerializedObject so, string propertyPath, string op, JObject patchObj, out bool changed) - { - changed = false; - try - { - // Phase 1.1: Normalize friendly path syntax (e.g., myList[5] → myList.Array.data[5]) - string normalizedPath = NormalizePropertyPath(propertyPath); - string normalizedOp = op.Trim().ToLowerInvariant(); - - switch (normalizedOp) - { - case "array_resize": - return ApplyArrayResize(so, normalizedPath, patchObj, out changed); - case "set": - default: - return ApplySet(so, normalizedPath, patchObj, out changed); - } - } - catch (Exception ex) - { - return new { propertyPath, op, ok = false, message = ex.Message }; - } - } - - /// - /// Normalizes friendly property path syntax to Unity's internal format. - /// Converts bracket notation (e.g., myList[5]) to Unity's Array.data format (myList.Array.data[5]). - /// - private static string NormalizePropertyPath(string path) - { - if (string.IsNullOrEmpty(path)) - return path; - - // Pattern: word[number] where it's not already in .Array.data[number] format - // We need to handle cases like: myList[5], nested.list[0].field, etc. - // But NOT: myList.Array.data[5] (already in Unity format) - - // Replace fieldName[index] with fieldName.Array.data[index] - // But only if it's not already in Array.data format - return Regex.Replace(path, @"(\w+)\[(\d+)\]", m => - { - string fieldName = m.Groups[1].Value; - string index = m.Groups[2].Value; - - // Check if this match is already part of .Array.data[index] pattern - // by checking if the text immediately before the field name is ".Array." - // and the field name is "data" - int matchStart = m.Index; - if (fieldName == "data" && matchStart >= 7) // Length of ".Array." - { - string preceding = path.Substring(matchStart - 7, 7); - if (preceding == ".Array.") - { - // Already in Unity format (e.g., myList.Array.data[0]), return as-is - return m.Value; - } - } - - return $"{fieldName}.Array.data[{index}]"; - }); - } - - /// - /// Ensures an array has sufficient capacity for the given index. - /// Automatically resizes the array if the target index is beyond current bounds. - /// - /// The SerializedObject containing the array - /// The normalized property path (must be in Array.data format) - /// True if the array was resized - /// True if the path is valid for setting, false if it cannot be resolved - private static bool EnsureArrayCapacity(SerializedObject so, string path, out bool resized) - { - resized = false; - - // Match pattern: something.Array.data[N] - var match = Regex.Match(path, @"^(.+?)\.Array\.data\[(\d+)\]"); - if (!match.Success) - { - // Not an array element path, nothing to do - return true; - } - - string arrayPath = match.Groups[1].Value; - if (!int.TryParse(match.Groups[2].Value, out int targetIndex)) - { - return false; - } - - var arrayProp = so.FindProperty(arrayPath); - if (arrayProp == null || !arrayProp.isArray) - { - // Array property not found or not an array - return false; - } - - if (arrayProp.arraySize <= targetIndex) - { - // Need to grow the array - arrayProp.arraySize = targetIndex + 1; - so.ApplyModifiedProperties(); - so.Update(); - resized = true; - } - - return true; - } - - private static object ApplyArrayResize(SerializedObject so, string propertyPath, JObject patchObj, out bool changed) - { - changed = false; - - // Use ParamCoercion for robust int parsing - var valueToken = patchObj["value"]; - if (valueToken == null || valueToken.Type == JTokenType.Null) - { - return new { propertyPath, op = "array_resize", ok = false, message = "array_resize requires integer 'value'." }; - } - - int newSize = ParamCoercion.CoerceInt(valueToken, -1); - if (newSize < 0) - { - return new { propertyPath, op = "array_resize", ok = false, message = "array_resize requires integer 'value'." }; - } - - newSize = Math.Max(0, newSize); - - // Unity supports resizing either: - // - the array/list property itself (prop.isArray -> prop.arraySize) - // - the synthetic leaf property ".Array.size" (prop.intValue) - // - // Different Unity versions/serialization edge cases can fail to resolve the synthetic leaf via FindProperty - // (or can return different property types), so we keep a "best-effort" fallback: - // - Prefer acting on the requested path if it resolves. - // - If the requested path doesn't resolve, try to resolve the *array property* and set arraySize directly. - SerializedProperty prop = so.FindProperty(propertyPath); - SerializedProperty arrayProp = null; - if (propertyPath.EndsWith(".Array.size", StringComparison.Ordinal)) - { - // Caller explicitly targeted the synthetic leaf. Resolve the parent array property as a fallback - // (Unity sometimes fails to resolve the synthetic leaf in certain serialization contexts). - var arrayPath = propertyPath.Substring(0, propertyPath.Length - ".Array.size".Length); - arrayProp = so.FindProperty(arrayPath); - } - else - { - // Caller targeted either the array property itself (e.g., "items") or some other property. - // If it's already an array, we can resize it directly. Otherwise, we attempt to resolve - // a synthetic ".Array.size" leaf as a convenience, which some clients may pass. - arrayProp = prop != null && prop.isArray ? prop : so.FindProperty(propertyPath + ".Array.size"); - } - - if (prop == null) - { - // If we failed to find the direct property but we *can* find the array property, use that. - if (arrayProp != null && arrayProp.isArray) - { - if (arrayProp.arraySize != newSize) - { - arrayProp.arraySize = newSize; - changed = true; - } - return new - { - propertyPath, - op = "array_resize", - ok = true, - resolvedPropertyType = "Array", - message = $"Set array size to {newSize}." - }; - } - - return new { propertyPath, op = "array_resize", ok = false, message = $"Property not found: {propertyPath}" }; - } - - // Unity may represent ".Array.size" as either Integer or ArraySize depending on version. - if ((prop.propertyType == SerializedPropertyType.Integer || prop.propertyType == SerializedPropertyType.ArraySize) - && propertyPath.EndsWith(".Array.size", StringComparison.Ordinal)) - { - // We successfully resolved the synthetic leaf; write the size through its intValue. - if (prop.intValue != newSize) - { - prop.intValue = newSize; - changed = true; - } - return new { propertyPath, op = "array_resize", ok = true, resolvedPropertyType = prop.propertyType.ToString(), message = $"Set array size to {newSize}." }; - } - - if (prop.isArray) - { - // We resolved the array property itself; write through arraySize. - if (prop.arraySize != newSize) - { - prop.arraySize = newSize; - changed = true; - } - return new { propertyPath, op = "array_resize", ok = true, resolvedPropertyType = "Array", message = $"Set array size to {newSize}." }; - } - - return new { propertyPath, op = "array_resize", ok = false, resolvedPropertyType = prop.propertyType.ToString(), message = $"Property is not an array or array-size field: {propertyPath}" }; - } - - private static object ApplySet(SerializedObject so, string propertyPath, JObject patchObj, out bool changed) - { - changed = false; - - // Phase 1.2: Auto-resize arrays if targeting an index beyond current bounds - if (!EnsureArrayCapacity(so, propertyPath, out bool arrayResized)) - { - // Could not resolve the array path - try to find the property anyway for a better error message - var checkProp = so.FindProperty(propertyPath); - if (checkProp == null) - { - // Try to provide helpful context about what went wrong - var arrayMatch = Regex.Match(propertyPath, @"^(.+?)\.Array\.data\[(\d+)\]"); - if (arrayMatch.Success) - { - string arrayPath = arrayMatch.Groups[1].Value; - var arrayProp = so.FindProperty(arrayPath); - if (arrayProp == null) - { - return new { propertyPath, op = "set", ok = false, message = $"Array property not found: {arrayPath}" }; - } - if (!arrayProp.isArray) - { - return new { propertyPath, op = "set", ok = false, message = $"Property is not an array: {arrayPath}" }; - } - } - return new { propertyPath, op = "set", ok = false, message = $"Property not found: {propertyPath}" }; - } - } - - var prop = so.FindProperty(propertyPath); - if (prop == null) - { - return new { propertyPath, op = "set", ok = false, message = $"Property not found: {propertyPath}" }; - } - - // Track if we resized - this counts as a change - if (arrayResized) - { - changed = true; - } - - if (prop.propertyType == SerializedPropertyType.ObjectReference) - { - var refObj = patchObj["ref"] as JObject; - var objRefValue = patchObj["value"]; - UnityEngine.Object newRef = null; - string refGuid = refObj?["guid"]?.ToString(); - string refPath = refObj?["path"]?.ToString(); - string resolveMethod = "explicit"; - - if (refObj == null && objRefValue?.Type == JTokenType.Null) - { - // Explicit null - clear the reference - newRef = null; - resolveMethod = "cleared"; - } - else if (!string.IsNullOrEmpty(refGuid) || !string.IsNullOrEmpty(refPath)) - { - // Traditional ref object with guid or path - string resolvedPath = !string.IsNullOrEmpty(refGuid) - ? AssetDatabase.GUIDToAssetPath(refGuid) - : AssetPathUtility.SanitizeAssetPath(refPath); - - if (!string.IsNullOrEmpty(resolvedPath)) - { - newRef = AssetDatabase.LoadAssetAtPath(resolvedPath); - } - resolveMethod = !string.IsNullOrEmpty(refGuid) ? "ref.guid" : "ref.path"; - } - else if (objRefValue?.Type == JTokenType.String) - { - // Phase 4: GUID shorthand - allow plain string value - string strVal = objRefValue.ToString(); - - // Check if it's a GUID (32 hex characters, no dashes) - if (Regex.IsMatch(strVal, @"^[0-9a-fA-F]{32}$")) - { - string guidPath = AssetDatabase.GUIDToAssetPath(strVal); - if (!string.IsNullOrEmpty(guidPath)) - { - newRef = AssetDatabase.LoadAssetAtPath(guidPath); - resolveMethod = "guid-shorthand"; - } - } - // Check if it looks like an asset path - else if (strVal.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) || - strVal.Contains("/")) - { - string sanitizedPath = AssetPathUtility.SanitizeAssetPath(strVal); - newRef = AssetDatabase.LoadAssetAtPath(sanitizedPath); - resolveMethod = "path-shorthand"; - } - } - - if (prop.objectReferenceValue != newRef) - { - prop.objectReferenceValue = newRef; - changed = true; - } - - string refMessage = newRef == null ? "Cleared reference." : $"Set reference ({resolveMethod})."; - return new { propertyPath, op = "set", ok = true, resolvedPropertyType = prop.propertyType.ToString(), message = refMessage }; - } - - var valueToken = patchObj["value"]; - if (valueToken == null) - { - return new { propertyPath, op = "set", ok = false, resolvedPropertyType = prop.propertyType.ToString(), message = "Missing required field: value" }; - } - - bool ok = TrySetValue(prop, valueToken, out string message); - changed = ok; - return new { propertyPath, op = "set", ok, resolvedPropertyType = prop.propertyType.ToString(), message }; - } - - private static bool TrySetValue(SerializedProperty prop, JToken valueToken, out string message) - { - return TrySetValueRecursive(prop, valueToken, out message, 0); - } - - /// - /// Recursively sets values on SerializedProperties, supporting bulk array and object mapping. - /// - /// The property to set - /// The JSON value - /// Output message describing the result - /// Current recursion depth (for safety limits) - private static bool TrySetValueRecursive(SerializedProperty prop, JToken valueToken, out string message, int depth) - { - message = null; - const int MaxRecursionDepth = 20; - - if (depth > MaxRecursionDepth) - { - message = $"Maximum recursion depth ({MaxRecursionDepth}) exceeded. Check for circular references."; - return false; - } - - try - { - // Phase 3.1: Handle bulk array mapping - JArray value for array/list properties - if (prop.isArray && prop.propertyType != SerializedPropertyType.String && valueToken is JArray jArray) - { - // Resize the array to match the JSON array - prop.arraySize = jArray.Count; - - // Get the SerializedObject and apply so we can access elements - var so = prop.serializedObject; - so.ApplyModifiedProperties(); - so.Update(); - - int successCount = 0; - var errors = new List(); - - for (int i = 0; i < jArray.Count; i++) - { - var elementProp = prop.GetArrayElementAtIndex(i); - if (elementProp == null) - { - errors.Add($"Could not get element at index {i}"); - continue; - } - - if (TrySetValueRecursive(elementProp, jArray[i], out string elemMessage, depth + 1)) - { - successCount++; - } - else - { - errors.Add($"[{i}]: {elemMessage}"); - } - } - - so.ApplyModifiedProperties(); - - if (errors.Count > 0) - { - message = $"Set {successCount}/{jArray.Count} elements. Errors: {string.Join("; ", errors)}"; - return successCount > 0; // Partial success - } - - message = $"Set array with {jArray.Count} elements."; - return true; - } - - // Phase 3.2: Handle bulk object mapping - JObject value for Generic (struct/class) properties - if (prop.propertyType == SerializedPropertyType.Generic && !prop.isArray && valueToken is JObject jObj) - { - int successCount = 0; - var errors = new List(); - var so = prop.serializedObject; - - foreach (var kvp in jObj) - { - string childPath = prop.propertyPath + "." + kvp.Key; - var childProp = so.FindProperty(childPath); - - if (childProp == null) - { - errors.Add($"Property not found: {kvp.Key}"); - continue; - } - - if (TrySetValueRecursive(childProp, kvp.Value, out string childMessage, depth + 1)) - { - successCount++; - } - else - { - errors.Add($"{kvp.Key}: {childMessage}"); - } - } - - so.ApplyModifiedProperties(); - - if (errors.Count > 0) - { - message = $"Set {successCount}/{jObj.Count} fields. Errors: {string.Join("; ", errors)}"; - return successCount > 0; // Partial success - } - - message = $"Set struct/class with {jObj.Count} fields."; - return true; - } - - // Supported Types: Integer, Boolean, Float, String, Enum, Vector2, Vector3, Vector4, Color - // Using shared helpers from ParamCoercion and VectorParsing - switch (prop.propertyType) - { - case SerializedPropertyType.Integer: - // Use ParamCoercion for robust int parsing - int intVal = ParamCoercion.CoerceInt(valueToken, int.MinValue); - if (intVal == int.MinValue && valueToken?.Type != JTokenType.Integer) - { - // Double-check: if it's actually int.MinValue or failed to parse - if (valueToken == null || valueToken.Type == JTokenType.Null || - (valueToken.Type == JTokenType.String && !int.TryParse(valueToken.ToString(), out _))) - { - message = "Expected integer value."; - return false; - } - } - prop.intValue = intVal; - message = "Set int."; - return true; - - case SerializedPropertyType.Boolean: - // Use ParamCoercion for robust bool parsing (handles "true", "1", "yes", etc.) - if (valueToken == null || valueToken.Type == JTokenType.Null) - { - message = "Expected boolean value."; - return false; - } - bool boolVal = ParamCoercion.CoerceBool(valueToken, false); - // Verify it actually looked like a bool - if (valueToken.Type != JTokenType.Boolean) - { - string strVal = valueToken.ToString().Trim().ToLowerInvariant(); - if (strVal != "true" && strVal != "false" && strVal != "1" && strVal != "0" && - strVal != "yes" && strVal != "no" && strVal != "on" && strVal != "off") - { - message = "Expected boolean value."; - return false; - } - } - prop.boolValue = boolVal; - message = "Set bool."; - return true; - - case SerializedPropertyType.Float: - // Use ParamCoercion for robust float parsing - float floatVal = ParamCoercion.CoerceFloat(valueToken, float.NaN); - if (float.IsNaN(floatVal)) - { - message = "Expected float value."; - return false; - } - prop.floatValue = floatVal; - message = "Set float."; - return true; - - case SerializedPropertyType.String: - prop.stringValue = valueToken.Type == JTokenType.Null ? null : valueToken.ToString(); - message = "Set string."; - return true; - - case SerializedPropertyType.Enum: - return TrySetEnum(prop, valueToken, out message); - - case SerializedPropertyType.Vector2: - // Use VectorParsing for Vector2 - var v2 = VectorParsing.ParseVector2(valueToken); - if (v2 == null) - { - message = "Expected Vector2 (array or object)."; - return false; - } - prop.vector2Value = v2.Value; - message = "Set Vector2."; - return true; - - case SerializedPropertyType.Vector3: - // Use VectorParsing for Vector3 - var v3 = VectorParsing.ParseVector3(valueToken); - if (v3 == null) - { - message = "Expected Vector3 (array or object)."; - return false; - } - prop.vector3Value = v3.Value; - message = "Set Vector3."; - return true; - - case SerializedPropertyType.Vector4: - // Use VectorParsing for Vector4 - var v4 = VectorParsing.ParseVector4(valueToken); - if (v4 == null) - { - message = "Expected Vector4 (array or object)."; - return false; - } - prop.vector4Value = v4.Value; - message = "Set Vector4."; - return true; - - case SerializedPropertyType.Color: - // Use VectorParsing for Color - var col = VectorParsing.ParseColor(valueToken); - if (col == null) - { - message = "Expected Color (array or object)."; - return false; - } - prop.colorValue = col.Value; - message = "Set Color."; - return true; - - case SerializedPropertyType.AnimationCurve: - return TrySetAnimationCurve(prop, valueToken, out message); - - case SerializedPropertyType.Quaternion: - return TrySetQuaternion(prop, valueToken, out message); - - case SerializedPropertyType.Generic: - // Generic properties (structs/classes) should be handled above with JObject mapping - // If we get here, the value wasn't a JObject - if (prop.isArray) - { - message = $"Expected array (JArray) for array property, got {valueToken?.Type.ToString() ?? "null"}."; - } - else - { - message = $"Expected object (JObject) for struct/class property, got {valueToken?.Type.ToString() ?? "null"}."; - } - return false; - - default: - message = $"Unsupported SerializedPropertyType: {prop.propertyType}. " + - "This type cannot be set via MCP patches. Consider editing the .asset file directly " + - "or using Unity's Inspector. For complex types, check if there's a supported alternative format."; - return false; - } - } - catch (Exception ex) - { - message = ex.Message; - return false; - } - } - - private static bool TrySetEnum(SerializedProperty prop, JToken valueToken, out string message) - { - message = null; - var names = prop.enumNames; - if (names == null || names.Length == 0) { message = "Enum has no names."; return false; } - - if (valueToken.Type == JTokenType.Integer) - { - int idx = valueToken.Value(); - if (idx < 0 || idx >= names.Length) { message = $"Enum index out of range: {idx}"; return false; } - prop.enumValueIndex = idx; message = "Set enum."; return true; - } - - string s = valueToken.ToString(); - for (int i = 0; i < names.Length; i++) - { - if (string.Equals(names[i], s, StringComparison.OrdinalIgnoreCase)) - { - prop.enumValueIndex = i; message = "Set enum."; return true; - } - } - message = $"Unknown enum name '{s}'."; - return false; - } - - /// - /// Sets an AnimationCurve property from a JSON structure. - /// - /// Supported formats: - /// - /// Wrapped: { "keys": [ { "time": 0, "value": 1.0 }, ... ] } - /// Direct array: [ { "time": 0, "value": 1.0 }, ... ] - /// Null/empty: Sets an empty AnimationCurve - /// - /// - /// Keyframe fields: - /// - /// time (float): Keyframe time position. Default: 0 - /// value (float): Keyframe value. Default: 0 - /// inSlope or inTangent (float): Incoming tangent slope. Default: 0 - /// outSlope or outTangent (float): Outgoing tangent slope. Default: 0 - /// weightedMode (int): Weighted mode enum (0=None, 1=In, 2=Out, 3=Both). Default: 0 (None) - /// inWeight (float): Incoming tangent weight. Default: 0 - /// outWeight (float): Outgoing tangent weight. Default: 0 - /// - /// - /// Note: All keyframe fields are optional. Missing fields gracefully default to 0, - /// which produces linear interpolation when both tangents are 0. - /// - /// The SerializedProperty of type AnimationCurve to set - /// JSON token containing the curve data - /// Output message describing the result - /// True if successful, false if the format is invalid - private static bool TrySetAnimationCurve(SerializedProperty prop, JToken valueToken, out string message) - { - message = null; - - if (valueToken == null || valueToken.Type == JTokenType.Null) - { - // Set to empty curve - prop.animationCurveValue = new AnimationCurve(); - message = "Set AnimationCurve to empty."; - return true; - } - - JArray keysArray = null; - - // Accept either { "keys": [...] } or just [...] - if (valueToken is JObject curveObj) - { - keysArray = curveObj["keys"] as JArray; - if (keysArray == null) - { - message = "AnimationCurve object requires 'keys' array. Expected: { \"keys\": [ { \"time\": 0, \"value\": 0 }, ... ] }"; - return false; - } - } - else if (valueToken is JArray directArray) - { - keysArray = directArray; - } - else - { - message = "AnimationCurve requires object with 'keys' or array of keyframes. " + - "Expected: { \"keys\": [ { \"time\": 0, \"value\": 0, \"inSlope\": 0, \"outSlope\": 0 }, ... ] }"; - return false; - } - - try - { - var curve = new AnimationCurve(); - foreach (var keyToken in keysArray) - { - if (keyToken is not JObject keyObj) - { - message = "Each keyframe must be an object with 'time' and 'value'."; - return false; - } - - float time = keyObj["time"]?.Value() ?? 0f; - float value = keyObj["value"]?.Value() ?? 0f; - float inSlope = keyObj["inSlope"]?.Value() ?? keyObj["inTangent"]?.Value() ?? 0f; - float outSlope = keyObj["outSlope"]?.Value() ?? keyObj["outTangent"]?.Value() ?? 0f; - - var keyframe = new Keyframe(time, value, inSlope, outSlope); - - // Optional: weighted tangent mode (Unity 2018.1+) - if (keyObj["weightedMode"] != null) - { - int weightedMode = keyObj["weightedMode"].Value(); - keyframe.weightedMode = (WeightedMode)weightedMode; - } - if (keyObj["inWeight"] != null) - { - keyframe.inWeight = keyObj["inWeight"].Value(); - } - if (keyObj["outWeight"] != null) - { - keyframe.outWeight = keyObj["outWeight"].Value(); - } - - curve.AddKey(keyframe); - } - - prop.animationCurveValue = curve; - message = $"Set AnimationCurve with {keysArray.Count} keyframes."; - return true; - } - catch (Exception ex) - { - message = $"Failed to parse AnimationCurve: {ex.Message}"; - return false; - } - } - - /// - /// Sets a Quaternion property from JSON. - /// - /// Supported formats: - /// - /// Euler array: [x, y, z] - Euler angles in degrees - /// Raw quaternion array: [x, y, z, w] - Direct quaternion components - /// Object format: { "x": 0, "y": 0, "z": 0, "w": 1 } - Direct components - /// Explicit euler: { "euler": [x, y, z] } - Euler angles in degrees - /// Null/empty: Sets Quaternion.identity (no rotation) - /// - /// - /// Format detection: - /// - /// 3-element array → Interpreted as Euler angles (degrees) - /// 4-element array → Interpreted as raw quaternion [x, y, z, w] - /// Object with euler → Uses euler array for rotation - /// Object with x, y, z, w → Uses raw quaternion components - /// - /// - /// The SerializedProperty of type Quaternion to set - /// JSON token containing the quaternion data - /// Output message describing the result - /// True if successful, false if the format is invalid - private static bool TrySetQuaternion(SerializedProperty prop, JToken valueToken, out string message) - { - message = null; - - if (valueToken == null || valueToken.Type == JTokenType.Null) - { - prop.quaternionValue = Quaternion.identity; - message = "Set Quaternion to identity."; - return true; - } - - try - { - if (valueToken is JArray arr) - { - if (arr.Count == 3) - { - // Euler angles [x, y, z] - var euler = new Vector3( - arr[0].Value(), - arr[1].Value(), - arr[2].Value() - ); - prop.quaternionValue = Quaternion.Euler(euler); - message = $"Set Quaternion from Euler({euler.x}, {euler.y}, {euler.z})."; - return true; - } - else if (arr.Count == 4) - { - // Raw quaternion [x, y, z, w] - prop.quaternionValue = new Quaternion( - arr[0].Value(), - arr[1].Value(), - arr[2].Value(), - arr[3].Value() - ); - message = "Set Quaternion from [x, y, z, w]."; - return true; - } - else - { - message = "Quaternion array must have 3 elements (Euler) or 4 elements (x, y, z, w)."; - return false; - } - } - else if (valueToken is JObject obj) - { - // Check for explicit euler property - if (obj["euler"] is JArray eulerArr && eulerArr.Count == 3) - { - var euler = new Vector3( - eulerArr[0].Value(), - eulerArr[1].Value(), - eulerArr[2].Value() - ); - prop.quaternionValue = Quaternion.Euler(euler); - message = $"Set Quaternion from euler: ({euler.x}, {euler.y}, {euler.z})."; - return true; - } - - // Object format { x, y, z, w } - if (obj["x"] != null && obj["y"] != null && obj["z"] != null && obj["w"] != null) - { - prop.quaternionValue = new Quaternion( - obj["x"].Value(), - obj["y"].Value(), - obj["z"].Value(), - obj["w"].Value() - ); - message = "Set Quaternion from { x, y, z, w }."; - return true; - } - - message = "Quaternion object must have { x, y, z, w } or { euler: [x, y, z] }."; - return false; - } - else - { - message = "Quaternion requires array [x,y,z] (Euler), [x,y,z,w] (raw), or object { x, y, z, w }."; - return false; - } - } - catch (Exception ex) - { - message = $"Failed to parse Quaternion: {ex.Message}"; - return false; - } - } - - private static bool TryResolveTarget(JToken targetToken, out UnityEngine.Object target, out string targetPath, out string targetGuid, out object error) - { - target = null; - targetPath = null; - targetGuid = null; - error = null; - - if (targetToken is not JObject targetObj) - { - error = new ErrorResponse(CodeInvalidParams, new { message = "'target' must be an object with {guid|path}." }); - return false; - } - - string guid = targetObj["guid"]?.ToString(); - string path = targetObj["path"]?.ToString(); - - if (string.IsNullOrWhiteSpace(guid) && string.IsNullOrWhiteSpace(path)) - { - error = new ErrorResponse(CodeInvalidParams, new { message = "'target' must include 'guid' or 'path'." }); - return false; - } - - string resolvedPath = !string.IsNullOrWhiteSpace(guid) - ? AssetDatabase.GUIDToAssetPath(guid) - : AssetPathUtility.SanitizeAssetPath(path); - - if (string.IsNullOrWhiteSpace(resolvedPath)) - { - error = new ErrorResponse(CodeTargetNotFound, new { message = "Could not resolve target path.", guid, path }); - return false; - } - - var obj = AssetDatabase.LoadAssetAtPath(resolvedPath); - if (obj == null) - { - error = new ErrorResponse(CodeTargetNotFound, new { message = "Target asset not found.", targetPath = resolvedPath, targetGuid = guid }); - return false; - } - - target = obj; - targetPath = resolvedPath; - targetGuid = string.IsNullOrWhiteSpace(guid) ? AssetDatabase.AssetPathToGUID(resolvedPath) : guid; - return true; - } - - private static void CoerceJsonStringArrayParameter(JObject @params, string paramName) - { - var token = @params?[paramName]; - if (token != null && token.Type == JTokenType.String) - { - try - { - var parsed = JToken.Parse(token.ToString()); - if (parsed is JArray arr) - { - @params[paramName] = arr; - } - } - catch (Exception e) - { - McpLog.Warn($"[MCP] Could not parse '{paramName}' JSON string: {e.Message}"); - } - } - } - - private static bool EnsureFolderExists(string folderPath, out string error) - { - error = null; - if (string.IsNullOrWhiteSpace(folderPath)) - { - error = "Folder path is empty."; - return false; - } - - // Expect normalized input here (Assets/... or Assets). - string sanitized = SanitizeSlashes(folderPath); - - if (!sanitized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase) - && !string.Equals(sanitized, "Assets", StringComparison.OrdinalIgnoreCase)) - { - error = "Folder path must be under Assets/."; - return false; - } - - if (string.Equals(sanitized, "Assets", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - sanitized = sanitized.TrimEnd('/'); - if (AssetDatabase.IsValidFolder(sanitized)) - { - return true; - } - - // Create recursively from Assets/ - var parts = sanitized.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 0 || !string.Equals(parts[0], "Assets", StringComparison.OrdinalIgnoreCase)) - { - error = "Folder path must start with Assets/"; - return false; - } - - string current = "Assets"; - for (int i = 1; i < parts.Length; i++) - { - string next = current + "/" + parts[i]; - if (!AssetDatabase.IsValidFolder(next)) - { - string guid = AssetDatabase.CreateFolder(current, parts[i]); - if (string.IsNullOrEmpty(guid)) - { - error = $"Failed to create folder: {next}"; - return false; - } - } - current = next; - } - - return AssetDatabase.IsValidFolder(sanitized); - } - - private static string SanitizeSlashes(string path) - { - if (string.IsNullOrWhiteSpace(path)) - { - return path; - } - - var s = AssetPathUtility.NormalizeSeparators(path); - while (s.IndexOf("//", StringComparison.Ordinal) >= 0) - { - s = s.Replace("//", "/", StringComparison.Ordinal); - } - return s; - } - - private static bool TryNormalizeFolderPath(string folderPath, out string normalized, out string error) - { - normalized = null; - error = null; - - if (string.IsNullOrWhiteSpace(folderPath)) - { - error = "Folder path is empty."; - return false; - } - - var s = SanitizeSlashes(folderPath.Trim()); - - // Reject obvious non-project/invalid roots. We only support Assets/ (and relative paths that will be rooted under Assets/). - if (s.StartsWith("/", StringComparison.Ordinal) - || s.StartsWith("file:", StringComparison.OrdinalIgnoreCase) - || Regex.IsMatch(s, @"^[a-zA-Z]:")) - { - error = "Folder path must be a project-relative path under Assets/."; - return false; - } - - if (s.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase) - || s.StartsWith("ProjectSettings/", StringComparison.OrdinalIgnoreCase) - || s.StartsWith("Library/", StringComparison.OrdinalIgnoreCase)) - { - error = "Folder path must be under Assets/."; - return false; - } - - if (string.Equals(s, "Assets", StringComparison.OrdinalIgnoreCase)) - { - normalized = "Assets"; - return true; - } - - if (s.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) - { - normalized = s.TrimEnd('/'); - return true; - } - - // Allow relative paths like "Temp/MyFolder" and root them under Assets/. - normalized = ("Assets/" + s.TrimStart('/')).TrimEnd('/'); - return true; - } - - // NOTE: Local TryGet* helpers have been removed. - // Using shared helpers instead: ParamCoercion (for int/float/bool) and VectorParsing (for Vector2/3/4, Color) - - private static string NormalizeAction(string raw) - { - var s = raw.Trim(); - s = s.Replace("-", "").Replace("_", ""); - return s.ToLowerInvariant(); - } - - private static bool IsCreateAction(string normalized) - { - return normalized == "create" || normalized == "createso"; - } - - /// - /// Resolves a type by name. Delegates to UnityTypeResolver.ResolveAny(). - /// - private static Type ResolveType(string typeName) - { - return Helpers.UnityTypeResolver.ResolveAny(typeName); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/ManageScriptableObject.cs.meta b/Assets/MCPForUnity/Editor/Tools/ManageScriptableObject.cs.meta deleted file mode 100644 index 153cfe3..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageScriptableObject.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 9e0bb5a8c1b24b7ea8bce09ce0a1f234 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/ManageScriptableObject.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/ManageShader.cs b/Assets/MCPForUnity/Editor/Tools/ManageShader.cs deleted file mode 100644 index 87b86fe..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageShader.cs +++ /dev/null @@ -1,344 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Handles CRUD operations for shader files within the Unity project. - /// - [McpForUnityTool("manage_shader", AutoRegister = false)] - public static class ManageShader - { - /// - /// Main handler for shader management actions. - /// - public static object HandleCommand(JObject @params) - { - // Extract parameters - string action = @params["action"]?.ToString()?.ToLowerInvariant(); - 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() ?? false; - if (contentsEncoded && @params["encodedContents"] != null) - { - try - { - contents = DecodeBase64(@params["encodedContents"].ToString()); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to decode shader contents: {e.Message}"); - } - } - else - { - contents = @params["contents"]?.ToString(); - } - - // Validate required parameters - if (string.IsNullOrEmpty(action)) - { - return new ErrorResponse("Action parameter is required."); - } - if (string.IsNullOrEmpty(name)) - { - return new ErrorResponse("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 new ErrorResponse( - $"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 = AssetPathUtility.NormalizeSeparators(relativeDir).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 = AssetPathUtility.NormalizeSeparators( - Path.Combine("Assets", relativeDir, shaderFileName) - ); // 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(ImportAssetOptions.ForceSynchronousImport); - } - } - catch (Exception e) - { - return new ErrorResponse( - $"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 new ErrorResponse( - $"Unknown action: '{action}'. Valid actions are: create, read, update, delete." - ); - } - } - - /// - /// Decode base64 string to normal text - /// - private static string DecodeBase64(string encoded) - { - byte[] data = Convert.FromBase64String(encoded); - return System.Text.Encoding.UTF8.GetString(data); - } - - /// - /// Encode text to base64 string - /// - 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 new ErrorResponse( - $"Shader already exists at '{relativePath}'. Use 'update' action to modify." - ); - } - - // Add validation for shader name conflicts in Unity - if (Shader.Find(name) != null) - { - return new ErrorResponse( - $"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, new System.Text.UTF8Encoding(false)); - AssetDatabase.ImportAsset(relativePath); - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); // Ensure Unity recognizes the new shader - return new SuccessResponse( - $"Shader '{name}.shader' created successfully at '{relativePath}'.", - new { path = relativePath } - ); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to create shader '{relativePath}': {e.Message}"); - } - } - - private static object ReadShader(string fullPath, string relativePath) - { - if (!File.Exists(fullPath)) - { - return new ErrorResponse($"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 new SuccessResponse( - $"Shader '{Path.GetFileName(relativePath)}' read successfully.", - responseData - ); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to read shader '{relativePath}': {e.Message}"); - } - } - - private static object UpdateShader( - string fullPath, - string relativePath, - string name, - string contents - ) - { - if (!File.Exists(fullPath)) - { - return new ErrorResponse( - $"Shader not found at '{relativePath}'. Use 'create' action to add a new shader." - ); - } - if (string.IsNullOrEmpty(contents)) - { - return new ErrorResponse("Content is required for the 'update' action."); - } - - try - { - File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false)); - AssetDatabase.ImportAsset(relativePath); - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); - return new SuccessResponse( - $"Shader '{Path.GetFileName(relativePath)}' updated successfully.", - new { path = relativePath } - ); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to update shader '{relativePath}': {e.Message}"); - } - } - - private static object DeleteShader(string fullPath, string relativePath) - { - if (!File.Exists(fullPath)) - { - return new ErrorResponse($"Shader not found at '{relativePath}'."); - } - - try - { - // Delete the asset through Unity's AssetDatabase first - bool success = AssetDatabase.DeleteAsset(relativePath); - if (!success) - { - return new ErrorResponse($"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 new SuccessResponse($"Shader '{Path.GetFileName(relativePath)}' deleted successfully."); - } - catch (Exception e) - { - return new ErrorResponse($"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 - } - } - }"; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/ManageShader.cs.meta b/Assets/MCPForUnity/Editor/Tools/ManageShader.cs.meta deleted file mode 100644 index 85f096a..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageShader.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: bcf4f1f3110494344b2af9324cf5c571 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/ManageShader.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/ManageTexture.cs b/Assets/MCPForUnity/Editor/Tools/ManageTexture.cs deleted file mode 100644 index 86e4290..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageTexture.cs +++ /dev/null @@ -1,1025 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; -using MCPForUnity.Editor.Helpers; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Handles procedural texture generation operations. - /// Supports patterns (checkerboard, stripes, dots, grid, brick), - /// gradients, noise, and direct pixel manipulation. - /// - [McpForUnityTool("manage_texture", AutoRegister = false)] - public static class ManageTexture - { - private const int MaxTextureDimension = 1024; - private const int MaxTexturePixels = 1024 * 1024; - private const int MaxNoiseWork = 4000000; - private static readonly List ValidActions = new List - { - "create", - "modify", - "delete", - "create_sprite", - "apply_pattern", - "apply_gradient", - "apply_noise" - }; - - private static ErrorResponse ValidateDimensions(int width, int height, List warnings) - { - if (width <= 0 || height <= 0) - return new ErrorResponse($"Invalid dimensions: {width}x{height}. Must be positive."); - if (width > MaxTextureDimension || height > MaxTextureDimension) - warnings.Add($"Dimensions exceed recommended max {MaxTextureDimension} per side (got {width}x{height})."); - long totalPixels = (long)width * height; - if (totalPixels > MaxTexturePixels) - warnings.Add($"Total pixels exceed recommended max {MaxTexturePixels} (got {width}x{height})."); - return null; - } - - - public static object HandleCommand(JObject @params) - { - string action = @params["action"]?.ToString()?.ToLowerInvariant(); - if (string.IsNullOrEmpty(action)) - { - return new ErrorResponse("Action parameter is required."); - } - - if (!ValidActions.Contains(action)) - { - string validActionsList = string.Join(", ", ValidActions); - return new ErrorResponse( - $"Unknown action: '{action}'. Valid actions are: {validActionsList}" - ); - } - - string path = @params["path"]?.ToString(); - - try - { - switch (action) - { - case "create": - case "create_sprite": - return CreateTexture(@params, action == "create_sprite"); - case "modify": - return ModifyTexture(@params); - case "delete": - return DeleteTexture(path); - case "apply_pattern": - return ApplyPattern(@params); - case "apply_gradient": - return ApplyGradient(@params); - case "apply_noise": - return ApplyNoise(@params); - default: - return new ErrorResponse($"Unknown action: '{action}'"); - } - } - catch (Exception e) - { - McpLog.Error($"[ManageTexture] Action '{action}' failed: {e}"); - return new ErrorResponse($"Internal error processing action '{action}': {e.Message}"); - } - } - - // --- Action Implementations --- - - private static object CreateTexture(JObject @params, bool asSprite) - { - string path = @params["path"]?.ToString(); - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for create."); - - string imagePath = @params["imagePath"]?.ToString(); - bool hasImage = !string.IsNullOrEmpty(imagePath); - - int width = @params["width"]?.ToObject() ?? 64; - int height = @params["height"]?.ToObject() ?? 64; - List warnings = new List(); - - // Validate dimensions - if (!hasImage) - { - var dimensionError = ValidateDimensions(width, height, warnings); - if (dimensionError != null) - return dimensionError; - } - - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - EnsureDirectoryExists(fullPath); - - try - { - var fillColorToken = @params["fillColor"]; - var patternToken = @params["pattern"]; - var pixelsToken = @params["pixels"]; - - if (hasImage && (fillColorToken != null || patternToken != null || pixelsToken != null)) - { - return new ErrorResponse("imagePath cannot be combined with fillColor, pattern, or pixels."); - } - - int patternSize = 8; - if (!hasImage && patternToken != null) - { - patternSize = @params["patternSize"]?.ToObject() ?? 8; - if (patternSize <= 0) - return new ErrorResponse("patternSize must be greater than 0."); - } - - Texture2D texture; - if (hasImage) - { - string resolvedImagePath = ResolveImagePath(imagePath); - if (!File.Exists(resolvedImagePath)) - return new ErrorResponse($"Image file not found at '{imagePath}'."); - - byte[] imageBytes = File.ReadAllBytes(resolvedImagePath); - texture = new Texture2D(2, 2, TextureFormat.RGBA32, false); - if (!texture.LoadImage(imageBytes)) - { - UnityEngine.Object.DestroyImmediate(texture); - return new ErrorResponse($"Failed to load image from '{imagePath}'."); - } - - width = texture.width; - height = texture.height; - var imageDimensionError = ValidateDimensions(width, height, warnings); - if (imageDimensionError != null) - { - UnityEngine.Object.DestroyImmediate(texture); - return imageDimensionError; - } - } - else - { - texture = new Texture2D(width, height, TextureFormat.RGBA32, false); - - // Check for fill color - if (fillColorToken != null && fillColorToken.Type == JTokenType.Array) - { - Color32 fillColor = TextureOps.ParseColor32(fillColorToken as JArray); - TextureOps.FillTexture(texture, fillColor); - } - - // Check for pattern - if (patternToken != null) - { - string pattern = patternToken.ToString(); - var palette = TextureOps.ParsePalette(@params["palette"] as JArray); - ApplyPatternToTexture(texture, pattern, palette, patternSize); - } - - // Check for direct pixel data - if (pixelsToken != null) - { - TextureOps.ApplyPixelData(texture, pixelsToken, width, height); - } - - // If nothing specified, create transparent texture - if (fillColorToken == null && patternToken == null && pixelsToken == null) - { - TextureOps.FillTexture(texture, new Color32(0, 0, 0, 0)); - } - } - - texture.Apply(); - - // Save to disk - byte[] imageData = TextureOps.EncodeTexture(texture, fullPath); - if (imageData == null || imageData.Length == 0) - { - UnityEngine.Object.DestroyImmediate(texture); - return new ErrorResponse($"Failed to encode texture for '{fullPath}'"); - } - File.WriteAllBytes(GetAbsolutePath(fullPath), imageData); - - AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); - - // Configure texture importer settings if provided - JToken importSettingsToken = @params["importSettings"]; - JToken spriteSettingsToken = @params["spriteSettings"]; - - if (importSettingsToken != null) - { - ConfigureTextureImporter(fullPath, importSettingsToken); - } - else if (asSprite || spriteSettingsToken != null) - { - // Legacy sprite configuration - ConfigureAsSprite(fullPath, spriteSettingsToken); - } - - // Clean up memory - UnityEngine.Object.DestroyImmediate(texture); - foreach (var warning in warnings) - { - McpLog.Warn($"[ManageTexture] {warning}"); - } - - return new SuccessResponse( - $"Texture created at '{fullPath}' ({width}x{height})" + (asSprite ? " as sprite" : ""), - new - { - path = fullPath, - width, - height, - asSprite = asSprite || spriteSettingsToken != null || (importSettingsToken?["textureType"]?.ToString() == "Sprite"), - warnings = warnings.Count > 0 ? warnings : null - } - ); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to create texture: {e.Message}"); - } - } - - private static object ModifyTexture(JObject @params) - { - string path = @params["path"]?.ToString(); - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for modify."); - - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - if (!AssetExists(fullPath)) - return new ErrorResponse($"Texture not found at path: {fullPath}"); - - try - { - Texture2D texture = AssetDatabase.LoadAssetAtPath(fullPath); - if (texture == null) - return new ErrorResponse($"Failed to load texture at path: {fullPath}"); - - // Make the texture readable - string absolutePath = GetAbsolutePath(fullPath); - byte[] fileData = File.ReadAllBytes(absolutePath); - Texture2D editableTexture = new Texture2D(texture.width, texture.height, TextureFormat.RGBA32, false); - editableTexture.LoadImage(fileData); - - // Apply modifications - var setPixelsToken = @params["setPixels"] as JObject; - if (setPixelsToken != null) - { - int x = setPixelsToken["x"]?.ToObject() ?? 0; - int y = setPixelsToken["y"]?.ToObject() ?? 0; - int w = setPixelsToken["width"]?.ToObject() ?? 1; - int h = setPixelsToken["height"]?.ToObject() ?? 1; - - if (w <= 0 || h <= 0) - { - UnityEngine.Object.DestroyImmediate(editableTexture); - return new ErrorResponse("setPixels width and height must be positive."); - } - - var pixelsToken = setPixelsToken["pixels"]; - var colorToken = setPixelsToken["color"]; - - if (pixelsToken != null) - { - TextureOps.ApplyPixelDataToRegion(editableTexture, pixelsToken, x, y, w, h); - } - else if (colorToken != null) - { - Color32 color = TextureOps.ParseColor32(colorToken as JArray); - int startX = Mathf.Max(0, x); - int startY = Mathf.Max(0, y); - int endX = Mathf.Min(x + w, editableTexture.width); - int endY = Mathf.Min(y + h, editableTexture.height); - - for (int py = startY; py < endY; py++) - { - for (int px = startX; px < endX; px++) - { - editableTexture.SetPixel(px, py, color); - } - } - } - else - { - UnityEngine.Object.DestroyImmediate(editableTexture); - return new ErrorResponse("setPixels requires 'color' or 'pixels'."); - } - } - - editableTexture.Apply(); - - // Save back to disk - byte[] imageData = TextureOps.EncodeTexture(editableTexture, fullPath); - if (imageData == null || imageData.Length == 0) - { - UnityEngine.Object.DestroyImmediate(editableTexture); - return new ErrorResponse($"Failed to encode texture for '{fullPath}'"); - } - File.WriteAllBytes(absolutePath, imageData); - - AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); - - UnityEngine.Object.DestroyImmediate(editableTexture); - - return new SuccessResponse($"Texture modified: {fullPath}"); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to modify texture: {e.Message}"); - } - } - - private static object DeleteTexture(string path) - { - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for delete."); - - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - if (!AssetExists(fullPath)) - return new ErrorResponse($"Texture not found at path: {fullPath}"); - - try - { - bool success = AssetDatabase.DeleteAsset(fullPath); - if (success) - return new SuccessResponse($"Texture deleted: {fullPath}"); - else - return new ErrorResponse($"Failed to delete texture: {fullPath}"); - } - catch (Exception e) - { - return new ErrorResponse($"Error deleting texture: {e.Message}"); - } - } - - private static object ApplyPattern(JObject @params) - { - // Reuse CreateTexture with pattern - return CreateTexture(@params, false); - } - - private static object ApplyGradient(JObject @params) - { - string path = @params["path"]?.ToString(); - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for apply_gradient."); - - int width = @params["width"]?.ToObject() ?? 64; - int height = @params["height"]?.ToObject() ?? 64; - List warnings = new List(); - var dimensionError = ValidateDimensions(width, height, warnings); - if (dimensionError != null) - return dimensionError; - string gradientType = @params["gradientType"]?.ToString() ?? "linear"; - float angle = @params["gradientAngle"]?.ToObject() ?? 0f; - - var palette = TextureOps.ParsePalette(@params["palette"] as JArray); - if (palette == null || palette.Count < 2) - { - // Default gradient palette - palette = new List { new Color32(0, 0, 0, 255), new Color32(255, 255, 255, 255) }; - } - - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - EnsureDirectoryExists(fullPath); - - Texture2D texture = null; - try - { - texture = new Texture2D(width, height, TextureFormat.RGBA32, false); - - if (gradientType == "radial") - { - ApplyRadialGradient(texture, palette); - } - else - { - ApplyLinearGradient(texture, palette, angle); - } - - texture.Apply(); - - byte[] imageData = TextureOps.EncodeTexture(texture, fullPath); - if (imageData == null || imageData.Length == 0) - { - return new ErrorResponse($"Failed to encode texture for '{fullPath}'"); - } - File.WriteAllBytes(GetAbsolutePath(fullPath), imageData); - - AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); - - // Configure as sprite if requested - JToken spriteSettingsToken = @params["spriteSettings"]; - if (spriteSettingsToken != null) - { - ConfigureAsSprite(fullPath, spriteSettingsToken); - } - - foreach (var warning in warnings) - { - McpLog.Warn($"[ManageTexture] {warning}"); - } - - return new SuccessResponse( - $"Gradient texture created at '{fullPath}' ({width}x{height})", - new - { - path = fullPath, - width, - height, - gradientType, - warnings = warnings.Count > 0 ? warnings : null - } - ); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to create gradient texture: {e.Message}"); - } - finally - { - if (texture != null) - UnityEngine.Object.DestroyImmediate(texture); - } - } - - private static object ApplyNoise(JObject @params) - { - string path = @params["path"]?.ToString(); - if (string.IsNullOrEmpty(path)) - return new ErrorResponse("'path' is required for apply_noise."); - - int width = @params["width"]?.ToObject() ?? 64; - int height = @params["height"]?.ToObject() ?? 64; - List warnings = new List(); - var dimensionError = ValidateDimensions(width, height, warnings); - if (dimensionError != null) - return dimensionError; - float scale = @params["noiseScale"]?.ToObject() ?? 0.1f; - int octaves = @params["octaves"]?.ToObject() ?? 1; - if (octaves <= 0) - return new ErrorResponse("octaves must be greater than 0."); - long noiseWork = (long)width * height * octaves; - if (noiseWork > MaxNoiseWork) - warnings.Add($"Noise workload exceeds recommended max {MaxNoiseWork} (got {width}x{height}x{octaves})."); - - var palette = TextureOps.ParsePalette(@params["palette"] as JArray); - if (palette == null || palette.Count < 2) - { - palette = new List { new Color32(0, 0, 0, 255), new Color32(255, 255, 255, 255) }; - } - - string fullPath = AssetPathUtility.SanitizeAssetPath(path); - EnsureDirectoryExists(fullPath); - - Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false); - try - { - ApplyPerlinNoise(texture, palette, scale, octaves); - - texture.Apply(); - - byte[] imageData = TextureOps.EncodeTexture(texture, fullPath); - if (imageData == null || imageData.Length == 0) - { - return new ErrorResponse($"Failed to encode texture for '{fullPath}'"); - } - File.WriteAllBytes(GetAbsolutePath(fullPath), imageData); - - AssetDatabase.ImportAsset(fullPath, ImportAssetOptions.ForceUpdate); - - // Configure as sprite if requested - JToken spriteSettingsToken = @params["spriteSettings"]; - if (spriteSettingsToken != null) - { - ConfigureAsSprite(fullPath, spriteSettingsToken); - } - - foreach (var warning in warnings) - { - McpLog.Warn($"[ManageTexture] {warning}"); - } - - return new SuccessResponse( - $"Noise texture created at '{fullPath}' ({width}x{height})", - new - { - path = fullPath, - width, - height, - noiseScale = scale, - octaves, - warnings = warnings.Count > 0 ? warnings : null - } - ); - } - catch (Exception e) - { - return new ErrorResponse($"Failed to create noise texture: {e.Message}"); - } - finally - { - if (texture != null) - UnityEngine.Object.DestroyImmediate(texture); - } - } - - // --- Pattern Helpers --- - - private static void ApplyPatternToTexture(Texture2D texture, string pattern, List palette, int patternSize) - { - if (palette == null || palette.Count == 0) - { - palette = new List { new Color32(255, 255, 255, 255), new Color32(0, 0, 0, 255) }; - } - - int width = texture.width; - int height = texture.height; - - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - Color32 color = GetPatternColor(x, y, pattern, palette, patternSize, width, height); - texture.SetPixel(x, y, color); - } - } - } - - private static Color32 GetPatternColor(int x, int y, string pattern, List palette, int size, int width, int height) - { - int colorIndex = 0; - - switch (pattern.ToLower()) - { - case "checkerboard": - colorIndex = ((x / size) + (y / size)) % 2; - break; - - case "stripes": - case "stripes_v": - colorIndex = (x / size) % palette.Count; - break; - - case "stripes_h": - colorIndex = (y / size) % palette.Count; - break; - - case "stripes_diag": - colorIndex = ((x + y) / size) % palette.Count; - break; - - case "dots": - int cx = (x % (size * 2)) - size; - int cy = (y % (size * 2)) - size; - bool inDot = (cx * cx + cy * cy) < (size * size / 4); - colorIndex = inDot ? 1 : 0; - break; - - case "grid": - bool onGridLine = (x % size == 0) || (y % size == 0); - colorIndex = onGridLine ? 1 : 0; - break; - - case "brick": - int row = y / size; - int offset = (row % 2) * (size / 2); - bool onBorder = ((x + offset) % size == 0) || (y % size == 0); - colorIndex = onBorder ? 1 : 0; - break; - - default: - colorIndex = 0; - break; - } - - return palette[Mathf.Clamp(colorIndex, 0, palette.Count - 1)]; - } - - // --- Gradient Helpers --- - - private static void ApplyLinearGradient(Texture2D texture, List palette, float angle) - { - int width = texture.width; - int height = texture.height; - float radians = angle * Mathf.Deg2Rad; - Vector2 dir = new Vector2(Mathf.Cos(radians), Mathf.Sin(radians)); - float denomX = Mathf.Max(1, width - 1); - float denomY = Mathf.Max(1, height - 1); - - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - float nx = x / denomX; - float ny = y / denomY; - float t = Vector2.Dot(new Vector2(nx, ny), dir); - t = Mathf.Clamp01((t + 1f) / 2f); - - Color32 color = LerpPalette(palette, t); - texture.SetPixel(x, y, color); - } - } - } - - private static void ApplyRadialGradient(Texture2D texture, List palette) - { - int width = texture.width; - int height = texture.height; - float cx = width / 2f; - float cy = height / 2f; - float maxDist = Mathf.Sqrt(cx * cx + cy * cy); - - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - float dx = x - cx; - float dy = y - cy; - float dist = Mathf.Sqrt(dx * dx + dy * dy); - float t = Mathf.Clamp01(dist / maxDist); - - Color32 color = LerpPalette(palette, t); - texture.SetPixel(x, y, color); - } - } - } - - private static Color32 LerpPalette(List palette, float t) - { - if (palette.Count == 1) return palette[0]; - if (t <= 0) return palette[0]; - if (t >= 1) return palette[palette.Count - 1]; - - float scaledT = t * (palette.Count - 1); - int index = Mathf.FloorToInt(scaledT); - float localT = scaledT - index; - - if (index >= palette.Count - 1) - return palette[palette.Count - 1]; - - Color c1 = palette[index]; - Color c2 = palette[index + 1]; - return Color.Lerp(c1, c2, localT); - } - - // --- Noise Helpers --- - - private static void ApplyPerlinNoise(Texture2D texture, List palette, float scale, int octaves) - { - int width = texture.width; - int height = texture.height; - - // Random offset to ensure different patterns - float offsetX = UnityEngine.Random.Range(0f, 1000f); - float offsetY = UnityEngine.Random.Range(0f, 1000f); - - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - float noiseValue = 0f; - float amplitude = 1f; - float frequency = 1f; - float maxValue = 0f; - - for (int o = 0; o < octaves; o++) - { - float sampleX = (x + offsetX) * scale * frequency; - float sampleY = (y + offsetY) * scale * frequency; - noiseValue += Mathf.PerlinNoise(sampleX, sampleY) * amplitude; - maxValue += amplitude; - amplitude *= 0.5f; - frequency *= 2f; - } - - float t = Mathf.Clamp01(noiseValue / maxValue); - Color32 color = LerpPalette(palette, t); - texture.SetPixel(x, y, color); - } - } - } - - private static void ConfigureAsSprite(string path, JToken spriteSettings) - { - TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter; - if (importer == null) - { - McpLog.Warn($"[ManageTexture] Could not get TextureImporter for {path}"); - return; - } - - importer.textureType = TextureImporterType.Sprite; - importer.spriteImportMode = SpriteImportMode.Single; - - if (spriteSettings != null && spriteSettings.Type == JTokenType.Object) - { - var settings = spriteSettings as JObject; - - // Pivot - var pivotToken = settings["pivot"]; - if (pivotToken is JArray pivotArray && pivotArray.Count >= 2) - { - importer.spritePivot = new Vector2( - pivotArray[0].ToObject(), - pivotArray[1].ToObject() - ); - } - - // Pixels per unit - var ppuToken = settings["pixelsPerUnit"]; - if (ppuToken != null) - { - importer.spritePixelsPerUnit = ppuToken.ToObject(); - } - } - - importer.SaveAndReimport(); - } - - private static void ConfigureTextureImporter(string path, JToken importSettings) - { - TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter; - if (importer == null) - { - McpLog.Warn($"[ManageTexture] Could not get TextureImporter for {path}"); - return; - } - - if (importSettings == null || importSettings.Type != JTokenType.Object) - { - return; - } - - var settings = importSettings as JObject; - - // Texture Type - var textureTypeToken = settings["textureType"]; - if (textureTypeToken != null) - { - string typeStr = textureTypeToken.ToString(); - if (TryParseEnum(typeStr, out var textureType)) - { - importer.textureType = textureType; - } - } - - // Texture Shape - var textureShapeToken = settings["textureShape"]; - if (textureShapeToken != null) - { - string shapeStr = textureShapeToken.ToString(); - if (TryParseEnum(shapeStr, out var textureShape)) - { - importer.textureShape = textureShape; - } - } - - // sRGB - var srgbToken = settings["sRGBTexture"]; - if (srgbToken != null) - { - importer.sRGBTexture = srgbToken.ToObject(); - } - - // Alpha Source - var alphaSourceToken = settings["alphaSource"]; - if (alphaSourceToken != null) - { - string alphaStr = alphaSourceToken.ToString(); - if (TryParseEnum(alphaStr, out var alphaSource)) - { - importer.alphaSource = alphaSource; - } - } - - // Alpha Is Transparency - var alphaTransToken = settings["alphaIsTransparency"]; - if (alphaTransToken != null) - { - importer.alphaIsTransparency = alphaTransToken.ToObject(); - } - - // Readable - var readableToken = settings["isReadable"]; - if (readableToken != null) - { - importer.isReadable = readableToken.ToObject(); - } - - // Mipmaps - var mipmapToken = settings["mipmapEnabled"]; - if (mipmapToken != null) - { - importer.mipmapEnabled = mipmapToken.ToObject(); - } - - // Mipmap Filter - var mipmapFilterToken = settings["mipmapFilter"]; - if (mipmapFilterToken != null) - { - string filterStr = mipmapFilterToken.ToString(); - if (TryParseEnum(filterStr, out var mipmapFilter)) - { - importer.mipmapFilter = mipmapFilter; - } - } - - // Wrap Mode - var wrapModeToken = settings["wrapMode"]; - if (wrapModeToken != null) - { - string wrapStr = wrapModeToken.ToString(); - if (TryParseEnum(wrapStr, out var wrapMode)) - { - importer.wrapMode = wrapMode; - } - } - - // Wrap Mode U - var wrapModeUToken = settings["wrapModeU"]; - if (wrapModeUToken != null) - { - string wrapStr = wrapModeUToken.ToString(); - if (TryParseEnum(wrapStr, out var wrapMode)) - { - importer.wrapModeU = wrapMode; - } - } - - // Wrap Mode V - var wrapModeVToken = settings["wrapModeV"]; - if (wrapModeVToken != null) - { - string wrapStr = wrapModeVToken.ToString(); - if (TryParseEnum(wrapStr, out var wrapMode)) - { - importer.wrapModeV = wrapMode; - } - } - - // Filter Mode - var filterModeToken = settings["filterMode"]; - if (filterModeToken != null) - { - string filterStr = filterModeToken.ToString(); - if (TryParseEnum(filterStr, out var filterMode)) - { - importer.filterMode = filterMode; - } - } - - // Aniso Level - var anisoToken = settings["anisoLevel"]; - if (anisoToken != null) - { - importer.anisoLevel = anisoToken.ToObject(); - } - - // Max Texture Size - var maxSizeToken = settings["maxTextureSize"]; - if (maxSizeToken != null) - { - importer.maxTextureSize = maxSizeToken.ToObject(); - } - - // Compression - var compressionToken = settings["textureCompression"]; - if (compressionToken != null) - { - string compStr = compressionToken.ToString(); - if (TryParseEnum(compStr, out var compression)) - { - importer.textureCompression = compression; - } - } - - // Crunched Compression - var crunchedToken = settings["crunchedCompression"]; - if (crunchedToken != null) - { - importer.crunchedCompression = crunchedToken.ToObject(); - } - - // Compression Quality - var qualityToken = settings["compressionQuality"]; - if (qualityToken != null) - { - importer.compressionQuality = qualityToken.ToObject(); - } - - // --- Sprite-specific settings --- - - // Sprite Import Mode - var spriteModeToken = settings["spriteImportMode"]; - if (spriteModeToken != null) - { - string modeStr = spriteModeToken.ToString(); - if (TryParseEnum(modeStr, out var spriteMode)) - { - importer.spriteImportMode = spriteMode; - } - } - - // Sprite Pixels Per Unit - var ppuToken = settings["spritePixelsPerUnit"]; - if (ppuToken != null) - { - importer.spritePixelsPerUnit = ppuToken.ToObject(); - } - - // Sprite Pivot - var pivotToken = settings["spritePivot"]; - if (pivotToken is JArray pivotArray && pivotArray.Count >= 2) - { - importer.spritePivot = new Vector2( - pivotArray[0].ToObject(), - pivotArray[1].ToObject() - ); - } - - // Apply sprite settings using TextureImporterSettings helper - TextureImporterSettings importerSettings = new TextureImporterSettings(); - importer.ReadTextureSettings(importerSettings); - - bool settingsChanged = false; - - // Sprite Mesh Type - var meshTypeToken = settings["spriteMeshType"]; - if (meshTypeToken != null) - { - string meshStr = meshTypeToken.ToString(); - if (TryParseEnum(meshStr, out var meshType)) - { - importerSettings.spriteMeshType = meshType; - settingsChanged = true; - } - } - - // Sprite Extrude - var extrudeToken = settings["spriteExtrude"]; - if (extrudeToken != null) - { - importerSettings.spriteExtrude = (uint)extrudeToken.ToObject(); - settingsChanged = true; - } - - if (settingsChanged) - { - importer.SetTextureSettings(importerSettings); - } - - importer.SaveAndReimport(); - } - - private static bool TryParseEnum(string value, out T result) where T : struct - { - // Try exact match first - if (Enum.TryParse(value, true, out result)) - { - return true; - } - - // Try without common prefixes/suffixes - string cleanValue = value.Replace("_", "").Replace("-", ""); - if (Enum.TryParse(cleanValue, true, out result)) - { - return true; - } - - result = default; - return false; - } - - private static bool AssetExists(string path) - { - return !string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(path)); - } - - private static void EnsureDirectoryExists(string assetPath) - { - string directory = Path.GetDirectoryName(assetPath); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(GetAbsolutePath(directory))) - { - Directory.CreateDirectory(GetAbsolutePath(directory)); - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); - } - } - - private static string GetAbsolutePath(string assetPath) - { - return Path.Combine(Directory.GetCurrentDirectory(), assetPath); - } - - private static string ResolveImagePath(string imagePath) - { - if (Path.IsPathRooted(imagePath)) - return imagePath; - - return Path.Combine(Directory.GetCurrentDirectory(), imagePath); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/ManageTexture.cs.meta b/Assets/MCPForUnity/Editor/Tools/ManageTexture.cs.meta deleted file mode 100644 index 597e8c4..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ManageTexture.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 8028b64102744ea5aad53a762d48079a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/ManageTexture.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs b/Assets/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs deleted file mode 100644 index e4db3a4..0000000 --- a/Assets/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Marks a class as an MCP tool handler - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class McpForUnityToolAttribute : Attribute - { - /// - /// Tool name (if null, derived from class name) - /// - public string Name { get; set; } - - /// - /// Tool description for LLM - /// - public string Description { get; set; } - - /// - /// Whether this tool returns structured output - /// - public bool StructuredOutput { get; set; } = true; - - /// - /// Controls whether this tool is automatically registered with FastMCP. - /// Defaults to true so most tools opt-in automatically. Set to false - /// for legacy/built-in tools that already exist server-side. - /// - public bool AutoRegister { get; set; } = true; - - /// - /// Enables the polling middleware for long-running tools. When true, Unity - /// should return a PendingResponse and the Python side will poll using - /// until completion. - /// - public bool RequiresPolling { get; set; } = false; - - /// - /// The action name to use when polling for status. Defaults to "status". - /// - public string PollAction { get; set; } = "status"; - - /// - /// The command name used to route requests to this tool. - /// If not specified, defaults to the PascalCase class name converted to snake_case. - /// Kept for backward compatibility. - /// - public string CommandName - { - get => Name; - set => Name = value; - } - - /// - /// Create an MCP tool attribute with auto-generated command name. - /// The command name will be derived from the class name (PascalCase → snake_case). - /// Example: ManageAsset → manage_asset - /// - public McpForUnityToolAttribute() - { - Name = null; // Will be auto-generated - } - - /// - /// Create an MCP tool attribute with explicit command name. - /// - /// The command name (e.g., "manage_asset") - public McpForUnityToolAttribute(string name = null) - { - Name = name; - } - } - - /// - /// Describes a tool parameter - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] - public class ToolParameterAttribute : Attribute - { - /// - /// Parameter name (if null, derived from property/field name) - /// - public string Name { get; } - - /// - /// Parameter description for LLM - /// - public string Description { get; set; } - - /// - /// Whether this parameter is required - /// - public bool Required { get; set; } = true; - - /// - /// Default value (as string) - /// - public string DefaultValue { get; set; } - - public ToolParameterAttribute(string description) - { - Description = description; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs.meta b/Assets/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs.meta deleted file mode 100644 index 70dad0a..0000000 --- a/Assets/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 804d07b886f4e4eb39316bbef34687c7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Prefabs.meta b/Assets/MCPForUnity/Editor/Tools/Prefabs.meta deleted file mode 100644 index 4fb95c5..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Prefabs.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 1bd48a1b7555c46bba168078ce0291cc -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs b/Assets/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs deleted file mode 100644 index 6df9d47..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs +++ /dev/null @@ -1,982 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; -using UnityEngine.SceneManagement; - -namespace MCPForUnity.Editor.Tools.Prefabs -{ - [McpForUnityTool("manage_prefabs", AutoRegister = false)] - /// - /// Tool to manage Unity Prefabs: create, inspect, and modify prefab assets. - /// Uses headless editing (no UI, no dialogs) for reliable automated workflows. - /// - public static class ManagePrefabs - { - // Action constants - private const string ACTION_CREATE_FROM_GAMEOBJECT = "create_from_gameobject"; - private const string ACTION_GET_INFO = "get_info"; - private const string ACTION_GET_HIERARCHY = "get_hierarchy"; - private const string ACTION_MODIFY_CONTENTS = "modify_contents"; - private const string SupportedActions = ACTION_CREATE_FROM_GAMEOBJECT + ", " + ACTION_GET_INFO + ", " + ACTION_GET_HIERARCHY + ", " + ACTION_MODIFY_CONTENTS; - - public static object HandleCommand(JObject @params) - { - if (@params == null) - { - return new ErrorResponse("Parameters cannot be null."); - } - - string action = @params["action"]?.ToString()?.ToLowerInvariant(); - if (string.IsNullOrEmpty(action)) - { - return new ErrorResponse($"Action parameter is required. Valid actions are: {SupportedActions}."); - } - - try - { - switch (action) - { - case ACTION_CREATE_FROM_GAMEOBJECT: - return CreatePrefabFromGameObject(@params); - case ACTION_GET_INFO: - return GetInfo(@params); - case ACTION_GET_HIERARCHY: - return GetHierarchy(@params); - case ACTION_MODIFY_CONTENTS: - return ModifyContents(@params); - default: - return new ErrorResponse($"Unknown action: '{action}'. Valid actions are: {SupportedActions}."); - } - } - catch (Exception e) - { - McpLog.Error($"[ManagePrefabs] Action '{action}' failed: {e}"); - return new ErrorResponse($"Internal error: {e.Message}"); - } - } - - #region Create Prefab from GameObject - - /// - /// Creates a prefab asset from a GameObject in the scene. - /// - private static object CreatePrefabFromGameObject(JObject @params) - { - // 1. Validate and parse parameters - var validation = ValidateCreatePrefabParams(@params); - if (!validation.isValid) - { - return new ErrorResponse(validation.errorMessage); - } - - string targetName = validation.targetName; - string finalPath = validation.finalPath; - bool includeInactive = validation.includeInactive; - bool replaceExisting = validation.replaceExisting; - bool unlinkIfInstance = validation.unlinkIfInstance; - - // 2. Find the source object - GameObject sourceObject = FindSceneObjectByName(targetName, includeInactive); - if (sourceObject == null) - { - return new ErrorResponse($"GameObject '{targetName}' not found in the active scene or prefab stage{(includeInactive ? " (including inactive objects)" : "")}."); - } - - // 3. Validate source object state - var objectValidation = ValidateSourceObjectForPrefab(sourceObject, unlinkIfInstance); - if (!objectValidation.isValid) - { - return new ErrorResponse(objectValidation.errorMessage); - } - - // 4. Check for path conflicts and track if file will be replaced - bool fileExistedAtPath = AssetDatabase.LoadAssetAtPath(finalPath) != null; - - if (!replaceExisting && fileExistedAtPath) - { - finalPath = AssetDatabase.GenerateUniqueAssetPath(finalPath); - McpLog.Info($"[ManagePrefabs] Generated unique path: {finalPath}"); - } - - // 5. Ensure directory exists - EnsureAssetDirectoryExists(finalPath); - - // 6. Unlink from existing prefab if needed - if (unlinkIfInstance && objectValidation.shouldUnlink) - { - try - { - // UnpackPrefabInstance requires the prefab instance root, not a child object - GameObject rootToUnlink = PrefabUtility.GetOutermostPrefabInstanceRoot(sourceObject); - if (rootToUnlink != null) - { - PrefabUtility.UnpackPrefabInstance(rootToUnlink, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction); - McpLog.Info($"[ManagePrefabs] Unpacked prefab instance '{rootToUnlink.name}' before creating new prefab."); - } - } - catch (Exception e) - { - return new ErrorResponse($"Failed to unlink prefab instance: {e.Message}"); - } - } - - // 7. Create the prefab - try - { - GameObject result = CreatePrefabAsset(sourceObject, finalPath, replaceExisting); - - if (result == null) - { - return new ErrorResponse($"Failed to create prefab asset at '{finalPath}'."); - } - - // 8. Select the newly created instance - Selection.activeGameObject = result; - - return new SuccessResponse( - $"Prefab created at '{finalPath}' and instance linked.", - new - { - prefabPath = finalPath, - instanceId = result.GetInstanceID(), - instanceName = result.name, - wasUnlinked = unlinkIfInstance && objectValidation.shouldUnlink, - wasReplaced = replaceExisting && fileExistedAtPath, - componentCount = result.GetComponents().Length, - childCount = result.transform.childCount - } - ); - } - catch (Exception e) - { - McpLog.Error($"[ManagePrefabs] Error creating prefab at '{finalPath}': {e}"); - return new ErrorResponse($"Error saving prefab asset: {e.Message}"); - } - } - - /// - /// Validates parameters for creating a prefab from GameObject. - /// - private static (bool isValid, string errorMessage, string targetName, string finalPath, bool includeInactive, bool replaceExisting, bool unlinkIfInstance) - ValidateCreatePrefabParams(JObject @params) - { - string targetName = @params["target"]?.ToString() ?? @params["name"]?.ToString(); - if (string.IsNullOrEmpty(targetName)) - { - return (false, "'target' parameter is required for create_from_gameobject.", null, null, false, false, false); - } - - string requestedPath = @params["prefabPath"]?.ToString(); - if (string.IsNullOrWhiteSpace(requestedPath)) - { - return (false, "'prefabPath' parameter is required for create_from_gameobject.", targetName, null, false, false, false); - } - - string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath); - if (sanitizedPath == null) - { - return (false, $"Invalid prefab path (path traversal detected): '{requestedPath}'", targetName, null, false, false, false); - } - if (string.IsNullOrEmpty(sanitizedPath)) - { - return (false, $"Invalid prefab path '{requestedPath}'. Path cannot be empty.", targetName, null, false, false, false); - } - if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) - { - sanitizedPath += ".prefab"; - } - - // Validate path is within Assets folder - if (!sanitizedPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) - { - return (false, $"Prefab path must be within the Assets folder. Got: '{sanitizedPath}'", targetName, null, false, false, false); - } - - bool includeInactive = @params["searchInactive"]?.ToObject() ?? false; - bool replaceExisting = @params["allowOverwrite"]?.ToObject() ?? false; - bool unlinkIfInstance = @params["unlinkIfInstance"]?.ToObject() ?? false; - - return (true, null, targetName, sanitizedPath, includeInactive, replaceExisting, unlinkIfInstance); - } - - /// - /// Validates source object can be converted to prefab. - /// - private static (bool isValid, string errorMessage, bool shouldUnlink, string existingPrefabPath) - ValidateSourceObjectForPrefab(GameObject sourceObject, bool unlinkIfInstance) - { - // Check if this is a Prefab Asset (the .prefab file itself in the editor) - if (PrefabUtility.IsPartOfPrefabAsset(sourceObject)) - { - return (false, - $"GameObject '{sourceObject.name}' is part of a prefab asset. " + - "Open the prefab stage to save changes instead.", - false, null); - } - - // Check if this is already a Prefab Instance - PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(sourceObject); - if (status != PrefabInstanceStatus.NotAPrefab) - { - string existingPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(sourceObject); - - if (!unlinkIfInstance) - { - return (false, - $"GameObject '{sourceObject.name}' is already linked to prefab '{existingPath}'. " + - "Set 'unlinkIfInstance' to true to unlink it first, or modify the existing prefab instead.", - false, existingPath); - } - - // Needs to be unlinked - return (true, null, true, existingPath); - } - - return (true, null, false, null); - } - - /// - /// Creates a prefab asset from a GameObject. - /// - private static GameObject CreatePrefabAsset(GameObject sourceObject, string path, bool replaceExisting) - { - GameObject result = PrefabUtility.SaveAsPrefabAssetAndConnect( - sourceObject, - path, - InteractionMode.AutomatedAction - ); - - string action = replaceExisting ? "Replaced existing" : "Created new"; - McpLog.Info($"[ManagePrefabs] {action} prefab at '{path}'."); - - if (result != null) - { - AssetDatabase.SaveAssets(); - AssetDatabase.Refresh(); - } - - return result; - } - - #endregion - - /// - /// Ensures the directory for an asset path exists, creating it if necessary. - /// - private static void EnsureAssetDirectoryExists(string assetPath) - { - string directory = Path.GetDirectoryName(assetPath); - if (string.IsNullOrEmpty(directory)) - { - return; - } - - // Use Application.dataPath for more reliable path resolution - // Application.dataPath points to the Assets folder (e.g., ".../ProjectName/Assets") - string assetsPath = Application.dataPath; - string projectRoot = Path.GetDirectoryName(assetsPath); - string fullDirectory = Path.Combine(projectRoot, directory); - - if (!Directory.Exists(fullDirectory)) - { - Directory.CreateDirectory(fullDirectory); - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); - McpLog.Info($"[ManagePrefabs] Created directory: {directory}"); - } - } - - /// - /// Finds a GameObject by name in the active scene or current prefab stage. - /// - private static GameObject FindSceneObjectByName(string name, bool includeInactive) - { - // First check if we're in Prefab Stage - PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage(); - if (stage?.prefabContentsRoot != null) - { - foreach (Transform transform in stage.prefabContentsRoot.GetComponentsInChildren(includeInactive)) - { - if (transform.name == name && (includeInactive || transform.gameObject.activeSelf)) - { - return transform.gameObject; - } - } - } - - // Search in the active scene - Scene activeScene = SceneManager.GetActiveScene(); - foreach (GameObject root in activeScene.GetRootGameObjects()) - { - // Check the root object itself - if (root.name == name && (includeInactive || root.activeSelf)) - { - return root; - } - - // Check children - foreach (Transform transform in root.GetComponentsInChildren(includeInactive)) - { - if (transform.name == name && (includeInactive || transform.gameObject.activeSelf)) - { - return transform.gameObject; - } - } - } - - return null; - } - - #region Read Operations - - /// - /// Gets basic metadata information about a prefab asset. - /// - private static object GetInfo(JObject @params) - { - string prefabPath = @params["prefabPath"]?.ToString() ?? @params["path"]?.ToString(); - if (string.IsNullOrEmpty(prefabPath)) - { - return new ErrorResponse("'prefabPath' parameter is required for get_info."); - } - - string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath); - if (string.IsNullOrEmpty(sanitizedPath)) - { - return new ErrorResponse($"Invalid prefab path: '{prefabPath}'."); - } - GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(sanitizedPath); - if (prefabAsset == null) - { - return new ErrorResponse($"No prefab asset found at path '{sanitizedPath}'."); - } - - string guid = PrefabUtilityHelper.GetPrefabGUID(sanitizedPath); - PrefabAssetType assetType = PrefabUtility.GetPrefabAssetType(prefabAsset); - string prefabTypeString = assetType.ToString(); - var componentTypes = PrefabUtilityHelper.GetComponentTypeNames(prefabAsset); - int childCount = PrefabUtilityHelper.CountChildrenRecursive(prefabAsset.transform); - var (isVariant, parentPrefab, _) = PrefabUtilityHelper.GetVariantInfo(prefabAsset); - - return new SuccessResponse( - $"Successfully retrieved prefab info.", - new - { - assetPath = sanitizedPath, - guid = guid, - prefabType = prefabTypeString, - rootObjectName = prefabAsset.name, - rootComponentTypes = componentTypes, - childCount = childCount, - isVariant = isVariant, - parentPrefab = parentPrefab - } - ); - } - - /// - /// Gets the hierarchical structure of a prefab asset. - /// Returns all objects in the prefab for full client-side filtering and search. - /// - private static object GetHierarchy(JObject @params) - { - string prefabPath = @params["prefabPath"]?.ToString() ?? @params["path"]?.ToString(); - if (string.IsNullOrEmpty(prefabPath)) - { - return new ErrorResponse("'prefabPath' parameter is required for get_hierarchy."); - } - - string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath); - if (string.IsNullOrEmpty(sanitizedPath)) - { - return new ErrorResponse($"Invalid prefab path '{prefabPath}'. Path traversal sequences are not allowed."); - } - - // Load prefab contents in background (without opening stage UI) - GameObject prefabContents = PrefabUtility.LoadPrefabContents(sanitizedPath); - if (prefabContents == null) - { - return new ErrorResponse($"Failed to load prefab contents from '{sanitizedPath}'."); - } - - try - { - // Build complete hierarchy items (no pagination) - var allItems = BuildHierarchyItems(prefabContents.transform, sanitizedPath); - - return new SuccessResponse( - $"Successfully retrieved prefab hierarchy. Found {allItems.Count} objects.", - new - { - prefabPath = sanitizedPath, - total = allItems.Count, - items = allItems - } - ); - } - finally - { - // Always unload prefab contents to free memory - PrefabUtility.UnloadPrefabContents(prefabContents); - } - } - - #endregion - - #region Headless Prefab Editing - - /// - /// Modifies a prefab's contents directly without opening the prefab stage. - /// This is ideal for automated/agentic workflows as it avoids UI, dirty flags, and dialogs. - /// - private static object ModifyContents(JObject @params) - { - string prefabPath = @params["prefabPath"]?.ToString() ?? @params["path"]?.ToString(); - if (string.IsNullOrEmpty(prefabPath)) - { - return new ErrorResponse("'prefabPath' parameter is required for modify_contents."); - } - - string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath); - if (string.IsNullOrEmpty(sanitizedPath)) - { - return new ErrorResponse($"Invalid prefab path '{prefabPath}'. Path traversal sequences are not allowed."); - } - - // Load prefab contents in isolated context (no UI) - GameObject prefabContents = PrefabUtility.LoadPrefabContents(sanitizedPath); - if (prefabContents == null) - { - return new ErrorResponse($"Failed to load prefab contents from '{sanitizedPath}'."); - } - - try - { - // Find target object within the prefab (defaults to root) - string targetName = @params["target"]?.ToString(); - GameObject targetGo = FindInPrefabContents(prefabContents, targetName); - - if (targetGo == null) - { - string searchedFor = string.IsNullOrEmpty(targetName) ? "root" : $"'{targetName}'"; - return new ErrorResponse($"Target {searchedFor} not found in prefab '{sanitizedPath}'."); - } - - // Apply modifications - var modifyResult = ApplyModificationsToPrefabObject(targetGo, @params, prefabContents); - if (modifyResult.error != null) - { - return modifyResult.error; - } - - // Skip saving when no modifications were made to avoid unnecessary asset writes - if (!modifyResult.modified) - { - return new SuccessResponse( - $"Prefab '{sanitizedPath}' is already up to date; no changes were applied.", - new - { - prefabPath = sanitizedPath, - targetName = targetGo.name, - modified = false - } - ); - } - - // Save the prefab - bool success; - PrefabUtility.SaveAsPrefabAsset(prefabContents, sanitizedPath, out success); - - if (!success) - { - return new ErrorResponse($"Failed to save prefab asset at '{sanitizedPath}'."); - } - - AssetDatabase.Refresh(); - - McpLog.Info($"[ManagePrefabs] Successfully modified and saved prefab '{sanitizedPath}' (headless)."); - - return new SuccessResponse( - $"Prefab '{sanitizedPath}' modified and saved successfully.", - new - { - prefabPath = sanitizedPath, - targetName = targetGo.name, - modified = modifyResult.modified, - transform = new - { - position = new { x = targetGo.transform.localPosition.x, y = targetGo.transform.localPosition.y, z = targetGo.transform.localPosition.z }, - rotation = new { x = targetGo.transform.localEulerAngles.x, y = targetGo.transform.localEulerAngles.y, z = targetGo.transform.localEulerAngles.z }, - scale = new { x = targetGo.transform.localScale.x, y = targetGo.transform.localScale.y, z = targetGo.transform.localScale.z } - }, - componentTypes = PrefabUtilityHelper.GetComponentTypeNames(targetGo) - } - ); - } - finally - { - // Always unload prefab contents to free memory - PrefabUtility.UnloadPrefabContents(prefabContents); - } - } - - /// - /// Finds a GameObject within loaded prefab contents by name or path. - /// - private static GameObject FindInPrefabContents(GameObject prefabContents, string target) - { - if (string.IsNullOrEmpty(target)) - { - // Return root if no target specified - return prefabContents; - } - - // Try to find by path first (e.g., "Parent/Child/Target") - if (target.Contains("/")) - { - Transform found = prefabContents.transform.Find(target); - if (found != null) - { - return found.gameObject; - } - - // If path starts with root name, try without it - if (target.StartsWith(prefabContents.name + "/")) - { - string relativePath = target.Substring(prefabContents.name.Length + 1); - found = prefabContents.transform.Find(relativePath); - if (found != null) - { - return found.gameObject; - } - } - } - - // Check if target matches root name - if (prefabContents.name == target) - { - return prefabContents; - } - - // Search by name in hierarchy - foreach (Transform t in prefabContents.GetComponentsInChildren(true)) - { - if (t.gameObject.name == target) - { - return t.gameObject; - } - } - - return null; - } - - /// - /// Applies modifications to a GameObject within loaded prefab contents. - /// Returns (modified: bool, error: ErrorResponse or null). - /// - private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabObject(GameObject targetGo, JObject @params, GameObject prefabRoot) - { - bool modified = false; - - // Name change - string newName = @params["name"]?.ToString(); - if (!string.IsNullOrEmpty(newName) && targetGo.name != newName) - { - // If renaming the root, this will affect the prefab asset name on save - targetGo.name = newName; - modified = true; - } - - // Active state - bool? setActive = @params["setActive"]?.ToObject(); - if (setActive.HasValue && targetGo.activeSelf != setActive.Value) - { - targetGo.SetActive(setActive.Value); - modified = true; - } - - // Tag - string tag = @params["tag"]?.ToString(); - if (tag != null && targetGo.tag != tag) - { - string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag; - try - { - targetGo.tag = tagToSet; - modified = true; - } - catch (Exception ex) - { - return (false, new ErrorResponse($"Failed to set tag to '{tagToSet}': {ex.Message}")); - } - } - - // Layer - string layerName = @params["layer"]?.ToString(); - if (!string.IsNullOrEmpty(layerName)) - { - int layerId = LayerMask.NameToLayer(layerName); - if (layerId == -1) - { - return (false, new ErrorResponse($"Invalid layer specified: '{layerName}'. Use a valid layer name.")); - } - if (targetGo.layer != layerId) - { - targetGo.layer = layerId; - modified = true; - } - } - - // Transform: position, rotation, scale - Vector3? position = VectorParsing.ParseVector3(@params["position"]); - Vector3? rotation = VectorParsing.ParseVector3(@params["rotation"]); - Vector3? scale = VectorParsing.ParseVector3(@params["scale"]); - - if (position.HasValue && targetGo.transform.localPosition != position.Value) - { - targetGo.transform.localPosition = position.Value; - modified = true; - } - if (rotation.HasValue && targetGo.transform.localEulerAngles != rotation.Value) - { - targetGo.transform.localEulerAngles = rotation.Value; - modified = true; - } - if (scale.HasValue && targetGo.transform.localScale != scale.Value) - { - targetGo.transform.localScale = scale.Value; - modified = true; - } - - // Parent change (within prefab hierarchy) - JToken parentToken = @params["parent"]; - if (parentToken != null) - { - string parentTarget = parentToken.ToString(); - Transform newParent = null; - - if (!string.IsNullOrEmpty(parentTarget)) - { - GameObject parentGo = FindInPrefabContents(prefabRoot, parentTarget); - if (parentGo == null) - { - return (false, new ErrorResponse($"Parent '{parentTarget}' not found in prefab.")); - } - if (parentGo.transform.IsChildOf(targetGo.transform)) - { - return (false, new ErrorResponse($"Cannot parent '{targetGo.name}' to '{parentGo.name}' as it would create a hierarchy loop.")); - } - newParent = parentGo.transform; - } - - if (targetGo.transform.parent != newParent) - { - targetGo.transform.SetParent(newParent, true); - modified = true; - } - } - - // Components to add - if (@params["componentsToAdd"] is JArray componentsToAdd) - { - foreach (var compToken in componentsToAdd) - { - string typeName = compToken.Type == JTokenType.String - ? compToken.ToString() - : (compToken as JObject)?["typeName"]?.ToString(); - - if (!string.IsNullOrEmpty(typeName)) - { - if (!ComponentResolver.TryResolve(typeName, out Type componentType, out string error)) - { - return (false, new ErrorResponse($"Component type '{typeName}' not found: {error}")); - } - targetGo.AddComponent(componentType); - modified = true; - } - } - } - - // Components to remove - if (@params["componentsToRemove"] is JArray componentsToRemove) - { - foreach (var compToken in componentsToRemove) - { - string typeName = compToken.ToString(); - if (!string.IsNullOrEmpty(typeName)) - { - if (!ComponentResolver.TryResolve(typeName, out Type componentType, out string error)) - { - return (false, new ErrorResponse($"Component type '{typeName}' not found: {error}")); - } - Component comp = targetGo.GetComponent(componentType); - if (comp != null) - { - UnityEngine.Object.DestroyImmediate(comp); - modified = true; - } - } - } - } - - // Create child GameObjects (supports single object or array) - JToken createChildToken = @params["createChild"] ?? @params["create_child"]; - if (createChildToken != null) - { - // Handle array of children - if (createChildToken is JArray childArray) - { - foreach (var childToken in childArray) - { - var childResult = CreateSingleChildInPrefab(childToken, targetGo, prefabRoot); - if (childResult.error != null) - { - return (false, childResult.error); - } - if (childResult.created) - { - modified = true; - } - } - } - else - { - // Handle single child object - var childResult = CreateSingleChildInPrefab(createChildToken, targetGo, prefabRoot); - if (childResult.error != null) - { - return (false, childResult.error); - } - if (childResult.created) - { - modified = true; - } - } - } - - return (modified, null); - } - - /// - /// Creates a single child GameObject within the prefab contents. - /// - private static (bool created, ErrorResponse error) CreateSingleChildInPrefab(JToken createChildToken, GameObject defaultParent, GameObject prefabRoot) - { - JObject childParams; - if (createChildToken is JObject obj) - { - childParams = obj; - } - else - { - return (false, new ErrorResponse("'create_child' must be an object with child properties.")); - } - - // Required: name - string childName = childParams["name"]?.ToString(); - if (string.IsNullOrEmpty(childName)) - { - return (false, new ErrorResponse("'create_child.name' is required.")); - } - - // Optional: parent (defaults to the target object) - string parentName = childParams["parent"]?.ToString(); - Transform parentTransform = defaultParent.transform; - if (!string.IsNullOrEmpty(parentName)) - { - GameObject parentGo = FindInPrefabContents(prefabRoot, parentName); - if (parentGo == null) - { - return (false, new ErrorResponse($"Parent '{parentName}' not found in prefab for create_child.")); - } - parentTransform = parentGo.transform; - } - - // Create the GameObject - GameObject newChild; - string primitiveType = childParams["primitiveType"]?.ToString() ?? childParams["primitive_type"]?.ToString(); - if (!string.IsNullOrEmpty(primitiveType)) - { - try - { - PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true); - newChild = GameObject.CreatePrimitive(type); - newChild.name = childName; - } - catch (ArgumentException) - { - return (false, new ErrorResponse($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}")); - } - } - else - { - newChild = new GameObject(childName); - } - - // Set parent - newChild.transform.SetParent(parentTransform, false); - - // Apply transform properties - Vector3? position = VectorParsing.ParseVector3(childParams["position"]); - Vector3? rotation = VectorParsing.ParseVector3(childParams["rotation"]); - Vector3? scale = VectorParsing.ParseVector3(childParams["scale"]); - - if (position.HasValue) - { - newChild.transform.localPosition = position.Value; - } - if (rotation.HasValue) - { - newChild.transform.localEulerAngles = rotation.Value; - } - if (scale.HasValue) - { - newChild.transform.localScale = scale.Value; - } - - // Add components - JArray componentsToAdd = childParams["componentsToAdd"] as JArray ?? childParams["components_to_add"] as JArray; - if (componentsToAdd != null) - { - for (int i = 0; i < componentsToAdd.Count; i++) - { - var compToken = componentsToAdd[i]; - string typeName = compToken.Type == JTokenType.String - ? compToken.ToString() - : (compToken as JObject)?["typeName"]?.ToString(); - - if (string.IsNullOrEmpty(typeName)) - { - // Clean up partially created child - UnityEngine.Object.DestroyImmediate(newChild); - return (false, new ErrorResponse($"create_child.components_to_add[{i}] must be a string or object with 'typeName' field, got {compToken.Type}")); - } - - if (!ComponentResolver.TryResolve(typeName, out Type componentType, out string error)) - { - // Clean up partially created child - UnityEngine.Object.DestroyImmediate(newChild); - return (false, new ErrorResponse($"Component type '{typeName}' not found for create_child: {error}")); - } - newChild.AddComponent(componentType); - } - } - - // Set tag if specified - string tag = childParams["tag"]?.ToString(); - if (!string.IsNullOrEmpty(tag)) - { - try - { - newChild.tag = tag; - } - catch (Exception ex) - { - UnityEngine.Object.DestroyImmediate(newChild); - return (false, new ErrorResponse($"Failed to set tag '{tag}' on child '{childName}': {ex.Message}")); - } - } - - // Set layer if specified - string layerName = childParams["layer"]?.ToString(); - if (!string.IsNullOrEmpty(layerName)) - { - int layerId = LayerMask.NameToLayer(layerName); - if (layerId == -1) - { - UnityEngine.Object.DestroyImmediate(newChild); - return (false, new ErrorResponse($"Invalid layer '{layerName}' for child '{childName}'. Use a valid layer name.")); - } - newChild.layer = layerId; - } - - // Set active state - bool? setActive = childParams["setActive"]?.ToObject() ?? childParams["set_active"]?.ToObject(); - if (setActive.HasValue) - { - newChild.SetActive(setActive.Value); - } - - McpLog.Info($"[ManagePrefabs] Created child '{childName}' under '{parentTransform.name}' in prefab."); - return (true, null); - } - - #endregion - - #region Hierarchy Builder - - /// - /// Builds a flat list of hierarchy items from a transform root. - /// - /// The root transform of the prefab. - /// Asset path of the main prefab. - /// List of hierarchy items with prefab information. - private static List BuildHierarchyItems(Transform root, string mainPrefabPath) - { - var items = new List(); - BuildHierarchyItemsRecursive(root, root, mainPrefabPath, "", items); - return items; - } - - /// - /// Recursively builds hierarchy items. - /// - /// Current transform being processed. - /// Root transform of the main prefab asset. - /// Asset path of the main prefab. - /// Parent path for building full hierarchy path. - /// List to accumulate hierarchy items. - private static void BuildHierarchyItemsRecursive(Transform transform, Transform mainPrefabRoot, string mainPrefabPath, string parentPath, List items) - { - if (transform == null) return; - - string name = transform.gameObject.name; - string path = string.IsNullOrEmpty(parentPath) ? name : $"{parentPath}/{name}"; - int instanceId = transform.gameObject.GetInstanceID(); - bool activeSelf = transform.gameObject.activeSelf; - int childCount = transform.childCount; - var componentTypes = PrefabUtilityHelper.GetComponentTypeNames(transform.gameObject); - - // Prefab information - bool isNestedPrefab = PrefabUtility.IsAnyPrefabInstanceRoot(transform.gameObject); - bool isPrefabRoot = transform == mainPrefabRoot; - int nestingDepth = isPrefabRoot ? 0 : PrefabUtilityHelper.GetPrefabNestingDepth(transform.gameObject, mainPrefabRoot); - string parentPrefabPath = isNestedPrefab && !isPrefabRoot - ? PrefabUtilityHelper.GetParentPrefabPath(transform.gameObject, mainPrefabRoot) - : null; - string nestedPrefabPath = isNestedPrefab ? PrefabUtilityHelper.GetNestedPrefabPath(transform.gameObject) : null; - - var item = new - { - name = name, - instanceId = instanceId, - path = path, - activeSelf = activeSelf, - childCount = childCount, - componentTypes = componentTypes, - prefab = new - { - isRoot = isPrefabRoot, - isNestedRoot = isNestedPrefab, - nestingDepth = nestingDepth, - assetPath = isNestedPrefab ? nestedPrefabPath : mainPrefabPath, - parentPath = parentPrefabPath - } - }; - - items.Add(item); - - // Recursively process children - foreach (Transform child in transform) - { - BuildHierarchyItemsRecursive(child, mainPrefabRoot, mainPrefabPath, path, items); - } - } - - #endregion - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs.meta b/Assets/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs.meta deleted file mode 100644 index 0a87366..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: c14e76b2aa7bb4570a88903b061e946e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/ReadConsole.cs b/Assets/MCPForUnity/Editor/Tools/ReadConsole.cs deleted file mode 100644 index 342f7b1..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ReadConsole.cs +++ /dev/null @@ -1,641 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using MCPForUnity.Editor.Helpers; // For Response class -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditorInternal; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Handles reading and clearing Unity Editor console log entries. - /// Uses reflection to access internal LogEntry methods/properties. - /// - [McpForUnityTool("read_console", AutoRegister = false)] - public static class ReadConsole - { - // (Calibration removed) - - // Reflection members for accessing internal LogEntry data - // private static MethodInfo _getEntriesMethod; // Removed as it's unused and fails reflection - private static MethodInfo _startGettingEntriesMethod; - private static MethodInfo _endGettingEntriesMethod; // Renamed from _stopGettingEntriesMethod, trying End... - private static MethodInfo _clearMethod; - private static MethodInfo _getCountMethod; - private static MethodInfo _getEntryMethod; - private static FieldInfo _modeField; - private static FieldInfo _messageField; - private static FieldInfo _fileField; - private static FieldInfo _lineField; - private static FieldInfo _instanceIdField; - - // Note: Timestamp is not directly available in LogEntry; need to parse message or find alternative? - - // Static constructor for reflection setup - static ReadConsole() - { - try - { - Type logEntriesType = typeof(EditorApplication).Assembly.GetType( - "UnityEditor.LogEntries" - ); - if (logEntriesType == null) - throw new Exception("Could not find internal type UnityEditor.LogEntries"); - - - - // Include NonPublic binding flags as internal APIs might change accessibility - BindingFlags staticFlags = - BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; - BindingFlags instanceFlags = - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - - _startGettingEntriesMethod = logEntriesType.GetMethod( - "StartGettingEntries", - staticFlags - ); - if (_startGettingEntriesMethod == null) - throw new Exception("Failed to reflect LogEntries.StartGettingEntries"); - - // Try reflecting EndGettingEntries based on warning message - _endGettingEntriesMethod = logEntriesType.GetMethod( - "EndGettingEntries", - staticFlags - ); - if (_endGettingEntriesMethod == null) - throw new Exception("Failed to reflect LogEntries.EndGettingEntries"); - - _clearMethod = logEntriesType.GetMethod("Clear", staticFlags); - if (_clearMethod == null) - throw new Exception("Failed to reflect LogEntries.Clear"); - - _getCountMethod = logEntriesType.GetMethod("GetCount", staticFlags); - if (_getCountMethod == null) - throw new Exception("Failed to reflect LogEntries.GetCount"); - - _getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", staticFlags); - if (_getEntryMethod == null) - throw new Exception("Failed to reflect LogEntries.GetEntryInternal"); - - Type logEntryType = typeof(EditorApplication).Assembly.GetType( - "UnityEditor.LogEntry" - ); - if (logEntryType == null) - throw new Exception("Could not find internal type UnityEditor.LogEntry"); - - _modeField = logEntryType.GetField("mode", instanceFlags); - if (_modeField == null) - throw new Exception("Failed to reflect LogEntry.mode"); - - _messageField = logEntryType.GetField("message", instanceFlags); - if (_messageField == null) - throw new Exception("Failed to reflect LogEntry.message"); - - _fileField = logEntryType.GetField("file", instanceFlags); - if (_fileField == null) - throw new Exception("Failed to reflect LogEntry.file"); - - _lineField = logEntryType.GetField("line", instanceFlags); - if (_lineField == null) - throw new Exception("Failed to reflect LogEntry.line"); - - _instanceIdField = logEntryType.GetField("instanceID", instanceFlags); - if (_instanceIdField == null) - throw new Exception("Failed to reflect LogEntry.instanceID"); - - // (Calibration removed) - - } - catch (Exception e) - { - McpLog.Error( - $"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries/LogEntry. Console reading/clearing will likely fail. Specific Error: {e.Message}" - ); - // Set members to null to prevent NullReferenceExceptions later, HandleCommand should check this. - _startGettingEntriesMethod = - _endGettingEntriesMethod = - _clearMethod = - _getCountMethod = - _getEntryMethod = - null; - _modeField = _messageField = _fileField = _lineField = _instanceIdField = null; - } - } - - // --- Main Handler --- - - public static object HandleCommand(JObject @params) - { - // Check if ALL required reflection members were successfully initialized. - if ( - _startGettingEntriesMethod == null - || _endGettingEntriesMethod == null - || _clearMethod == null - || _getCountMethod == null - || _getEntryMethod == null - || _modeField == null - || _messageField == null - || _fileField == null - || _lineField == null - || _instanceIdField == null - ) - { - // Log the error here as well for easier debugging in Unity Console - McpLog.Error( - "[ReadConsole] HandleCommand called but reflection members are not initialized. Static constructor might have failed silently or there's an issue." - ); - return new ErrorResponse( - "ReadConsole handler failed to initialize due to reflection errors. Cannot access console logs." - ); - } - - if (@params == null) - { - return new ErrorResponse("Parameters cannot be null."); - } - - var p = new ToolParams(@params); - string action = p.Get("action", "get").ToLower(); - - try - { - if (action == "clear") - { - return ClearConsole(); - } - else if (action == "get") - { - // Extract parameters for 'get' - var types = - (p.GetRaw("types") as JArray)?.Select(t => t.ToString().ToLower()).ToList() - ?? new List { "error", "warning" }; - int? count = p.GetInt("count"); - int? pageSize = p.GetInt("pageSize"); - int? cursor = p.GetInt("cursor"); - string filterText = p.Get("filterText"); - string sinceTimestampStr = p.Get("sinceTimestamp"); // TODO: Implement timestamp filtering - string format = p.Get("format", "plain").ToLower(); - bool includeStacktrace = p.GetBool("includeStacktrace", false); - - if (types.Contains("all")) - { - types = new List { "error", "warning", "log" }; // Expand 'all' - } - - if (!string.IsNullOrEmpty(sinceTimestampStr)) - { - McpLog.Warn( - "[ReadConsole] Filtering by 'since_timestamp' is not currently implemented." - ); - // Need a way to get timestamp per log entry. - } - - return GetConsoleEntries( - types, - count, - pageSize, - cursor, - filterText, - format, - includeStacktrace - ); - } - else - { - return new ErrorResponse( - $"Unknown action: '{action}'. Valid actions are 'get' or 'clear'." - ); - } - } - catch (Exception e) - { - McpLog.Error($"[ReadConsole] Action '{action}' failed: {e}"); - return new ErrorResponse($"Internal error processing action '{action}': {e.Message}"); - } - } - - // --- Action Implementations --- - - private static object ClearConsole() - { - try - { - _clearMethod.Invoke(null, null); // Static method, no instance, no parameters - return new SuccessResponse("Console cleared successfully."); - } - catch (Exception e) - { - McpLog.Error($"[ReadConsole] Failed to clear console: {e}"); - return new ErrorResponse($"Failed to clear console: {e.Message}"); - } - } - - /// - /// Retrieves console log entries with optional filtering and paging. - /// - /// Log types to include (e.g., "error", "warning", "log"). - /// Maximum entries to return in non-paging mode. Ignored when paging is active. - /// Number of entries per page. Defaults to 50 when omitted. - /// Starting index for paging (0-based). Defaults to 0. - /// Optional text filter (case-insensitive substring match). - /// Output format: "plain", "detailed", or "json". - /// Whether to include stack traces in the output. - /// A success response with entries, or an error response. - private static object GetConsoleEntries( - List types, - int? count, - int? pageSize, - int? cursor, - string filterText, - string format, - bool includeStacktrace - ) - { - List formattedEntries = new List(); - int retrievedCount = 0; - int totalMatches = 0; - bool usePaging = pageSize.HasValue || cursor.HasValue; - // pageSize defaults to 50 when omitted; count is the overall non-paging limit only - int resolvedPageSize = Mathf.Clamp(pageSize ?? 50, 1, 500); - int resolvedCursor = Mathf.Max(0, cursor ?? 0); - int pageEndExclusive = resolvedCursor + resolvedPageSize; - - try - { - // LogEntries requires calling Start/Stop around GetEntries/GetEntryInternal - _startGettingEntriesMethod.Invoke(null, null); - - int totalEntries = (int)_getCountMethod.Invoke(null, null); - // Create instance to pass to GetEntryInternal - Ensure the type is correct - Type logEntryType = typeof(EditorApplication).Assembly.GetType( - "UnityEditor.LogEntry" - ); - if (logEntryType == null) - throw new Exception( - "Could not find internal type UnityEditor.LogEntry during GetConsoleEntries." - ); - object logEntryInstance = Activator.CreateInstance(logEntryType); - - for (int i = 0; i < totalEntries; i++) - { - // Get the entry data into our instance using reflection - _getEntryMethod.Invoke(null, new object[] { i, logEntryInstance }); - - // Extract data using reflection - int mode = (int)_modeField.GetValue(logEntryInstance); - string message = (string)_messageField.GetValue(logEntryInstance); - string file = (string)_fileField.GetValue(logEntryInstance); - - int line = (int)_lineField.GetValue(logEntryInstance); - // int instanceId = (int)_instanceIdField.GetValue(logEntryInstance); - - if (string.IsNullOrEmpty(message)) - { - continue; // Skip empty messages - } - - // (Calibration removed) - - // --- Filtering --- - // Prefer classifying severity from message/stacktrace; fallback to mode bits if needed - LogType unityType = InferTypeFromMessage(message); - bool isExplicitDebug = IsExplicitDebugLog(message); - if (!isExplicitDebug && unityType == LogType.Log) - { - unityType = GetLogTypeFromMode(mode); - } - - bool want; - // Treat Exception/Assert as errors for filtering convenience - if (unityType == LogType.Exception) - { - want = types.Contains("error") || types.Contains("exception"); - } - else if (unityType == LogType.Assert) - { - want = types.Contains("error") || types.Contains("assert"); - } - else - { - want = types.Contains(unityType.ToString().ToLowerInvariant()); - } - - if (!want) continue; - - // Filter by text (case-insensitive) - if ( - !string.IsNullOrEmpty(filterText) - && message.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) < 0 - ) - { - continue; - } - - // TODO: Filter by timestamp (requires timestamp data) - - // --- Formatting --- - string stackTrace = includeStacktrace ? ExtractStackTrace(message) : null; - // Always get first line for the message, use full message only if no stack trace exists - string[] messageLines = message.Split( - new[] { '\n', '\r' }, - StringSplitOptions.RemoveEmptyEntries - ); - string messageOnly = messageLines.Length > 0 ? messageLines[0] : message; - - // If not including stacktrace, ensure we only show the first line - if (!includeStacktrace) - { - stackTrace = null; - } - - object formattedEntry = null; - switch (format) - { - case "plain": - formattedEntry = messageOnly; - break; - case "json": - case "detailed": // Treat detailed as json for structured return - default: - formattedEntry = new - { - type = unityType.ToString(), - message = messageOnly, - file = file, - line = line, - // timestamp = "", // TODO - stackTrace = stackTrace, // Will be null if includeStacktrace is false or no stack found - }; - break; - } - - totalMatches++; - - if (usePaging) - { - if (totalMatches > resolvedCursor && totalMatches <= pageEndExclusive) - { - formattedEntries.Add(formattedEntry); - retrievedCount++; - } - // Early exit: we've filled the page and only need to check if more exist - else if (totalMatches > pageEndExclusive) - { - // We've passed the page; totalMatches now indicates truncation - break; - } - } - else - { - formattedEntries.Add(formattedEntry); - retrievedCount++; - - // Apply count limit (after filtering) - if (count.HasValue && retrievedCount >= count.Value) - { - break; - } - } - } - } - catch (Exception e) - { - McpLog.Error($"[ReadConsole] Error while retrieving log entries: {e}"); - // EndGettingEntries will be called in the finally block - return new ErrorResponse($"Error retrieving log entries: {e.Message}"); - } - finally - { - // Ensure we always call EndGettingEntries - try - { - _endGettingEntriesMethod.Invoke(null, null); - } - catch (Exception e) - { - McpLog.Error($"[ReadConsole] Failed to call EndGettingEntries: {e}"); - // Don't return error here as we might have valid data, but log it. - } - } - - if (usePaging) - { - bool truncated = totalMatches > pageEndExclusive; - string nextCursor = truncated ? pageEndExclusive.ToString() : null; - var payload = new - { - cursor = resolvedCursor, - pageSize = resolvedPageSize, - nextCursor = nextCursor, - truncated = truncated, - total = totalMatches, - items = formattedEntries, - }; - - return new SuccessResponse( - $"Retrieved {formattedEntries.Count} log entries.", - payload - ); - } - - // Return the filtered and formatted list (might be empty) - return new SuccessResponse( - $"Retrieved {formattedEntries.Count} log entries.", - formattedEntries - ); - } - - // --- Internal Helpers --- - - // Mapping bits from LogEntry.mode. These may vary by Unity version. - private const int ModeBitError = 1 << 0; - private const int ModeBitAssert = 1 << 1; - private const int ModeBitWarning = 1 << 2; - private const int ModeBitLog = 1 << 3; - private const int ModeBitException = 1 << 4; // often combined with Error bits - private const int ModeBitScriptingError = 1 << 9; - private const int ModeBitScriptingWarning = 1 << 10; - private const int ModeBitScriptingLog = 1 << 11; - private const int ModeBitScriptingException = 1 << 18; - private const int ModeBitScriptingAssertion = 1 << 22; - - private static LogType GetLogTypeFromMode(int mode) - { - // Preserve Unity's real type (no remapping); bits may vary by version - if ((mode & (ModeBitException | ModeBitScriptingException)) != 0) return LogType.Exception; - if ((mode & (ModeBitError | ModeBitScriptingError)) != 0) return LogType.Error; - if ((mode & (ModeBitAssert | ModeBitScriptingAssertion)) != 0) return LogType.Assert; - if ((mode & (ModeBitWarning | ModeBitScriptingWarning)) != 0) return LogType.Warning; - return LogType.Log; - } - - // (Calibration helpers removed) - - /// - /// Classifies severity using message/stacktrace content. Works across Unity versions. - /// - private static LogType InferTypeFromMessage(string fullMessage) - { - if (string.IsNullOrEmpty(fullMessage)) return LogType.Log; - - // Fast path: look for explicit Debug API names in the appended stack trace - // e.g., "UnityEngine.Debug:LogError (object)" or "LogWarning" - if (fullMessage.IndexOf("LogError", StringComparison.OrdinalIgnoreCase) >= 0) - return LogType.Error; - if (fullMessage.IndexOf("LogWarning", StringComparison.OrdinalIgnoreCase) >= 0) - return LogType.Warning; - - // Compiler diagnostics (C#): "warning CSxxxx" / "error CSxxxx" - if (fullMessage.IndexOf(" warning CS", StringComparison.OrdinalIgnoreCase) >= 0 - || fullMessage.IndexOf(": warning CS", StringComparison.OrdinalIgnoreCase) >= 0) - return LogType.Warning; - if (fullMessage.IndexOf(" error CS", StringComparison.OrdinalIgnoreCase) >= 0 - || fullMessage.IndexOf(": error CS", StringComparison.OrdinalIgnoreCase) >= 0) - return LogType.Error; - - // Exceptions (avoid misclassifying compiler diagnostics) - if (fullMessage.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0) - return LogType.Exception; - - // Unity assertions - if (fullMessage.IndexOf("Assertion", StringComparison.OrdinalIgnoreCase) >= 0) - return LogType.Assert; - - return LogType.Log; - } - - private static bool IsExplicitDebugLog(string fullMessage) - { - if (string.IsNullOrEmpty(fullMessage)) return false; - if (fullMessage.IndexOf("Debug:Log (", StringComparison.OrdinalIgnoreCase) >= 0) return true; - if (fullMessage.IndexOf("UnityEngine.Debug:Log (", StringComparison.OrdinalIgnoreCase) >= 0) return true; - return false; - } - - /// - /// Applies the "one level lower" remapping for filtering, like the old version. - /// This ensures compatibility with the filtering logic that expects remapped types. - /// - private static LogType GetRemappedTypeForFiltering(LogType unityType) - { - switch (unityType) - { - case LogType.Error: - return LogType.Warning; // Error becomes Warning - case LogType.Warning: - return LogType.Log; // Warning becomes Log - case LogType.Assert: - return LogType.Assert; // Assert remains Assert - case LogType.Log: - return LogType.Log; // Log remains Log - case LogType.Exception: - return LogType.Warning; // Exception becomes Warning - default: - return LogType.Log; // Default fallback - } - } - - /// - /// Attempts to extract the stack trace part from a log message. - /// Unity log messages often have the stack trace appended after the main message, - /// starting on a new line and typically indented or beginning with "at ". - /// - /// The complete log message including potential stack trace. - /// The extracted stack trace string, or null if none is found. - private static string ExtractStackTrace(string fullMessage) - { - if (string.IsNullOrEmpty(fullMessage)) - return null; - - // Split into lines, removing empty ones to handle different line endings gracefully. - // Using StringSplitOptions.None might be better if empty lines matter within stack trace, but RemoveEmptyEntries is usually safer here. - string[] lines = fullMessage.Split( - new[] { '\r', '\n' }, - StringSplitOptions.RemoveEmptyEntries - ); - - // If there's only one line or less, there's no separate stack trace. - if (lines.Length <= 1) - return null; - - int stackStartIndex = -1; - - // Start checking from the second line onwards. - for (int i = 1; i < lines.Length; ++i) - { - // Performance: TrimStart creates a new string. Consider using IsWhiteSpace check if performance critical. - string trimmedLine = lines[i].TrimStart(); - - // Check for common stack trace patterns. - if ( - trimmedLine.StartsWith("at ") - || trimmedLine.StartsWith("UnityEngine.") - || trimmedLine.StartsWith("UnityEditor.") - || trimmedLine.Contains("(at ") - || // Covers "(at Assets/..." pattern - // Heuristic: Check if line starts with likely namespace/class pattern (Uppercase.Something) - ( - trimmedLine.Length > 0 - && char.IsUpper(trimmedLine[0]) - && trimmedLine.Contains('.') - ) - ) - { - stackStartIndex = i; - break; // Found the likely start of the stack trace - } - } - - // If a potential start index was found... - if (stackStartIndex > 0) - { - // Join the lines from the stack start index onwards using standard newline characters. - // This reconstructs the stack trace part of the message. - return string.Join("\n", lines.Skip(stackStartIndex)); - } - - // No clear stack trace found based on the patterns. - return null; - } - - /* LogEntry.mode bits exploration (based on Unity decompilation/observation): - May change between versions. - - Basic Types: - kError = 1 << 0 (1) - kAssert = 1 << 1 (2) - kWarning = 1 << 2 (4) - kLog = 1 << 3 (8) - kFatal = 1 << 4 (16) - Often treated as Exception/Error - - Modifiers/Context: - kAssetImportError = 1 << 7 (128) - kAssetImportWarning = 1 << 8 (256) - kScriptingError = 1 << 9 (512) - kScriptingWarning = 1 << 10 (1024) - kScriptingLog = 1 << 11 (2048) - kScriptCompileError = 1 << 12 (4096) - kScriptCompileWarning = 1 << 13 (8192) - kStickyError = 1 << 14 (16384) - Stays visible even after Clear On Play - kMayIgnoreLineNumber = 1 << 15 (32768) - kReportBug = 1 << 16 (65536) - Shows the "Report Bug" button - kDisplayPreviousErrorInStatusBar = 1 << 17 (131072) - kScriptingException = 1 << 18 (262144) - kDontExtractStacktrace = 1 << 19 (524288) - Hint to the console UI - kShouldClearOnPlay = 1 << 20 (1048576) - Default behavior - kGraphCompileError = 1 << 21 (2097152) - kScriptingAssertion = 1 << 22 (4194304) - kVisualScriptingError = 1 << 23 (8388608) - - Example observed values: - Log: 2048 (ScriptingLog) or 8 (Log) - Warning: 1028 (ScriptingWarning | Warning) or 4 (Warning) - Error: 513 (ScriptingError | Error) or 1 (Error) - Exception: 262161 (ScriptingException | Error | kFatal?) - Complex combination - Assertion: 4194306 (ScriptingAssertion | Assert) or 2 (Assert) - */ - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/ReadConsole.cs.meta b/Assets/MCPForUnity/Editor/Tools/ReadConsole.cs.meta deleted file mode 100644 index 6be1d6f..0000000 --- a/Assets/MCPForUnity/Editor/Tools/ReadConsole.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 46c4f3614ed61f547ba823f0b2790267 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/ReadConsole.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/RefreshUnity.cs b/Assets/MCPForUnity/Editor/Tools/RefreshUnity.cs deleted file mode 100644 index 537472a..0000000 --- a/Assets/MCPForUnity/Editor/Tools/RefreshUnity.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEditor.Compilation; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Explicitly refreshes Unity's asset database and optionally requests a script compilation. - /// This is side-effectful and should be treated as a tool. - /// - [McpForUnityTool("refresh_unity", AutoRegister = false)] - public static class RefreshUnity - { - private const int DefaultWaitTimeoutSeconds = 60; - - public static async Task HandleCommand(JObject @params) - { - string mode = @params?["mode"]?.ToString() ?? "if_dirty"; - string scope = @params?["scope"]?.ToString() ?? "all"; - string compile = @params?["compile"]?.ToString() ?? "none"; - bool waitForReady = ParamCoercion.CoerceBool(@params?["wait_for_ready"], false); - - if (TestRunStatus.IsRunning) - { - return new ErrorResponse("tests_running", new - { - reason = "tests_running", - retry_after_ms = 5000 - }); - } - - bool refreshTriggered = false; - bool compileRequested = false; - - try - { - // Best-effort semantics: if_dirty currently behaves like force unless future dirty signals are added. - bool shouldRefresh = string.Equals(mode, "force", StringComparison.OrdinalIgnoreCase) - || string.Equals(mode, "if_dirty", StringComparison.OrdinalIgnoreCase); - - if (shouldRefresh) - { - if (string.Equals(scope, "scripts", StringComparison.OrdinalIgnoreCase)) - { - // For scripts, requesting compilation is usually the meaningful action. - // We avoid a heavyweight full refresh by default. - } - else - { - AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport); - refreshTriggered = true; - } - } - - if (string.Equals(compile, "request", StringComparison.OrdinalIgnoreCase)) - { - CompilationPipeline.RequestScriptCompilation(); - compileRequested = true; - } - - if (string.Equals(scope, "all", StringComparison.OrdinalIgnoreCase) && !refreshTriggered) - { - // If the caller asked for "all" and we skipped refresh above (e.g., scripts-only path), - // do a lightweight refresh now. Use ForceSynchronousImport to ensure the refresh - // completes before returning, preventing stalls when Unity is backgrounded. - AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); - refreshTriggered = true; - } - } - catch (Exception ex) - { - return new ErrorResponse($"refresh_failed: {ex.Message}"); - } - - // Unity 6+ fix: Skip wait_for_ready when compile was requested. - // The EditorApplication.update polling in WaitForUnityReadyAsync doesn't survive - // domain reloads properly in Unity 6+, causing infinite compilation loops. - // When compilation is requested, return immediately and let client poll editor_state. - // Earlier Unity versions retain the original behavior. -#if UNITY_6000_0_OR_NEWER - bool shouldWaitForReady = waitForReady && !compileRequested; -#else - bool shouldWaitForReady = waitForReady; -#endif - if (shouldWaitForReady) - { - try - { - await WaitForUnityReadyAsync( - TimeSpan.FromSeconds(DefaultWaitTimeoutSeconds)).ConfigureAwait(true); - } - catch (TimeoutException) - { - return new ErrorResponse("refresh_timeout_waiting_for_ready", new - { - refresh_triggered = refreshTriggered, - compile_requested = compileRequested, - resulting_state = "unknown", - }); - } - catch (Exception ex) - { - return new ErrorResponse($"refresh_wait_failed: {ex.Message}"); - } - } - - string resultingState = EditorApplication.isCompiling - ? "compiling" - : (EditorApplication.isUpdating ? "asset_import" : "idle"); - - return new SuccessResponse("Refresh requested.", new - { - refresh_triggered = refreshTriggered, - compile_requested = compileRequested, - resulting_state = resultingState, - hint = shouldWaitForReady - ? "Unity refresh completed; editor should be ready." - : "If Unity enters compilation/domain reload, poll editor_state until ready_for_tools is true." - }); - } - - private static Task WaitForUnityReadyAsync(TimeSpan timeout) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var start = DateTime.UtcNow; - - void Tick() - { - try - { - if (tcs.Task.IsCompleted) - { - EditorApplication.update -= Tick; - return; - } - - if ((DateTime.UtcNow - start) > timeout) - { - EditorApplication.update -= Tick; - tcs.TrySetException(new TimeoutException()); - return; - } - - if (!EditorApplication.isCompiling - && !EditorApplication.isUpdating - && !TestRunStatus.IsRunning - && !EditorApplication.isPlayingOrWillChangePlaymode) - { - EditorApplication.update -= Tick; - tcs.TrySetResult(true); - } - } - catch (Exception ex) - { - EditorApplication.update -= Tick; - tcs.TrySetException(ex); - } - } - - EditorApplication.update += Tick; - // Nudge Unity to pump once in case update is throttled. - try { EditorApplication.QueuePlayerLoopUpdate(); } catch { } - return tcs.Task; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/RefreshUnity.cs.meta b/Assets/MCPForUnity/Editor/Tools/RefreshUnity.cs.meta deleted file mode 100644 index 5b94eaa..0000000 --- a/Assets/MCPForUnity/Editor/Tools/RefreshUnity.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: c2c02170faca940d09c813706493ecb3 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/RefreshUnity.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/RunTests.cs b/Assets/MCPForUnity/Editor/Tools/RunTests.cs deleted file mode 100644 index e9b55f2..0000000 --- a/Assets/MCPForUnity/Editor/Tools/RunTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Resources.Tests; -using MCPForUnity.Editor.Services; -using Newtonsoft.Json.Linq; -using UnityEditor.TestTools.TestRunner.Api; - -namespace MCPForUnity.Editor.Tools -{ - /// - /// Starts a Unity Test Runner run asynchronously and returns a job id immediately. - /// Use get_test_job(job_id) to poll status/results. - /// - [McpForUnityTool("run_tests", AutoRegister = false)] - public static class RunTests - { - public static Task HandleCommand(JObject @params) - { - try - { - // Check for clear_stuck action first - if (ParamCoercion.CoerceBool(@params?["clear_stuck"], false)) - { - bool wasCleared = TestJobManager.ClearStuckJob(); - return Task.FromResult(new SuccessResponse( - wasCleared ? "Stuck job cleared." : "No running job to clear.", - new { cleared = wasCleared } - )); - } - - string modeStr = @params?["mode"]?.ToString(); - if (string.IsNullOrWhiteSpace(modeStr)) - { - modeStr = "EditMode"; - } - - if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) - { - return Task.FromResult(new ErrorResponse(parseError)); - } - - bool includeDetails = ParamCoercion.CoerceBool(@params?["includeDetails"], false); - bool includeFailedTests = ParamCoercion.CoerceBool(@params?["includeFailedTests"], false); - - var filterOptions = GetFilterOptions(@params); - string jobId = TestJobManager.StartJob(parsedMode.Value, filterOptions); - - return Task.FromResult(new SuccessResponse("Test job started.", new - { - job_id = jobId, - status = "running", - mode = parsedMode.Value.ToString(), - include_details = includeDetails, - include_failed_tests = includeFailedTests - })); - } - catch (Exception ex) - { - // Normalize the already-running case to a stable error token. - if (ex.Message != null && ex.Message.IndexOf("already in progress", StringComparison.OrdinalIgnoreCase) >= 0) - { - return Task.FromResult(new ErrorResponse("tests_running", new { reason = "tests_running", retry_after_ms = 5000 })); - } - return Task.FromResult(new ErrorResponse($"Failed to start test job: {ex.Message}")); - } - } - - private static TestFilterOptions GetFilterOptions(JObject @params) - { - if (@params == null) - { - return null; - } - - string[] ParseStringArray(string key) - { - var token = @params[key]; - if (token == null) return null; - if (token.Type == JTokenType.String) - { - var value = token.ToString(); - return string.IsNullOrWhiteSpace(value) ? null : new[] { value }; - } - if (token.Type == JTokenType.Array) - { - var array = token as JArray; - if (array == null || array.Count == 0) return null; - var values = array - .Values() - .Where(s => !string.IsNullOrWhiteSpace(s)) - .ToArray(); - return values.Length > 0 ? values : null; - } - return null; - } - - var testNames = ParseStringArray("testNames"); - var groupNames = ParseStringArray("groupNames"); - var categoryNames = ParseStringArray("categoryNames"); - var assemblyNames = ParseStringArray("assemblyNames"); - - if (testNames == null && groupNames == null && categoryNames == null && assemblyNames == null) - { - return null; - } - - return new TestFilterOptions - { - TestNames = testNames, - GroupNames = groupNames, - CategoryNames = categoryNames, - AssemblyNames = assemblyNames - }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/RunTests.cs.meta b/Assets/MCPForUnity/Editor/Tools/RunTests.cs.meta deleted file mode 100644 index c30adc1..0000000 --- a/Assets/MCPForUnity/Editor/Tools/RunTests.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 5cc0c41b1a8b4e0e9d0f1f8b1d7d2a9c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/RunTests.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx.meta b/Assets/MCPForUnity/Editor/Tools/Vfx.meta deleted file mode 100644 index b128ae3..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 1805768600c6a4228bae31231f2a4a9f -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs deleted file mode 100644 index fcaf55b..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs +++ /dev/null @@ -1,220 +0,0 @@ -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; -using MCPForUnity.Editor.Helpers; - -namespace MCPForUnity.Editor.Tools.Vfx -{ - internal static class LineCreate - { - public static object CreateLine(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - Vector3 start = ManageVfxCommon.ParseVector3(@params["start"]); - Vector3 end = ManageVfxCommon.ParseVector3(@params["end"]); - - Undo.RecordObject(lr, "Create Line"); - lr.positionCount = 2; - lr.SetPosition(0, start); - lr.SetPosition(1, end); - - RendererHelpers.EnsureMaterial(lr); - - // Apply optional width - if (@params["width"] != null) - { - float w = @params["width"].ToObject(); - lr.startWidth = w; - lr.endWidth = w; - } - if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject(); - if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject(); - - // Apply optional color - if (@params["color"] != null) - { - Color c = ManageVfxCommon.ParseColor(@params["color"]); - lr.startColor = c; - lr.endColor = c; - } - if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]); - if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]); - - EditorUtility.SetDirty(lr); - - return new { success = true, message = "Created line" }; - } - - public static object CreateCircle(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - Vector3 center = ManageVfxCommon.ParseVector3(@params["center"]); - float radius = @params["radius"]?.ToObject() ?? 1f; - int segments = @params["segments"]?.ToObject() ?? 32; - Vector3 normal = @params["normal"] != null ? ManageVfxCommon.ParseVector3(@params["normal"]).normalized : Vector3.up; - - Vector3 right = Vector3.Cross(normal, Vector3.forward); - if (right.sqrMagnitude < 0.001f) right = Vector3.Cross(normal, Vector3.up); - right = right.normalized; - Vector3 forward = Vector3.Cross(right, normal).normalized; - - Undo.RecordObject(lr, "Create Circle"); - lr.positionCount = segments; - lr.loop = true; - - for (int i = 0; i < segments; i++) - { - float angle = (float)i / segments * Mathf.PI * 2f; - Vector3 point = center + (right * Mathf.Cos(angle) + forward * Mathf.Sin(angle)) * radius; - lr.SetPosition(i, point); - } - - RendererHelpers.EnsureMaterial(lr); - - // Apply optional width - if (@params["width"] != null) - { - float w = @params["width"].ToObject(); - lr.startWidth = w; - lr.endWidth = w; - } - if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject(); - if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject(); - - // Apply optional color - if (@params["color"] != null) - { - Color c = ManageVfxCommon.ParseColor(@params["color"]); - lr.startColor = c; - lr.endColor = c; - } - if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]); - if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]); - - EditorUtility.SetDirty(lr); - return new { success = true, message = $"Created circle with {segments} segments" }; - } - - public static object CreateArc(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - Vector3 center = ManageVfxCommon.ParseVector3(@params["center"]); - float radius = @params["radius"]?.ToObject() ?? 1f; - float startAngle = (@params["startAngle"]?.ToObject() ?? 0f) * Mathf.Deg2Rad; - float endAngle = (@params["endAngle"]?.ToObject() ?? 180f) * Mathf.Deg2Rad; - int segments = @params["segments"]?.ToObject() ?? 16; - Vector3 normal = @params["normal"] != null ? ManageVfxCommon.ParseVector3(@params["normal"]).normalized : Vector3.up; - - Vector3 right = Vector3.Cross(normal, Vector3.forward); - if (right.sqrMagnitude < 0.001f) right = Vector3.Cross(normal, Vector3.up); - right = right.normalized; - Vector3 forward = Vector3.Cross(right, normal).normalized; - - Undo.RecordObject(lr, "Create Arc"); - lr.positionCount = segments + 1; - lr.loop = false; - - for (int i = 0; i <= segments; i++) - { - float t = (float)i / segments; - float angle = Mathf.Lerp(startAngle, endAngle, t); - Vector3 point = center + (right * Mathf.Cos(angle) + forward * Mathf.Sin(angle)) * radius; - lr.SetPosition(i, point); - } - - RendererHelpers.EnsureMaterial(lr); - - // Apply optional width - if (@params["width"] != null) - { - float w = @params["width"].ToObject(); - lr.startWidth = w; - lr.endWidth = w; - } - if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject(); - if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject(); - - // Apply optional color - if (@params["color"] != null) - { - Color c = ManageVfxCommon.ParseColor(@params["color"]); - lr.startColor = c; - lr.endColor = c; - } - if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]); - if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]); - - EditorUtility.SetDirty(lr); - return new { success = true, message = $"Created arc with {segments} segments" }; - } - - public static object CreateBezier(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - Vector3 start = ManageVfxCommon.ParseVector3(@params["start"]); - Vector3 end = ManageVfxCommon.ParseVector3(@params["end"]); - Vector3 cp1 = ManageVfxCommon.ParseVector3(@params["controlPoint1"] ?? @params["control1"]); - Vector3 cp2 = @params["controlPoint2"] != null || @params["control2"] != null - ? ManageVfxCommon.ParseVector3(@params["controlPoint2"] ?? @params["control2"]) - : cp1; - int segments = @params["segments"]?.ToObject() ?? 32; - bool isQuadratic = @params["controlPoint2"] == null && @params["control2"] == null; - - Undo.RecordObject(lr, "Create Bezier"); - lr.positionCount = segments + 1; - lr.loop = false; - - for (int i = 0; i <= segments; i++) - { - float t = (float)i / segments; - Vector3 point; - - if (isQuadratic) - { - float u = 1 - t; - point = u * u * start + 2 * u * t * cp1 + t * t * end; - } - else - { - float u = 1 - t; - point = u * u * u * start + 3 * u * u * t * cp1 + 3 * u * t * t * cp2 + t * t * t * end; - } - - lr.SetPosition(i, point); - } - - RendererHelpers.EnsureMaterial(lr); - - // Apply optional width - if (@params["width"] != null) - { - float w = @params["width"].ToObject(); - lr.startWidth = w; - lr.endWidth = w; - } - if (@params["startWidth"] != null) lr.startWidth = @params["startWidth"].ToObject(); - if (@params["endWidth"] != null) lr.endWidth = @params["endWidth"].ToObject(); - - // Apply optional color - if (@params["color"] != null) - { - Color c = ManageVfxCommon.ParseColor(@params["color"]); - lr.startColor = c; - lr.endColor = c; - } - if (@params["startColor"] != null) lr.startColor = ManageVfxCommon.ParseColor(@params["startColor"]); - if (@params["endColor"] != null) lr.endColor = ManageVfxCommon.ParseColor(@params["endColor"]); - - EditorUtility.SetDirty(lr); - return new { success = true, message = $"Created {(isQuadratic ? "quadratic" : "cubic")} Bezier" }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs.meta deleted file mode 100644 index ca49cb0..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 6d553d3837ecc4d999225bc9b3160a26 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/LineCreate.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/LineRead.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/LineRead.cs deleted file mode 100644 index 3dd0c06..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/LineRead.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Linq; -using Newtonsoft.Json.Linq; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.Vfx -{ - internal static class LineRead - { - public static LineRenderer FindLineRenderer(JObject @params) - { - GameObject go = ManageVfxCommon.FindTargetGameObject(@params); - return go?.GetComponent(); - } - - public static object GetInfo(JObject @params) - { - LineRenderer lr = FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - var positions = new Vector3[lr.positionCount]; - lr.GetPositions(positions); - - return new - { - success = true, - data = new - { - gameObject = lr.gameObject.name, - positionCount = lr.positionCount, - positions = positions.Select(p => new { x = p.x, y = p.y, z = p.z }).ToArray(), - startWidth = lr.startWidth, - endWidth = lr.endWidth, - loop = lr.loop, - useWorldSpace = lr.useWorldSpace, - alignment = lr.alignment.ToString(), - textureMode = lr.textureMode.ToString(), - numCornerVertices = lr.numCornerVertices, - numCapVertices = lr.numCapVertices, - generateLightingData = lr.generateLightingData, - material = lr.sharedMaterial?.name, - shadowCastingMode = lr.shadowCastingMode.ToString(), - receiveShadows = lr.receiveShadows, - lightProbeUsage = lr.lightProbeUsage.ToString(), - reflectionProbeUsage = lr.reflectionProbeUsage.ToString(), - sortingOrder = lr.sortingOrder, - sortingLayerName = lr.sortingLayerName, - renderingLayerMask = lr.renderingLayerMask - } - }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/LineRead.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/LineRead.cs.meta deleted file mode 100644 index c89d0ed..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/LineRead.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: df77cf0ca14344b0cb2f1b84c5eb15e7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/LineRead.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs deleted file mode 100644 index 339e9c8..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System.Collections.Generic; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.Vfx -{ - internal static class LineWrite - { - public static object SetPositions(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - RendererHelpers.EnsureMaterial(lr); - - JArray posArr = @params["positions"] as JArray; - if (posArr == null) return new { success = false, message = "Positions array required" }; - - var positions = new Vector3[posArr.Count]; - for (int i = 0; i < posArr.Count; i++) - { - positions[i] = ManageVfxCommon.ParseVector3(posArr[i]); - } - - Undo.RecordObject(lr, "Set Line Positions"); - lr.positionCount = positions.Length; - lr.SetPositions(positions); - EditorUtility.SetDirty(lr); - - return new { success = true, message = $"Set {positions.Length} positions" }; - } - - public static object AddPosition(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - RendererHelpers.EnsureMaterial(lr); - - Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]); - - Undo.RecordObject(lr, "Add Line Position"); - int idx = lr.positionCount; - lr.positionCount = idx + 1; - lr.SetPosition(idx, pos); - EditorUtility.SetDirty(lr); - - return new { success = true, message = $"Added position at index {idx}", index = idx }; - } - - public static object SetPosition(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - RendererHelpers.EnsureMaterial(lr); - - int index = @params["index"]?.ToObject() ?? -1; - if (index < 0 || index >= lr.positionCount) return new { success = false, message = $"Invalid index {index}" }; - - Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]); - - Undo.RecordObject(lr, "Set Line Position"); - lr.SetPosition(index, pos); - EditorUtility.SetDirty(lr); - - return new { success = true, message = $"Set position at index {index}" }; - } - - public static object SetWidth(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - RendererHelpers.EnsureMaterial(lr); - - Undo.RecordObject(lr, "Set Line Width"); - var changes = new List(); - - RendererHelpers.ApplyWidthProperties(@params, changes, - v => lr.startWidth = v, v => lr.endWidth = v, - v => lr.widthCurve = v, v => lr.widthMultiplier = v, - ManageVfxCommon.ParseAnimationCurve); - - EditorUtility.SetDirty(lr); - return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; - } - - public static object SetColor(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - RendererHelpers.EnsureMaterial(lr); - - Undo.RecordObject(lr, "Set Line Color"); - var changes = new List(); - - RendererHelpers.ApplyColorProperties(@params, changes, - v => lr.startColor = v, v => lr.endColor = v, - v => lr.colorGradient = v, - ManageVfxCommon.ParseColor, ManageVfxCommon.ParseGradient, fadeEndAlpha: false); - - EditorUtility.SetDirty(lr); - return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; - } - - public static object SetMaterial(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - return RendererHelpers.SetRendererMaterial(lr, @params, "Set Line Material", ManageVfxCommon.FindMaterialByPath); - } - - public static object SetProperties(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - RendererHelpers.EnsureMaterial(lr); - - Undo.RecordObject(lr, "Set Line Properties"); - var changes = new List(); - - // Handle material if provided - if (@params["materialPath"] != null) - { - Material mat = ManageVfxCommon.FindMaterialByPath(@params["materialPath"].ToString()); - if (mat != null) - { - lr.sharedMaterial = mat; - changes.Add($"material={mat.name}"); - } - else - { - McpLog.Warn($"Material not found: {@params["materialPath"]}"); - } - } - - // Handle positions if provided - if (@params["positions"] != null) - { - JArray posArr = @params["positions"] as JArray; - if (posArr != null && posArr.Count > 0) - { - var positions = new Vector3[posArr.Count]; - for (int i = 0; i < posArr.Count; i++) - { - positions[i] = ManageVfxCommon.ParseVector3(posArr[i]); - } - lr.positionCount = positions.Length; - lr.SetPositions(positions); - changes.Add($"positions({positions.Length})"); - } - } - else if (@params["positionCount"] != null) - { - int count = @params["positionCount"].ToObject(); - lr.positionCount = count; - changes.Add("positionCount"); - } - - RendererHelpers.ApplyLineTrailProperties(@params, changes, - v => lr.loop = v, v => lr.useWorldSpace = v, - v => lr.numCornerVertices = v, v => lr.numCapVertices = v, - v => lr.alignment = v, v => lr.textureMode = v, - v => lr.generateLightingData = v); - - RendererHelpers.ApplyCommonRendererProperties(lr, @params, changes); - - EditorUtility.SetDirty(lr); - return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; - } - - public static object Clear(JObject @params) - { - LineRenderer lr = LineRead.FindLineRenderer(@params); - if (lr == null) return new { success = false, message = "LineRenderer not found" }; - - int count = lr.positionCount; - Undo.RecordObject(lr, "Clear Line"); - lr.positionCount = 0; - EditorUtility.SetDirty(lr); - - return new { success = true, message = $"Cleared {count} positions" }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs.meta deleted file mode 100644 index a0ec5ad..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 3911acc5a6a6a494cb88a647e0426d67 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/LineWrite.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs deleted file mode 100644 index 55e7b38..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs +++ /dev/null @@ -1,412 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using MCPForUnity.Editor.Helpers; -using UnityEngine; -using UnityEditor; - -#if UNITY_VFX_GRAPH //Please enable the symbol in the project settings for VisualEffectGraph to work -using UnityEngine.VFX; -#endif - -namespace MCPForUnity.Editor.Tools.Vfx -{ - /// - /// Tool for managing Unity VFX components: - /// - ParticleSystem (legacy particle effects) - /// - Visual Effect Graph (modern GPU particles, currently only support HDRP, other SRPs may not work) - /// - LineRenderer (lines, bezier curves, shapes) - /// - TrailRenderer (motion trails) - /// - /// COMPONENT REQUIREMENTS: - /// - particle_* actions require ParticleSystem component on target GameObject - /// - vfx_* actions require VisualEffect component (+ com.unity.visualeffectgraph package) - /// - line_* actions require LineRenderer component - /// - trail_* actions require TrailRenderer component - /// - /// TARGETING: - /// Use 'target' parameter with optional 'searchMethod': - /// - by_name (default): "Fire" finds first GameObject named "Fire" - /// - by_path: "Effects/Fire" finds GameObject at hierarchy path - /// - by_id: "12345" finds GameObject by instance ID (most reliable) - /// - by_tag: "Enemy" finds first GameObject with tag - /// - /// AUTOMATIC MATERIAL ASSIGNMENT: - /// VFX components (ParticleSystem, LineRenderer, TrailRenderer) automatically receive - /// appropriate default materials based on the active rendering pipeline when no material - /// is explicitly specified: - /// - Built-in Pipeline: Uses Unity's built-in Default-Particle.mat and Default-Line.mat - /// - URP/HDRP: Creates materials with pipeline-appropriate unlit shaders - /// - Materials are cached to avoid recreation - /// - Explicit materialPath parameter always overrides auto-assignment - /// - Auto-assigned materials are logged for transparency - /// - /// AVAILABLE ACTIONS: - /// - /// ParticleSystem (particle_*): - /// - particle_get_info: Get system info and current state - /// - particle_set_main: Set main module (duration, looping, startLifetime, startSpeed, startSize, startColor, gravityModifier, maxParticles, simulationSpace, playOnAwake, etc.) - /// - particle_set_emission: Set emission module (rateOverTime, rateOverDistance) - /// - particle_set_shape: Set shape module (shapeType, radius, angle, arc, position, rotation, scale) - /// - particle_set_color_over_lifetime: Set color gradient over particle lifetime - /// - particle_set_size_over_lifetime: Set size curve over particle lifetime - /// - particle_set_velocity_over_lifetime: Set velocity (x, y, z, speedModifier, space) - /// - particle_set_noise: Set noise turbulence (strength, frequency, scrollSpeed, damping, octaveCount, quality) - /// - particle_set_renderer: Set renderer (renderMode, material, sortMode, minParticleSize, maxParticleSize, etc.) - /// - particle_enable_module: Enable/disable modules by name - /// - particle_play/stop/pause/restart/clear: Playback control (withChildren optional) - /// - particle_add_burst: Add emission burst (time, count, cycles, interval, probability) - /// - particle_clear_bursts: Clear all bursts - /// - /// Visual Effect Graph (vfx_*): - /// Asset Management: - /// - vfx_create_asset: Create new VFX asset file (assetName, folderPath, template, overwrite) - /// - vfx_assign_asset: Assign VFX asset to VisualEffect component (target, assetPath) - /// - vfx_list_templates: List available VFX templates in project and packages - /// - vfx_list_assets: List all VFX assets (folder, search filters) - /// Runtime Control: - /// - vfx_get_info: Get VFX info including exposed parameters - /// - vfx_set_float/int/bool: Set exposed scalar parameters (parameter, value) - /// - vfx_set_vector2/vector3/vector4: Set exposed vector parameters (parameter, value as array) - /// - vfx_set_color: Set exposed color (parameter, color as [r,g,b,a]) - /// - vfx_set_gradient: Set exposed gradient (parameter, gradient) - /// - vfx_set_texture: Set exposed texture (parameter, texturePath) - /// - vfx_set_mesh: Set exposed mesh (parameter, meshPath) - /// - vfx_set_curve: Set exposed animation curve (parameter, curve) - /// - vfx_send_event: Send event with attributes (eventName, position, velocity, color, size, lifetime) - /// - vfx_play/stop/pause/reinit: Playback control - /// - vfx_set_playback_speed: Set playback speed multiplier (playRate) - /// - vfx_set_seed: Set random seed (seed, resetSeedOnPlay) - /// - /// LineRenderer (line_*): - /// - line_get_info: Get line info (position count, width, color, etc.) - /// - line_set_positions: Set all positions (positions as [[x,y,z], ...]) - /// - line_add_position: Add position at end (position as [x,y,z]) - /// - line_set_position: Set specific position (index, position) - /// - line_set_width: Set width (width, startWidth, endWidth, widthCurve, widthMultiplier) - /// - line_set_color: Set color (color, gradient, startColor, endColor) - /// - line_set_material: Set material (materialPath) - /// - line_set_properties: Set renderer properties (loop, useWorldSpace, alignment, textureMode, numCornerVertices, numCapVertices, etc.) - /// - line_clear: Clear all positions - /// Shape Creation: - /// - line_create_line: Create simple line (start, end, segments) - /// - line_create_circle: Create circle (center, radius, segments, normal) - /// - line_create_arc: Create arc (center, radius, startAngle, endAngle, segments, normal) - /// - line_create_bezier: Create Bezier curve (start, end, controlPoint1, controlPoint2, segments) - /// - /// TrailRenderer (trail_*): - /// - trail_get_info: Get trail info - /// - trail_set_time: Set trail duration (time) - /// - trail_set_width: Set width (width, startWidth, endWidth, widthCurve, widthMultiplier) - /// - trail_set_color: Set color (color, gradient, startColor, endColor) - /// - trail_set_material: Set material (materialPath) - /// - trail_set_properties: Set properties (minVertexDistance, autodestruct, emitting, alignment, textureMode, etc.) - /// - trail_clear: Clear trail - /// - trail_emit: Emit point at current position (Unity 2021.1+) - /// - /// COMMON PARAMETERS: - /// - target (string): GameObject identifier - /// - searchMethod (string): "by_id" | "by_name" | "by_path" | "by_tag" | "by_layer" - /// - materialPath (string): Asset path to material (e.g., "Assets/Materials/Fire.mat") - /// - color (array): Color as [r, g, b, a] with values 0-1 - /// - position (array): 3D position as [x, y, z] - /// - gradient (object): {colorKeys: [{color: [r,g,b,a], time: 0-1}], alphaKeys: [{alpha: 0-1, time: 0-1}]} - /// - curve (object): {keys: [{time: 0-1, value: number, inTangent: number, outTangent: number}]} - /// - /// For full parameter details, refer to Unity documentation for each component type. - /// - [McpForUnityTool("manage_vfx", AutoRegister = false)] - public static class ManageVFX - { - private static readonly Dictionary ParamAliases = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "size_over_lifetime", "size" }, - { "start_color_line", "startColor" }, - { "sorting_layer_id", "sortingLayerID" }, - { "material", "materialPath" }, - }; - - private static JObject NormalizeParams(JObject source) - { - if (source == null) - { - return new JObject(); - } - - var normalized = new JObject(); - var properties = ExtractProperties(source); - if (properties != null) - { - foreach (var prop in properties.Properties()) - { - normalized[NormalizeKey(prop.Name, true)] = NormalizeToken(prop.Value); - } - } - - foreach (var prop in source.Properties()) - { - if (string.Equals(prop.Name, "properties", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - normalized[NormalizeKey(prop.Name, true)] = NormalizeToken(prop.Value); - } - - return normalized; - } - - private static JObject ExtractProperties(JObject source) - { - if (source == null) - { - return null; - } - - if (!source.TryGetValue("properties", StringComparison.OrdinalIgnoreCase, out var token)) - { - return null; - } - - if (token == null || token.Type == JTokenType.Null) - { - return null; - } - - if (token is JObject obj) - { - return obj; - } - - if (token.Type == JTokenType.String) - { - try - { - return JToken.Parse(token.ToString()) as JObject; - } - catch (JsonException ex) - { - throw new JsonException( - $"Failed to parse 'properties' JSON string. Raw value: {token}", - ex); - } - } - - return null; - } - - private static string NormalizeKey(string key, bool allowAliases) - { - if (string.IsNullOrEmpty(key)) - { - return key; - } - if (string.Equals(key, "action", StringComparison.OrdinalIgnoreCase)) - { - return "action"; - } - if (allowAliases && ParamAliases.TryGetValue(key, out var alias)) - { - return alias; - } - if (key.IndexOf('_') >= 0) - { - return ToCamelCase(key); - } - return key; - } - - private static JToken NormalizeToken(JToken token) - { - if (token == null) - { - return null; - } - - if (token is JObject obj) - { - var normalized = new JObject(); - foreach (var prop in obj.Properties()) - { - normalized[NormalizeKey(prop.Name, false)] = NormalizeToken(prop.Value); - } - return normalized; - } - - if (token is JArray array) - { - var normalized = new JArray(); - foreach (var item in array) - { - normalized.Add(NormalizeToken(item)); - } - return normalized; - } - - return token; - } - - private static string ToCamelCase(string key) => StringCaseUtility.ToCamelCase(key); - - public static object HandleCommand(JObject @params) - { - JObject normalizedParams = NormalizeParams(@params); - string action = normalizedParams["action"]?.ToString(); - if (string.IsNullOrEmpty(action)) - { - return new { success = false, message = "Action is required" }; - } - - try - { - string actionLower = action.ToLowerInvariant(); - - // Route to appropriate handler based on action prefix - if (actionLower == "ping") - { - return new { success = true, tool = "manage_vfx", components = new[] { "ParticleSystem", "VisualEffect", "LineRenderer", "TrailRenderer" } }; - } - - // ParticleSystem actions (particle_*) - if (actionLower.StartsWith("particle_")) - { - return HandleParticleSystemAction(normalizedParams, actionLower.Substring(9)); - } - - // VFX Graph actions (vfx_*) - if (actionLower.StartsWith("vfx_")) - { - return HandleVFXGraphAction(normalizedParams, actionLower.Substring(4)); - } - - // LineRenderer actions (line_*) - if (actionLower.StartsWith("line_")) - { - return HandleLineRendererAction(normalizedParams, actionLower.Substring(5)); - } - - // TrailRenderer actions (trail_*) - if (actionLower.StartsWith("trail_")) - { - return HandleTrailRendererAction(normalizedParams, actionLower.Substring(6)); - } - - return new { success = false, message = $"Unknown action: {action}. Actions must be prefixed with: particle_, vfx_, line_, or trail_" }; - } - catch (Exception ex) - { - return new { success = false, message = ex.Message, stackTrace = ex.StackTrace }; - } - } - - private static object HandleParticleSystemAction(JObject @params, string action) - { - switch (action) - { - case "get_info": return ParticleRead.GetInfo(@params); - case "set_main": return ParticleWrite.SetMain(@params); - case "set_emission": return ParticleWrite.SetEmission(@params); - case "set_shape": return ParticleWrite.SetShape(@params); - case "set_color_over_lifetime": return ParticleWrite.SetColorOverLifetime(@params); - case "set_size_over_lifetime": return ParticleWrite.SetSizeOverLifetime(@params); - case "set_velocity_over_lifetime": return ParticleWrite.SetVelocityOverLifetime(@params); - case "set_noise": return ParticleWrite.SetNoise(@params); - case "set_renderer": return ParticleWrite.SetRenderer(@params); - case "enable_module": return ParticleControl.EnableModule(@params); - case "play": return ParticleControl.Control(@params, "play"); - case "stop": return ParticleControl.Control(@params, "stop"); - case "pause": return ParticleControl.Control(@params, "pause"); - case "restart": return ParticleControl.Control(@params, "restart"); - case "clear": return ParticleControl.Control(@params, "clear"); - case "add_burst": return ParticleControl.AddBurst(@params); - case "clear_bursts": return ParticleControl.ClearBursts(@params); - default: - return new { success = false, message = $"Unknown particle action: {action}. Valid: get_info, set_main, set_emission, set_shape, set_color_over_lifetime, set_size_over_lifetime, set_velocity_over_lifetime, set_noise, set_renderer, enable_module, play, stop, pause, restart, clear, add_burst, clear_bursts" }; - } - } - - // ==================== VFX GRAPH ==================== - #region VFX Graph - - private static object HandleVFXGraphAction(JObject @params, string action) - { -#if !UNITY_VFX_GRAPH - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; -#else - switch (action) - { - // Asset management - case "create_asset": return VfxGraphAssets.CreateAsset(@params); - case "assign_asset": return VfxGraphAssets.AssignAsset(@params); - case "list_templates": return VfxGraphAssets.ListTemplates(@params); - case "list_assets": return VfxGraphAssets.ListAssets(@params); - - // Runtime parameter control - case "get_info": return VfxGraphRead.GetInfo(@params); - case "set_float": return VfxGraphWrite.SetParameter(@params, (vfx, n, v) => vfx.SetFloat(n, v)); - case "set_int": return VfxGraphWrite.SetParameter(@params, (vfx, n, v) => vfx.SetInt(n, v)); - case "set_bool": return VfxGraphWrite.SetParameter(@params, (vfx, n, v) => vfx.SetBool(n, v)); - case "set_vector2": return VfxGraphWrite.SetVector(@params, 2); - case "set_vector3": return VfxGraphWrite.SetVector(@params, 3); - case "set_vector4": return VfxGraphWrite.SetVector(@params, 4); - case "set_color": return VfxGraphWrite.SetColor(@params); - case "set_gradient": return VfxGraphWrite.SetGradient(@params); - case "set_texture": return VfxGraphWrite.SetTexture(@params); - case "set_mesh": return VfxGraphWrite.SetMesh(@params); - case "set_curve": return VfxGraphWrite.SetCurve(@params); - case "send_event": return VfxGraphWrite.SendEvent(@params); - case "play": return VfxGraphControl.Control(@params, "play"); - case "stop": return VfxGraphControl.Control(@params, "stop"); - case "pause": return VfxGraphControl.Control(@params, "pause"); - case "reinit": return VfxGraphControl.Control(@params, "reinit"); - case "set_playback_speed": return VfxGraphControl.SetPlaybackSpeed(@params); - case "set_seed": return VfxGraphControl.SetSeed(@params); - default: - return new { success = false, message = $"Unknown vfx action: {action}. Valid: create_asset, assign_asset, list_templates, list_assets, get_info, set_float, set_int, set_bool, set_vector2/3/4, set_color, set_gradient, set_texture, set_mesh, set_curve, send_event, play, stop, pause, reinit, set_playback_speed, set_seed" }; - } -#endif - } - - - #endregion - - private static object HandleLineRendererAction(JObject @params, string action) - { - switch (action) - { - case "get_info": return LineRead.GetInfo(@params); - case "set_positions": return LineWrite.SetPositions(@params); - case "add_position": return LineWrite.AddPosition(@params); - case "set_position": return LineWrite.SetPosition(@params); - case "set_width": return LineWrite.SetWidth(@params); - case "set_color": return LineWrite.SetColor(@params); - case "set_material": return LineWrite.SetMaterial(@params); - case "set_properties": return LineWrite.SetProperties(@params); - case "clear": return LineWrite.Clear(@params); - case "create_line": return LineCreate.CreateLine(@params); - case "create_circle": return LineCreate.CreateCircle(@params); - case "create_arc": return LineCreate.CreateArc(@params); - case "create_bezier": return LineCreate.CreateBezier(@params); - default: - return new { success = false, message = $"Unknown line action: {action}. Valid: get_info, set_positions, add_position, set_position, set_width, set_color, set_material, set_properties, clear, create_line, create_circle, create_arc, create_bezier" }; - } - } - - private static object HandleTrailRendererAction(JObject @params, string action) - { - switch (action) - { - case "get_info": return TrailRead.GetInfo(@params); - case "set_time": return TrailWrite.SetTime(@params); - case "set_width": return TrailWrite.SetWidth(@params); - case "set_color": return TrailWrite.SetColor(@params); - case "set_material": return TrailWrite.SetMaterial(@params); - case "set_properties": return TrailWrite.SetProperties(@params); - case "clear": return TrailControl.Clear(@params); - case "emit": return TrailControl.Emit(@params); - default: - return new { success = false, message = $"Unknown trail action: {action}. Valid: get_info, set_time, set_width, set_color, set_material, set_properties, clear, emit" }; - } - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs.meta deleted file mode 100644 index 0ae7e7b..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: a8f3d2c1e9b74f6a8c5d0e2f1a3b4c5d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVfxCommon.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVfxCommon.cs deleted file mode 100644 index aa2d668..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVfxCommon.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Newtonsoft.Json.Linq; -using MCPForUnity.Editor.Helpers; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.Vfx -{ - internal static class ManageVfxCommon - { - public static Color ParseColor(JToken token) => VectorParsing.ParseColorOrDefault(token); - public static Vector3 ParseVector3(JToken token) => VectorParsing.ParseVector3OrDefault(token); - public static Vector4 ParseVector4(JToken token) => VectorParsing.ParseVector4OrDefault(token); - public static Gradient ParseGradient(JToken token) => VectorParsing.ParseGradientOrDefault(token); - public static AnimationCurve ParseAnimationCurve(JToken token, float defaultValue = 1f) - => VectorParsing.ParseAnimationCurveOrDefault(token, defaultValue); - - public static GameObject FindTargetGameObject(JObject @params) - => ObjectResolver.ResolveGameObject(@params["target"], @params["searchMethod"]?.ToString()); - - public static Material FindMaterialByPath(string path) - => ObjectResolver.ResolveMaterial(path); - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVfxCommon.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVfxCommon.cs.meta deleted file mode 100644 index 764f2ec..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ManageVfxCommon.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 1c5e603b26d2f47529394c1ec6b8ed79 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/ManageVfxCommon.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleCommon.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleCommon.cs deleted file mode 100644 index b941873..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleCommon.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Newtonsoft.Json.Linq; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.Vfx -{ - internal static class ParticleCommon - { - public static ParticleSystem FindParticleSystem(JObject @params) - { - GameObject go = ManageVfxCommon.FindTargetGameObject(@params); - return go?.GetComponent(); - } - - public static ParticleSystem.MinMaxCurve ParseMinMaxCurve(JToken token, float defaultValue = 1f) - { - if (token == null) - return new ParticleSystem.MinMaxCurve(defaultValue); - - if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer) - { - return new ParticleSystem.MinMaxCurve(token.ToObject()); - } - - if (token is JObject obj) - { - string mode = obj["mode"]?.ToString()?.ToLowerInvariant() ?? "constant"; - - switch (mode) - { - case "constant": - float constant = obj["value"]?.ToObject() ?? defaultValue; - return new ParticleSystem.MinMaxCurve(constant); - - case "random_between_constants": - case "two_constants": - float min = obj["min"]?.ToObject() ?? 0f; - float max = obj["max"]?.ToObject() ?? 1f; - return new ParticleSystem.MinMaxCurve(min, max); - - case "curve": - AnimationCurve curve = ManageVfxCommon.ParseAnimationCurve(obj, defaultValue); - return new ParticleSystem.MinMaxCurve(obj["multiplier"]?.ToObject() ?? 1f, curve); - - default: - return new ParticleSystem.MinMaxCurve(defaultValue); - } - } - - return new ParticleSystem.MinMaxCurve(defaultValue); - } - - public static ParticleSystem.MinMaxGradient ParseMinMaxGradient(JToken token) - { - if (token == null) - return new ParticleSystem.MinMaxGradient(Color.white); - - if (token is JArray arr && arr.Count >= 3) - { - return new ParticleSystem.MinMaxGradient(ManageVfxCommon.ParseColor(arr)); - } - - if (token is JObject obj) - { - string mode = obj["mode"]?.ToString()?.ToLowerInvariant() ?? "color"; - - switch (mode) - { - case "color": - return new ParticleSystem.MinMaxGradient(ManageVfxCommon.ParseColor(obj["color"])); - - case "two_colors": - Color colorMin = ManageVfxCommon.ParseColor(obj["colorMin"]); - Color colorMax = ManageVfxCommon.ParseColor(obj["colorMax"]); - return new ParticleSystem.MinMaxGradient(colorMin, colorMax); - - case "gradient": - return new ParticleSystem.MinMaxGradient(ManageVfxCommon.ParseGradient(obj)); - - default: - return new ParticleSystem.MinMaxGradient(Color.white); - } - } - - return new ParticleSystem.MinMaxGradient(Color.white); - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleCommon.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleCommon.cs.meta deleted file mode 100644 index a75d829..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleCommon.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: a3a91aa6f6b9c4121a2ccc1a8147bbf9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/ParticleCommon.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs deleted file mode 100644 index 4fb8584..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; -using MCPForUnity.Editor.Helpers; - -namespace MCPForUnity.Editor.Tools.Vfx -{ - internal static class ParticleControl - { - public static object EnableModule(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - string moduleName = @params["module"]?.ToString()?.ToLowerInvariant(); - bool enabled = @params["enabled"]?.ToObject() ?? true; - - if (string.IsNullOrEmpty(moduleName)) return new { success = false, message = "Module name required" }; - - Undo.RecordObject(ps, $"Toggle {moduleName}"); - - switch (moduleName.Replace("_", "")) - { - case "emission": var em = ps.emission; em.enabled = enabled; break; - case "shape": var sh = ps.shape; sh.enabled = enabled; break; - case "coloroverlifetime": var col = ps.colorOverLifetime; col.enabled = enabled; break; - case "sizeoverlifetime": var sol = ps.sizeOverLifetime; sol.enabled = enabled; break; - case "velocityoverlifetime": var vol = ps.velocityOverLifetime; vol.enabled = enabled; break; - case "noise": var n = ps.noise; n.enabled = enabled; break; - case "collision": var coll = ps.collision; coll.enabled = enabled; break; - case "trails": var tr = ps.trails; tr.enabled = enabled; break; - case "lights": var li = ps.lights; li.enabled = enabled; break; - default: return new { success = false, message = $"Unknown module: {moduleName}" }; - } - - EditorUtility.SetDirty(ps); - return new { success = true, message = $"Module '{moduleName}' {(enabled ? "enabled" : "disabled")}" }; - } - - public static object Control(JObject @params, string action) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - // Ensure material is assigned before playing - if (action == "play" || action == "restart") - { - var renderer = ps.GetComponent(); - if (renderer != null) - { - RendererHelpers.EnsureMaterial(renderer); - } - } - - bool withChildren = @params["withChildren"]?.ToObject() ?? true; - - switch (action) - { - case "play": ps.Play(withChildren); break; - case "stop": ps.Stop(withChildren, ParticleSystemStopBehavior.StopEmitting); break; - case "pause": ps.Pause(withChildren); break; - case "restart": ps.Stop(withChildren, ParticleSystemStopBehavior.StopEmittingAndClear); ps.Play(withChildren); break; - case "clear": ps.Clear(withChildren); break; - default: return new { success = false, message = $"Unknown action: {action}" }; - } - - return new { success = true, message = $"ParticleSystem {action}" }; - } - - public static object AddBurst(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - // Ensure material is assigned - var renderer = ps.GetComponent(); - if (renderer != null) - { - RendererHelpers.EnsureMaterial(renderer); - } - - Undo.RecordObject(ps, "Add Burst"); - var emission = ps.emission; - - float time = @params["time"]?.ToObject() ?? 0f; - int minCountRaw = @params["minCount"]?.ToObject() ?? @params["count"]?.ToObject() ?? 30; - int maxCountRaw = @params["maxCount"]?.ToObject() ?? @params["count"]?.ToObject() ?? 30; - short minCount = (short)Math.Clamp(minCountRaw, 0, short.MaxValue); - short maxCount = (short)Math.Clamp(maxCountRaw, 0, short.MaxValue); - int cycles = @params["cycles"]?.ToObject() ?? 1; - float interval = @params["interval"]?.ToObject() ?? 0.01f; - - var burst = new ParticleSystem.Burst(time, minCount, maxCount, cycles, interval); - burst.probability = @params["probability"]?.ToObject() ?? 1f; - - int idx = emission.burstCount; - var bursts = new ParticleSystem.Burst[idx + 1]; - emission.GetBursts(bursts); - bursts[idx] = burst; - emission.SetBursts(bursts); - - EditorUtility.SetDirty(ps); - return new { success = true, message = $"Added burst at t={time}", burstIndex = idx }; - } - - public static object ClearBursts(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - Undo.RecordObject(ps, "Clear Bursts"); - var emission = ps.emission; - int count = emission.burstCount; - emission.SetBursts(new ParticleSystem.Burst[0]); - - EditorUtility.SetDirty(ps); - return new { success = true, message = $"Cleared {count} bursts" }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs.meta deleted file mode 100644 index de48a3e..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 04e1bfb655f184337943edd5a3fbbcdb -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/ParticleControl.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleRead.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleRead.cs deleted file mode 100644 index 65afcd4..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleRead.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Newtonsoft.Json.Linq; -using System.Linq; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.Vfx -{ - internal static class ParticleRead - { - private static object SerializeAnimationCurve(AnimationCurve curve) - { - if (curve == null) - { - return null; - } - - return new - { - keys = curve.keys.Select(k => new - { - time = k.time, - value = k.value, - inTangent = k.inTangent, - outTangent = k.outTangent - }).ToArray() - }; - } - - private static object SerializeMinMaxCurve(ParticleSystem.MinMaxCurve curve) - { - switch (curve.mode) - { - case ParticleSystemCurveMode.Constant: - return new - { - mode = "constant", - value = curve.constant - }; - - case ParticleSystemCurveMode.TwoConstants: - return new - { - mode = "two_constants", - min = curve.constantMin, - max = curve.constantMax - }; - - case ParticleSystemCurveMode.Curve: - return new - { - mode = "curve", - multiplier = curve.curveMultiplier, - keys = curve.curve.keys.Select(k => new - { - time = k.time, - value = k.value, - inTangent = k.inTangent, - outTangent = k.outTangent - }).ToArray() - }; - - case ParticleSystemCurveMode.TwoCurves: - return new - { - mode = "curve", - multiplier = curve.curveMultiplier, - keys = curve.curveMax.keys.Select(k => new - { - time = k.time, - value = k.value, - inTangent = k.inTangent, - outTangent = k.outTangent - }).ToArray(), - originalMode = "two_curves", - curveMin = SerializeAnimationCurve(curve.curveMin), - curveMax = SerializeAnimationCurve(curve.curveMax) - }; - - default: - return new - { - mode = "constant", - value = curve.constant - }; - } - } - - public static object GetInfo(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) - { - return new { success = false, message = "ParticleSystem not found" }; - } - - var main = ps.main; - var emission = ps.emission; - var shape = ps.shape; - var renderer = ps.GetComponent(); - - return new - { - success = true, - data = new - { - gameObject = ps.gameObject.name, - isPlaying = ps.isPlaying, - isPaused = ps.isPaused, - particleCount = ps.particleCount, - main = new - { - duration = main.duration, - looping = main.loop, - startLifetime = SerializeMinMaxCurve(main.startLifetime), - startSpeed = SerializeMinMaxCurve(main.startSpeed), - startSize = SerializeMinMaxCurve(main.startSize), - gravityModifier = SerializeMinMaxCurve(main.gravityModifier), - simulationSpace = main.simulationSpace.ToString(), - maxParticles = main.maxParticles - }, - emission = new - { - enabled = emission.enabled, - rateOverTime = SerializeMinMaxCurve(emission.rateOverTime), - burstCount = emission.burstCount - }, - shape = new - { - enabled = shape.enabled, - shapeType = shape.shapeType.ToString(), - radius = shape.radius, - angle = shape.angle - }, - renderer = renderer != null ? new - { - renderMode = renderer.renderMode.ToString(), - sortMode = renderer.sortMode.ToString(), - material = renderer.sharedMaterial?.name, - trailMaterial = renderer.trailMaterial?.name, - minParticleSize = renderer.minParticleSize, - maxParticleSize = renderer.maxParticleSize, - shadowCastingMode = renderer.shadowCastingMode.ToString(), - receiveShadows = renderer.receiveShadows, - lightProbeUsage = renderer.lightProbeUsage.ToString(), - reflectionProbeUsage = renderer.reflectionProbeUsage.ToString(), - sortingOrder = renderer.sortingOrder, - sortingLayerName = renderer.sortingLayerName, - renderingLayerMask = renderer.renderingLayerMask - } : null - } - }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleRead.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleRead.cs.meta deleted file mode 100644 index e1e5b82..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleRead.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 74bb7c48a4e1944bcba43b3619653cb9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/ParticleRead.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs deleted file mode 100644 index 21c0384..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs +++ /dev/null @@ -1,295 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; -using MCPForUnity.Editor.Helpers; - -namespace MCPForUnity.Editor.Tools.Vfx -{ - internal static class ParticleWrite - { - public static object SetMain(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - // Ensure material is assigned before any configuration - var renderer = ps.GetComponent(); - if (renderer != null) - { - RendererHelpers.EnsureMaterial(renderer); - } - - // Stop particle system if it's playing and duration needs to be changed - bool wasPlaying = ps.isPlaying; - bool needsStop = @params["duration"] != null && wasPlaying; - if (needsStop) - { - ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); - } - - Undo.RecordObject(ps, "Set ParticleSystem Main"); - var main = ps.main; - var changes = new List(); - - if (@params["duration"] != null) { main.duration = @params["duration"].ToObject(); changes.Add("duration"); } - if (@params["looping"] != null) { main.loop = @params["looping"].ToObject(); changes.Add("looping"); } - if (@params["prewarm"] != null) { main.prewarm = @params["prewarm"].ToObject(); changes.Add("prewarm"); } - if (@params["startDelay"] != null) { main.startDelay = ParticleCommon.ParseMinMaxCurve(@params["startDelay"], 0f); changes.Add("startDelay"); } - if (@params["startLifetime"] != null) { main.startLifetime = ParticleCommon.ParseMinMaxCurve(@params["startLifetime"], 5f); changes.Add("startLifetime"); } - if (@params["startSpeed"] != null) { main.startSpeed = ParticleCommon.ParseMinMaxCurve(@params["startSpeed"], 5f); changes.Add("startSpeed"); } - if (@params["startSize"] != null) { main.startSize = ParticleCommon.ParseMinMaxCurve(@params["startSize"], 1f); changes.Add("startSize"); } - if (@params["startRotation"] != null) { main.startRotation = ParticleCommon.ParseMinMaxCurve(@params["startRotation"], 0f); changes.Add("startRotation"); } - if (@params["startColor"] != null) { main.startColor = ParticleCommon.ParseMinMaxGradient(@params["startColor"]); changes.Add("startColor"); } - if (@params["gravityModifier"] != null) { main.gravityModifier = ParticleCommon.ParseMinMaxCurve(@params["gravityModifier"], 0f); changes.Add("gravityModifier"); } - if (@params["simulationSpace"] != null && Enum.TryParse(@params["simulationSpace"].ToString(), true, out var simSpace)) { main.simulationSpace = simSpace; changes.Add("simulationSpace"); } - if (@params["scalingMode"] != null && Enum.TryParse(@params["scalingMode"].ToString(), true, out var scaleMode)) { main.scalingMode = scaleMode; changes.Add("scalingMode"); } - if (@params["playOnAwake"] != null) { main.playOnAwake = @params["playOnAwake"].ToObject(); changes.Add("playOnAwake"); } - if (@params["maxParticles"] != null) { main.maxParticles = @params["maxParticles"].ToObject(); changes.Add("maxParticles"); } - - EditorUtility.SetDirty(ps); - - // Restart particle system if it was playing - if (needsStop && wasPlaying) - { - ps.Play(true); - changes.Add("(restarted after duration change)"); - } - - return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; - } - - public static object SetEmission(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - // Ensure material is assigned - var renderer = ps.GetComponent(); - if (renderer != null) - { - RendererHelpers.EnsureMaterial(renderer); - } - - Undo.RecordObject(ps, "Set ParticleSystem Emission"); - var emission = ps.emission; - var changes = new List(); - - if (@params["enabled"] != null) { emission.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); } - if (@params["rateOverTime"] != null) { emission.rateOverTime = ParticleCommon.ParseMinMaxCurve(@params["rateOverTime"], 10f); changes.Add("rateOverTime"); } - if (@params["rateOverDistance"] != null) { emission.rateOverDistance = ParticleCommon.ParseMinMaxCurve(@params["rateOverDistance"], 0f); changes.Add("rateOverDistance"); } - - EditorUtility.SetDirty(ps); - return new { success = true, message = $"Updated emission: {string.Join(", ", changes)}" }; - } - - public static object SetShape(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - // Ensure material is assigned - var renderer = ps.GetComponent(); - if (renderer != null) - { - RendererHelpers.EnsureMaterial(renderer); - } - - Undo.RecordObject(ps, "Set ParticleSystem Shape"); - var shape = ps.shape; - var changes = new List(); - - if (@params["enabled"] != null) { shape.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); } - if (@params["shapeType"] != null && Enum.TryParse(@params["shapeType"].ToString(), true, out var shapeType)) { shape.shapeType = shapeType; changes.Add("shapeType"); } - if (@params["radius"] != null) { shape.radius = @params["radius"].ToObject(); changes.Add("radius"); } - if (@params["radiusThickness"] != null) { shape.radiusThickness = @params["radiusThickness"].ToObject(); changes.Add("radiusThickness"); } - if (@params["angle"] != null) { shape.angle = @params["angle"].ToObject(); changes.Add("angle"); } - if (@params["arc"] != null) { shape.arc = @params["arc"].ToObject(); changes.Add("arc"); } - if (@params["position"] != null) { shape.position = ManageVfxCommon.ParseVector3(@params["position"]); changes.Add("position"); } - if (@params["rotation"] != null) { shape.rotation = ManageVfxCommon.ParseVector3(@params["rotation"]); changes.Add("rotation"); } - if (@params["scale"] != null) { shape.scale = ManageVfxCommon.ParseVector3(@params["scale"]); changes.Add("scale"); } - - EditorUtility.SetDirty(ps); - return new { success = true, message = $"Updated shape: {string.Join(", ", changes)}" }; - } - - public static object SetColorOverLifetime(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - // Ensure material is assigned - var renderer = ps.GetComponent(); - if (renderer != null) - { - RendererHelpers.EnsureMaterial(renderer); - } - - Undo.RecordObject(ps, "Set ParticleSystem Color Over Lifetime"); - var col = ps.colorOverLifetime; - var changes = new List(); - - if (@params["enabled"] != null) { col.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); } - if (@params["color"] != null) { col.color = ParticleCommon.ParseMinMaxGradient(@params["color"]); changes.Add("color"); } - - EditorUtility.SetDirty(ps); - return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; - } - - public static object SetSizeOverLifetime(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - // Ensure material is assigned - var renderer = ps.GetComponent(); - if (renderer != null) - { - RendererHelpers.EnsureMaterial(renderer); - } - - Undo.RecordObject(ps, "Set ParticleSystem Size Over Lifetime"); - var sol = ps.sizeOverLifetime; - var changes = new List(); - - bool hasSizeProperty = @params["size"] != null || @params["sizeX"] != null || - @params["sizeY"] != null || @params["sizeZ"] != null; - if (hasSizeProperty && @params["enabled"] == null && !sol.enabled) - { - sol.enabled = true; - changes.Add("enabled"); - } - else if (@params["enabled"] != null) - { - sol.enabled = @params["enabled"].ToObject(); - changes.Add("enabled"); - } - - if (@params["separateAxes"] != null) { sol.separateAxes = @params["separateAxes"].ToObject(); changes.Add("separateAxes"); } - if (@params["size"] != null) { sol.size = ParticleCommon.ParseMinMaxCurve(@params["size"], 1f); changes.Add("size"); } - if (@params["sizeX"] != null) { sol.x = ParticleCommon.ParseMinMaxCurve(@params["sizeX"], 1f); changes.Add("sizeX"); } - if (@params["sizeY"] != null) { sol.y = ParticleCommon.ParseMinMaxCurve(@params["sizeY"], 1f); changes.Add("sizeY"); } - if (@params["sizeZ"] != null) { sol.z = ParticleCommon.ParseMinMaxCurve(@params["sizeZ"], 1f); changes.Add("sizeZ"); } - - EditorUtility.SetDirty(ps); - return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; - } - - public static object SetVelocityOverLifetime(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - // Ensure material is assigned - var renderer = ps.GetComponent(); - if (renderer != null) - { - RendererHelpers.EnsureMaterial(renderer); - } - - Undo.RecordObject(ps, "Set ParticleSystem Velocity Over Lifetime"); - var vol = ps.velocityOverLifetime; - var changes = new List(); - - if (@params["enabled"] != null) { vol.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); } - if (@params["space"] != null && Enum.TryParse(@params["space"].ToString(), true, out var space)) { vol.space = space; changes.Add("space"); } - if (@params["x"] != null) { vol.x = ParticleCommon.ParseMinMaxCurve(@params["x"], 0f); changes.Add("x"); } - if (@params["y"] != null) { vol.y = ParticleCommon.ParseMinMaxCurve(@params["y"], 0f); changes.Add("y"); } - if (@params["z"] != null) { vol.z = ParticleCommon.ParseMinMaxCurve(@params["z"], 0f); changes.Add("z"); } - if (@params["speedModifier"] != null) { vol.speedModifier = ParticleCommon.ParseMinMaxCurve(@params["speedModifier"], 1f); changes.Add("speedModifier"); } - - EditorUtility.SetDirty(ps); - return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; - } - - public static object SetNoise(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - // Ensure material is assigned - var renderer = ps.GetComponent(); - if (renderer != null) - { - RendererHelpers.EnsureMaterial(renderer); - } - - Undo.RecordObject(ps, "Set ParticleSystem Noise"); - var noise = ps.noise; - var changes = new List(); - - if (@params["enabled"] != null) { noise.enabled = @params["enabled"].ToObject(); changes.Add("enabled"); } - if (@params["strength"] != null) { noise.strength = ParticleCommon.ParseMinMaxCurve(@params["strength"], 1f); changes.Add("strength"); } - if (@params["frequency"] != null) { noise.frequency = @params["frequency"].ToObject(); changes.Add("frequency"); } - if (@params["scrollSpeed"] != null) { noise.scrollSpeed = ParticleCommon.ParseMinMaxCurve(@params["scrollSpeed"], 0f); changes.Add("scrollSpeed"); } - if (@params["damping"] != null) { noise.damping = @params["damping"].ToObject(); changes.Add("damping"); } - if (@params["octaveCount"] != null) { noise.octaveCount = @params["octaveCount"].ToObject(); changes.Add("octaveCount"); } - if (@params["quality"] != null && Enum.TryParse(@params["quality"].ToString(), true, out var quality)) { noise.quality = quality; changes.Add("quality"); } - - EditorUtility.SetDirty(ps); - return new { success = true, message = $"Updated noise: {string.Join(", ", changes)}" }; - } - - public static object SetRenderer(JObject @params) - { - ParticleSystem ps = ParticleCommon.FindParticleSystem(@params); - if (ps == null) return new { success = false, message = "ParticleSystem not found" }; - - var renderer = ps.GetComponent(); - if (renderer == null) return new { success = false, message = "ParticleSystemRenderer not found" }; - - // Ensure material is set before any other operations - RendererHelpers.EnsureMaterial(renderer); - - Undo.RecordObject(renderer, "Set ParticleSystem Renderer"); - var changes = new List(); - - if (@params["renderMode"] != null && Enum.TryParse(@params["renderMode"].ToString(), true, out var renderMode)) { renderer.renderMode = renderMode; changes.Add("renderMode"); } - if (@params["sortMode"] != null && Enum.TryParse(@params["sortMode"].ToString(), true, out var sortMode)) { renderer.sortMode = sortMode; changes.Add("sortMode"); } - - if (@params["minParticleSize"] != null) { renderer.minParticleSize = @params["minParticleSize"].ToObject(); changes.Add("minParticleSize"); } - if (@params["maxParticleSize"] != null) { renderer.maxParticleSize = @params["maxParticleSize"].ToObject(); changes.Add("maxParticleSize"); } - - if (@params["lengthScale"] != null) { renderer.lengthScale = @params["lengthScale"].ToObject(); changes.Add("lengthScale"); } - if (@params["velocityScale"] != null) { renderer.velocityScale = @params["velocityScale"].ToObject(); changes.Add("velocityScale"); } - if (@params["cameraVelocityScale"] != null) { renderer.cameraVelocityScale = @params["cameraVelocityScale"].ToObject(); changes.Add("cameraVelocityScale"); } - if (@params["normalDirection"] != null) { renderer.normalDirection = @params["normalDirection"].ToObject(); changes.Add("normalDirection"); } - - if (@params["alignment"] != null && Enum.TryParse(@params["alignment"].ToString(), true, out var alignment)) { renderer.alignment = alignment; changes.Add("alignment"); } - if (@params["pivot"] != null) { renderer.pivot = ManageVfxCommon.ParseVector3(@params["pivot"]); changes.Add("pivot"); } - if (@params["flip"] != null) { renderer.flip = ManageVfxCommon.ParseVector3(@params["flip"]); changes.Add("flip"); } - if (@params["allowRoll"] != null) { renderer.allowRoll = @params["allowRoll"].ToObject(); changes.Add("allowRoll"); } - - if (@params["shadowBias"] != null) { renderer.shadowBias = @params["shadowBias"].ToObject(); changes.Add("shadowBias"); } - - RendererHelpers.ApplyCommonRendererProperties(renderer, @params, changes); - - if (@params["materialPath"] != null) - { - string matPath = @params["materialPath"].ToString(); - var findInst = new JObject { ["find"] = matPath }; - Material mat = ObjectResolver.Resolve(findInst, typeof(Material)) as Material; - if (mat != null) - { - renderer.sharedMaterial = mat; - changes.Add($"material={mat.name}"); - } - else - { - McpLog.Warn($"Material not found at path: {matPath}. Keeping existing material."); - } - } - - if (@params["trailMaterialPath"] != null) - { - var findInst = new JObject { ["find"] = @params["trailMaterialPath"].ToString() }; - Material mat = ObjectResolver.Resolve(findInst, typeof(Material)) as Material; - if (mat != null) { renderer.trailMaterial = mat; changes.Add("trailMaterial"); } - } - - EditorUtility.SetDirty(renderer); - return new { success = true, message = $"Updated renderer: {string.Join(", ", changes)}" }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs.meta deleted file mode 100644 index a89dc39..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 2a68818a59fac4e2c83ad23433ddc9c1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/ParticleWrite.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs deleted file mode 100644 index ad6acc6..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; -using MCPForUnity.Editor.Helpers; - -namespace MCPForUnity.Editor.Tools.Vfx -{ - internal static class TrailControl - { - public static object Clear(JObject @params) - { - TrailRenderer tr = TrailRead.FindTrailRenderer(@params); - if (tr == null) return new { success = false, message = "TrailRenderer not found" }; - - Undo.RecordObject(tr, "Clear Trail"); - tr.Clear(); - return new { success = true, message = "Trail cleared" }; - } - - public static object Emit(JObject @params) - { - TrailRenderer tr = TrailRead.FindTrailRenderer(@params); - if (tr == null) return new { success = false, message = "TrailRenderer not found" }; - - RendererHelpers.EnsureMaterial(tr); - -#if UNITY_2021_1_OR_NEWER - Vector3 pos = ManageVfxCommon.ParseVector3(@params["position"]); - tr.AddPosition(pos); - return new { success = true, message = $"Emitted at ({pos.x}, {pos.y}, {pos.z})" }; -#else - return new { success = false, message = "AddPosition requires Unity 2021.1+" }; -#endif - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs.meta deleted file mode 100644 index 4cfb51e..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: edebad99699494d5585418395a2bf518 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/TrailControl.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailRead.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/TrailRead.cs deleted file mode 100644 index 4fae75a..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailRead.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Newtonsoft.Json.Linq; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.Vfx -{ - internal static class TrailRead - { - public static TrailRenderer FindTrailRenderer(JObject @params) - { - GameObject go = ManageVfxCommon.FindTargetGameObject(@params); - return go?.GetComponent(); - } - - public static object GetInfo(JObject @params) - { - TrailRenderer tr = FindTrailRenderer(@params); - if (tr == null) return new { success = false, message = "TrailRenderer not found" }; - - return new - { - success = true, - data = new - { - gameObject = tr.gameObject.name, - time = tr.time, - startWidth = tr.startWidth, - endWidth = tr.endWidth, - minVertexDistance = tr.minVertexDistance, - emitting = tr.emitting, - autodestruct = tr.autodestruct, - positionCount = tr.positionCount, - alignment = tr.alignment.ToString(), - textureMode = tr.textureMode.ToString(), - numCornerVertices = tr.numCornerVertices, - numCapVertices = tr.numCapVertices, - generateLightingData = tr.generateLightingData, - material = tr.sharedMaterial?.name, - shadowCastingMode = tr.shadowCastingMode.ToString(), - receiveShadows = tr.receiveShadows, - lightProbeUsage = tr.lightProbeUsage.ToString(), - reflectionProbeUsage = tr.reflectionProbeUsage.ToString(), - sortingOrder = tr.sortingOrder, - sortingLayerName = tr.sortingLayerName, - renderingLayerMask = tr.renderingLayerMask - } - }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailRead.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/TrailRead.cs.meta deleted file mode 100644 index f71666d..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailRead.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 2921f0042777b4ebbaec4c79c60908a1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/TrailRead.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs deleted file mode 100644 index fa11bbf..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections.Generic; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -namespace MCPForUnity.Editor.Tools.Vfx -{ - internal static class TrailWrite - { - public static object SetTime(JObject @params) - { - TrailRenderer tr = TrailRead.FindTrailRenderer(@params); - if (tr == null) return new { success = false, message = "TrailRenderer not found" }; - - RendererHelpers.EnsureMaterial(tr); - - float time = @params["time"]?.ToObject() ?? 5f; - - Undo.RecordObject(tr, "Set Trail Time"); - tr.time = time; - EditorUtility.SetDirty(tr); - - return new { success = true, message = $"Set trail time to {time}s" }; - } - - public static object SetWidth(JObject @params) - { - TrailRenderer tr = TrailRead.FindTrailRenderer(@params); - if (tr == null) return new { success = false, message = "TrailRenderer not found" }; - - RendererHelpers.EnsureMaterial(tr); - - Undo.RecordObject(tr, "Set Trail Width"); - var changes = new List(); - - RendererHelpers.ApplyWidthProperties(@params, changes, - v => tr.startWidth = v, v => tr.endWidth = v, - v => tr.widthCurve = v, v => tr.widthMultiplier = v, - ManageVfxCommon.ParseAnimationCurve); - - EditorUtility.SetDirty(tr); - return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; - } - - public static object SetColor(JObject @params) - { - TrailRenderer tr = TrailRead.FindTrailRenderer(@params); - if (tr == null) return new { success = false, message = "TrailRenderer not found" }; - - RendererHelpers.EnsureMaterial(tr); - - Undo.RecordObject(tr, "Set Trail Color"); - var changes = new List(); - - RendererHelpers.ApplyColorProperties(@params, changes, - v => tr.startColor = v, v => tr.endColor = v, - v => tr.colorGradient = v, - ManageVfxCommon.ParseColor, ManageVfxCommon.ParseGradient, fadeEndAlpha: true); - - EditorUtility.SetDirty(tr); - return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; - } - - public static object SetMaterial(JObject @params) - { - TrailRenderer tr = TrailRead.FindTrailRenderer(@params); - return RendererHelpers.SetRendererMaterial(tr, @params, "Set Trail Material", ManageVfxCommon.FindMaterialByPath); - } - - public static object SetProperties(JObject @params) - { - TrailRenderer tr = TrailRead.FindTrailRenderer(@params); - if (tr == null) return new { success = false, message = "TrailRenderer not found" }; - - RendererHelpers.EnsureMaterial(tr); - - Undo.RecordObject(tr, "Set Trail Properties"); - var changes = new List(); - - // Handle material if provided - if (@params["materialPath"] != null) - { - Material mat = ManageVfxCommon.FindMaterialByPath(@params["materialPath"].ToString()); - if (mat != null) - { - tr.sharedMaterial = mat; - changes.Add($"material={mat.name}"); - } - else - { - McpLog.Warn($"Material not found: {@params["materialPath"]}"); - } - } - - // Handle time if provided - if (@params["time"] != null) { tr.time = @params["time"].ToObject(); changes.Add("time"); } - - // Handle width properties if provided - if (@params["width"] != null || @params["startWidth"] != null || @params["endWidth"] != null) - { - if (@params["width"] != null) - { - float w = @params["width"].ToObject(); - tr.startWidth = w; - tr.endWidth = w; - changes.Add("width"); - } - if (@params["startWidth"] != null) { tr.startWidth = @params["startWidth"].ToObject(); changes.Add("startWidth"); } - if (@params["endWidth"] != null) { tr.endWidth = @params["endWidth"].ToObject(); changes.Add("endWidth"); } - } - - if (@params["minVertexDistance"] != null) { tr.minVertexDistance = @params["minVertexDistance"].ToObject(); changes.Add("minVertexDistance"); } - if (@params["autodestruct"] != null) { tr.autodestruct = @params["autodestruct"].ToObject(); changes.Add("autodestruct"); } - if (@params["emitting"] != null) { tr.emitting = @params["emitting"].ToObject(); changes.Add("emitting"); } - - RendererHelpers.ApplyLineTrailProperties(@params, changes, - null, null, - v => tr.numCornerVertices = v, v => tr.numCapVertices = v, - v => tr.alignment = v, v => tr.textureMode = v, - v => tr.generateLightingData = v); - - RendererHelpers.ApplyCommonRendererProperties(tr, @params, changes); - - EditorUtility.SetDirty(tr); - return new { success = true, message = $"Updated: {string.Join(", ", changes)}" }; - } - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs.meta deleted file mode 100644 index c5e4cfc..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 33ba432240c134206a4f71ab24f0fb3a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/TrailWrite.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs deleted file mode 100644 index 5f2e575..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs +++ /dev/null @@ -1,568 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -#if UNITY_VFX_GRAPH -using UnityEngine.VFX; -#endif - -namespace MCPForUnity.Editor.Tools.Vfx -{ - /// - /// Asset management operations for VFX Graph. - /// Handles creating, assigning, and listing VFX assets. - /// Requires com.unity.visualeffectgraph package and UNITY_VFX_GRAPH symbol. - /// - internal static class VfxGraphAssets - { -#if !UNITY_VFX_GRAPH - public static object CreateAsset(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object AssignAsset(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object ListTemplates(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object ListAssets(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } -#else - private static readonly string[] SupportedVfxGraphVersions = { "12.1" }; - - /// - /// Creates a new VFX Graph asset file from a template. - /// - public static object CreateAsset(JObject @params) - { - string assetName = @params["assetName"]?.ToString(); - string folderPath = @params["folderPath"]?.ToString() ?? "Assets/VFX"; - string template = @params["template"]?.ToString() ?? "empty"; - - if (string.IsNullOrEmpty(assetName)) - { - return new { success = false, message = "assetName is required" }; - } - - string versionError = ValidateVfxGraphVersion(); - if (!string.IsNullOrEmpty(versionError)) - { - return new { success = false, message = versionError }; - } - - // Ensure folder exists - if (!AssetDatabase.IsValidFolder(folderPath)) - { - string[] folders = folderPath.Split('/'); - string currentPath = folders[0]; - for (int i = 1; i < folders.Length; i++) - { - string newPath = currentPath + "/" + folders[i]; - if (!AssetDatabase.IsValidFolder(newPath)) - { - AssetDatabase.CreateFolder(currentPath, folders[i]); - } - currentPath = newPath; - } - } - - string assetPath = $"{folderPath}/{assetName}.vfx"; - - // Check if asset already exists - if (AssetDatabase.LoadAssetAtPath(assetPath) != null) - { - bool overwrite = @params["overwrite"]?.ToObject() ?? false; - if (!overwrite) - { - return new { success = false, message = $"Asset already exists at {assetPath}. Set overwrite=true to replace." }; - } - AssetDatabase.DeleteAsset(assetPath); - } - - // Find template asset and copy it - string templatePath = FindTemplate(template); - string templateAssetPath = TryGetAssetPathFromFileSystem(templatePath); - VisualEffectAsset newAsset = null; - - if (!string.IsNullOrEmpty(templateAssetPath)) - { - // Copy the asset to create a new VFX Graph asset - if (!AssetDatabase.CopyAsset(templateAssetPath, assetPath)) - { - return new { success = false, message = $"Failed to copy VFX template from {templateAssetPath}" }; - } - AssetDatabase.Refresh(); - newAsset = AssetDatabase.LoadAssetAtPath(assetPath); - } - else - { - return new { success = false, message = "VFX template not found. Add a .vfx template asset or install VFX Graph templates." }; - } - - if (newAsset == null) - { - return new { success = false, message = "Failed to create VFX asset. Try using a template from list_templates." }; - } - - return new - { - success = true, - message = $"Created VFX asset: {assetPath}", - data = new - { - assetPath = assetPath, - assetName = newAsset.name, - template = template - } - }; - } - - /// - /// Finds VFX template path by name. - /// - private static string FindTemplate(string templateName) - { - // Get the actual filesystem path for the VFX Graph package using PackageManager API - var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph"); - - var searchPaths = new List(); - - if (packageInfo != null) - { - // Use the resolved path from PackageManager (handles Library/PackageCache paths) - searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Editor/Templates")); - searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Samples")); - } - - // Also search project-local paths - searchPaths.Add("Assets/VFX/Templates"); - - string[] templatePatterns = new[] - { - $"{templateName}.vfx", - $"VFX{templateName}.vfx", - $"Simple{templateName}.vfx", - $"{templateName}VFX.vfx" - }; - - foreach (string basePath in searchPaths) - { - string searchRoot = basePath; - if (basePath.StartsWith("Assets/")) - { - searchRoot = System.IO.Path.Combine(UnityEngine.Application.dataPath, basePath.Substring("Assets/".Length)); - } - - if (!System.IO.Directory.Exists(searchRoot)) - { - continue; - } - - foreach (string pattern in templatePatterns) - { - string[] files = System.IO.Directory.GetFiles(searchRoot, pattern, System.IO.SearchOption.AllDirectories); - if (files.Length > 0) - { - return files[0]; - } - } - - // Also search by partial match - try - { - string[] allVfxFiles = System.IO.Directory.GetFiles(searchRoot, "*.vfx", System.IO.SearchOption.AllDirectories); - foreach (string file in allVfxFiles) - { - if (System.IO.Path.GetFileNameWithoutExtension(file).ToLower().Contains(templateName.ToLower())) - { - return file; - } - } - } - catch (Exception ex) - { - Debug.LogWarning($"Failed to search VFX templates under '{searchRoot}': {ex.Message}"); - } - } - - // Search in project assets - string[] guids = AssetDatabase.FindAssets("t:VisualEffectAsset " + templateName); - if (guids.Length > 0) - { - string assetPath = AssetDatabase.GUIDToAssetPath(guids[0]); - // Convert asset path (e.g., "Assets/...") to absolute filesystem path - if (!string.IsNullOrEmpty(assetPath) && assetPath.StartsWith("Assets/")) - { - return System.IO.Path.Combine(UnityEngine.Application.dataPath, assetPath.Substring("Assets/".Length)); - } - if (!string.IsNullOrEmpty(assetPath) && assetPath.StartsWith("Packages/")) - { - var info = UnityEditor.PackageManager.PackageInfo.FindForAssetPath(assetPath); - if (info != null) - { - string relPath = assetPath.Substring(("Packages/" + info.name + "/").Length); - return System.IO.Path.Combine(info.resolvedPath, relPath); - } - } - return null; - } - - return null; - } - - /// - /// Assigns a VFX asset to a VisualEffect component. - /// - public static object AssignAsset(JObject @params) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect component not found" }; - } - - string assetPath = @params["assetPath"]?.ToString(); - if (string.IsNullOrEmpty(assetPath)) - { - return new { success = false, message = "assetPath is required" }; - } - - // Validate and normalize path - // Reject absolute paths, parent directory traversal, and backslashes - if (assetPath.Contains("\\") || assetPath.Contains("..") || System.IO.Path.IsPathRooted(assetPath)) - { - return new { success = false, message = "Invalid assetPath: traversal and absolute paths are not allowed" }; - } - - if (assetPath.StartsWith("Packages/")) - { - return new { success = false, message = "Invalid assetPath: VFX assets must live under Assets/." }; - } - - if (!assetPath.StartsWith("Assets/")) - { - assetPath = "Assets/" + assetPath; - } - if (!assetPath.EndsWith(".vfx")) - { - assetPath += ".vfx"; - } - - // Verify the normalized path doesn't escape the project - string fullPath = System.IO.Path.Combine(UnityEngine.Application.dataPath, assetPath.Substring("Assets/".Length)); - string canonicalProjectRoot = System.IO.Path.GetFullPath(UnityEngine.Application.dataPath); - string canonicalAssetPath = System.IO.Path.GetFullPath(fullPath); - if (!canonicalAssetPath.StartsWith(canonicalProjectRoot + System.IO.Path.DirectorySeparatorChar) && - canonicalAssetPath != canonicalProjectRoot) - { - return new { success = false, message = "Invalid assetPath: would escape project directory" }; - } - - var asset = AssetDatabase.LoadAssetAtPath(assetPath); - if (asset == null) - { - // Try searching by name - string searchName = System.IO.Path.GetFileNameWithoutExtension(assetPath); - string[] guids = AssetDatabase.FindAssets($"t:VisualEffectAsset {searchName}"); - if (guids.Length > 0) - { - assetPath = AssetDatabase.GUIDToAssetPath(guids[0]); - asset = AssetDatabase.LoadAssetAtPath(assetPath); - } - } - - if (asset == null) - { - return new { success = false, message = $"VFX asset not found: {assetPath}" }; - } - - Undo.RecordObject(vfx, "Assign VFX Asset"); - vfx.visualEffectAsset = asset; - EditorUtility.SetDirty(vfx); - - return new - { - success = true, - message = $"Assigned VFX asset '{asset.name}' to {vfx.gameObject.name}", - data = new - { - gameObject = vfx.gameObject.name, - assetName = asset.name, - assetPath = assetPath - } - }; - } - - /// - /// Lists available VFX templates. - /// - public static object ListTemplates(JObject @params) - { - var templates = new List(); - var seenPaths = new HashSet(StringComparer.OrdinalIgnoreCase); - - // Get the actual filesystem path for the VFX Graph package using PackageManager API - var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph"); - - var searchPaths = new List(); - - if (packageInfo != null) - { - // Use the resolved path from PackageManager (handles Library/PackageCache paths) - searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Editor/Templates")); - searchPaths.Add(System.IO.Path.Combine(packageInfo.resolvedPath, "Samples")); - } - - // Also search project-local paths - searchPaths.Add("Assets/VFX/Templates"); - searchPaths.Add("Assets/VFX"); - - // Precompute normalized package path for comparison - string normalizedPackagePath = null; - if (packageInfo != null) - { - normalizedPackagePath = packageInfo.resolvedPath.Replace("\\", "/"); - } - - // Precompute the Assets base path for converting absolute paths to project-relative - string assetsBasePath = Application.dataPath.Replace("\\", "/"); - - foreach (string basePath in searchPaths) - { - if (!System.IO.Directory.Exists(basePath)) - { - continue; - } - - try - { - string[] vfxFiles = System.IO.Directory.GetFiles(basePath, "*.vfx", System.IO.SearchOption.AllDirectories); - foreach (string file in vfxFiles) - { - string absolutePath = file.Replace("\\", "/"); - string name = System.IO.Path.GetFileNameWithoutExtension(file); - bool isPackage = normalizedPackagePath != null && absolutePath.StartsWith(normalizedPackagePath); - - // Convert absolute path to project-relative path - string projectRelativePath; - if (isPackage) - { - // For package paths, convert to Packages/... format - projectRelativePath = "Packages/" + packageInfo.name + absolutePath.Substring(normalizedPackagePath.Length); - } - else if (absolutePath.StartsWith(assetsBasePath)) - { - // For project assets, convert to Assets/... format - projectRelativePath = "Assets" + absolutePath.Substring(assetsBasePath.Length); - } - else - { - // Fallback: use the absolute path if we can't determine the relative path - projectRelativePath = absolutePath; - } - - string normalizedPath = projectRelativePath.Replace("\\", "/"); - if (seenPaths.Add(normalizedPath)) - { - templates.Add(new { name = name, path = projectRelativePath, source = isPackage ? "package" : "project" }); - } - } - } - catch (Exception ex) - { - Debug.LogWarning($"Failed to list VFX templates under '{basePath}': {ex.Message}"); - } - } - - // Also search project assets - string[] guids = AssetDatabase.FindAssets("t:VisualEffectAsset"); - foreach (string guid in guids) - { - string path = AssetDatabase.GUIDToAssetPath(guid); - string normalizedPath = path.Replace("\\", "/"); - if (seenPaths.Add(normalizedPath)) - { - string name = System.IO.Path.GetFileNameWithoutExtension(path); - templates.Add(new { name = name, path = path, source = "project" }); - } - } - - return new - { - success = true, - data = new - { - count = templates.Count, - templates = templates - } - }; - } - - /// - /// Lists all VFX assets in the project. - /// - public static object ListAssets(JObject @params) - { - string searchFolder = @params["folder"]?.ToString(); - string searchPattern = @params["search"]?.ToString(); - - string filter = "t:VisualEffectAsset"; - if (!string.IsNullOrEmpty(searchPattern)) - { - filter += " " + searchPattern; - } - - string[] guids; - if (!string.IsNullOrEmpty(searchFolder)) - { - if (searchFolder.Contains("\\") || searchFolder.Contains("..") || System.IO.Path.IsPathRooted(searchFolder)) - { - return new { success = false, message = "Invalid folder: traversal and absolute paths are not allowed" }; - } - - if (searchFolder.StartsWith("Packages/")) - { - return new { success = false, message = "Invalid folder: VFX assets must live under Assets/." }; - } - - if (!searchFolder.StartsWith("Assets/")) - { - searchFolder = "Assets/" + searchFolder; - } - - string fullPath = System.IO.Path.Combine(UnityEngine.Application.dataPath, searchFolder.Substring("Assets/".Length)); - string canonicalProjectRoot = System.IO.Path.GetFullPath(UnityEngine.Application.dataPath); - string canonicalSearchFolder = System.IO.Path.GetFullPath(fullPath); - if (!canonicalSearchFolder.StartsWith(canonicalProjectRoot + System.IO.Path.DirectorySeparatorChar) && - canonicalSearchFolder != canonicalProjectRoot) - { - return new { success = false, message = "Invalid folder: would escape project directory" }; - } - - guids = AssetDatabase.FindAssets(filter, new[] { searchFolder }); - } - else - { - guids = AssetDatabase.FindAssets(filter); - } - - var assets = new List(); - foreach (string guid in guids) - { - string path = AssetDatabase.GUIDToAssetPath(guid); - var asset = AssetDatabase.LoadAssetAtPath(path); - if (asset != null) - { - assets.Add(new - { - name = asset.name, - path = path, - guid = guid - }); - } - } - - return new - { - success = true, - data = new - { - count = assets.Count, - assets = assets - } - }; - } - - private static string ValidateVfxGraphVersion() - { - var info = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph"); - if (info == null) - { - return "VFX Graph package (com.unity.visualeffectgraph) not installed"; - } - - if (IsVersionSupported(info.version)) - { - return null; - } - - string supported = string.Join(", ", SupportedVfxGraphVersions.Select(version => $"{version}.x")); - return $"Unsupported VFX Graph version {info.version}. Supported versions: {supported}."; - } - - private static bool IsVersionSupported(string installedVersion) - { - if (string.IsNullOrEmpty(installedVersion)) - { - return false; - } - - string normalized = installedVersion; - int suffixIndex = normalized.IndexOfAny(new[] { '-', '+' }); - if (suffixIndex >= 0) - { - normalized = normalized.Substring(0, suffixIndex); - } - - if (!Version.TryParse(normalized, out Version installed)) - { - return false; - } - - foreach (string supported in SupportedVfxGraphVersions) - { - if (!Version.TryParse(supported, out Version target)) - { - continue; - } - - if (installed.Major == target.Major && installed.Minor == target.Minor) - { - return true; - } - } - - return false; - } - - private static string TryGetAssetPathFromFileSystem(string templatePath) - { - if (string.IsNullOrEmpty(templatePath)) - { - return null; - } - - string normalized = templatePath.Replace("\\", "/"); - string assetsRoot = Application.dataPath.Replace("\\", "/"); - - if (normalized.StartsWith(assetsRoot + "/")) - { - return "Assets/" + normalized.Substring(assetsRoot.Length + 1); - } - - var packageInfo = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph"); - if (packageInfo != null) - { - string packageRoot = packageInfo.resolvedPath.Replace("\\", "/"); - if (normalized.StartsWith(packageRoot + "/")) - { - return "Packages/" + packageInfo.name + "/" + normalized.Substring(packageRoot.Length + 1); - } - } - - return null; - } -#endif - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs.meta deleted file mode 100644 index 3aab85d..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: a1dfb51f038764a6da23619cac60f299 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphCommon.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphCommon.cs deleted file mode 100644 index d342085..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphCommon.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Newtonsoft.Json.Linq; -using UnityEngine; - -#if UNITY_VFX_GRAPH -using UnityEngine.VFX; -#endif - -namespace MCPForUnity.Editor.Tools.Vfx -{ - /// - /// Common utilities for VFX Graph operations. - /// - internal static class VfxGraphCommon - { -#if UNITY_VFX_GRAPH - /// - /// Finds a VisualEffect component on the target GameObject. - /// - public static VisualEffect FindVisualEffect(JObject @params) - { - if (@params == null) - return null; - - GameObject go = ManageVfxCommon.FindTargetGameObject(@params); - return go?.GetComponent(); - } -#endif - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphCommon.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphCommon.cs.meta deleted file mode 100644 index f4ac784..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphCommon.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 0a6dbf78125194cf29b98d658af1039a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphCommon.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphControl.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphControl.cs deleted file mode 100644 index e906481..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphControl.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Newtonsoft.Json.Linq; -using UnityEditor; - -#if UNITY_VFX_GRAPH -using UnityEngine.VFX; -#endif - -namespace MCPForUnity.Editor.Tools.Vfx -{ - /// - /// Playback control operations for VFX Graph (VisualEffect component). - /// Requires com.unity.visualeffectgraph package and UNITY_VFX_GRAPH symbol. - /// - internal static class VfxGraphControl - { -#if !UNITY_VFX_GRAPH - public static object Control(JObject @params, string action) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object SetPlaybackSpeed(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object SetSeed(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } -#else - public static object Control(JObject @params, string action) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - switch (action) - { - case "play": vfx.Play(); break; - case "stop": vfx.Stop(); break; - case "pause": vfx.pause = !vfx.pause; break; - case "reinit": vfx.Reinit(); break; - default: - return new { success = false, message = $"Unknown VFX action: {action}" }; - } - - return new { success = true, message = $"VFX {action}", isPaused = vfx.pause }; - } - - public static object SetPlaybackSpeed(JObject @params) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - float rate = @params["playRate"]?.ToObject() ?? 1f; - Undo.RecordObject(vfx, "Set VFX Play Rate"); - vfx.playRate = rate; - EditorUtility.SetDirty(vfx); - - return new { success = true, message = $"Set play rate = {rate}" }; - } - - public static object SetSeed(JObject @params) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - uint seed = @params["seed"]?.ToObject() ?? 0; - bool resetOnPlay = @params["resetSeedOnPlay"]?.ToObject() ?? true; - - Undo.RecordObject(vfx, "Set VFX Seed"); - vfx.startSeed = seed; - vfx.resetSeedOnPlay = resetOnPlay; - EditorUtility.SetDirty(vfx); - - return new { success = true, message = $"Set seed = {seed}" }; - } -#endif - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphControl.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphControl.cs.meta deleted file mode 100644 index 20fa26d..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphControl.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 4720d53b13bc14989803670a788a1eaa -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphControl.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphRead.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphRead.cs deleted file mode 100644 index e3cb8f4..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphRead.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Newtonsoft.Json.Linq; -using UnityEngine; - -#if UNITY_VFX_GRAPH -using UnityEngine.VFX; -#endif - -namespace MCPForUnity.Editor.Tools.Vfx -{ - /// - /// Read operations for VFX Graph (VisualEffect component). - /// Requires com.unity.visualeffectgraph package and UNITY_VFX_GRAPH symbol. - /// - internal static class VfxGraphRead - { -#if !UNITY_VFX_GRAPH - public static object GetInfo(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } -#else - public static object GetInfo(JObject @params) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - return new - { - success = true, - data = new - { - gameObject = vfx.gameObject.name, - assetName = vfx.visualEffectAsset?.name ?? "None", - aliveParticleCount = vfx.aliveParticleCount, - culled = vfx.culled, - pause = vfx.pause, - playRate = vfx.playRate, - startSeed = vfx.startSeed - } - }; - } -#endif - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphRead.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphRead.cs.meta deleted file mode 100644 index 810950a..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphRead.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 419e293a95ea64af5ad6984b1d02b9b1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphRead.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphWrite.cs b/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphWrite.cs deleted file mode 100644 index 530bde9..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphWrite.cs +++ /dev/null @@ -1,310 +0,0 @@ -using System; -using MCPForUnity.Editor.Helpers; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; - -#if UNITY_VFX_GRAPH -using UnityEngine.VFX; -#endif - -namespace MCPForUnity.Editor.Tools.Vfx -{ - /// - /// Parameter setter operations for VFX Graph (VisualEffect component). - /// Requires com.unity.visualeffectgraph package and UNITY_VFX_GRAPH symbol. - /// - internal static class VfxGraphWrite - { -#if !UNITY_VFX_GRAPH - public static object SetParameter(JObject @params, Action setter) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object SetVector(JObject @params, int dims) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object SetColor(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object SetGradient(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object SetTexture(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object SetMesh(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object SetCurve(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } - - public static object SendEvent(JObject @params) - { - return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; - } -#else - public static object SetParameter(JObject @params, Action setter) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - string param = @params["parameter"]?.ToString(); - if (string.IsNullOrEmpty(param)) - { - return new { success = false, message = "Parameter name required" }; - } - - JToken valueToken = @params["value"]; - if (valueToken == null) - { - return new { success = false, message = "Value required" }; - } - - // Safely deserialize the value - T value; - try - { - value = valueToken.ToObject(); - } - catch (JsonException ex) - { - return new { success = false, message = $"Invalid value for {param}: {ex.Message}" }; - } - catch (InvalidCastException ex) - { - return new { success = false, message = $"Invalid value type for {param}: {ex.Message}" }; - } - - Undo.RecordObject(vfx, $"Set VFX {param}"); - setter(vfx, param, value); - EditorUtility.SetDirty(vfx); - - return new { success = true, message = $"Set {param} = {value}" }; - } - - public static object SetVector(JObject @params, int dims) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - string param = @params["parameter"]?.ToString(); - if (string.IsNullOrEmpty(param)) - { - return new { success = false, message = "Parameter name required" }; - } - - if (dims != 2 && dims != 3 && dims != 4) - { - return new { success = false, message = $"Unsupported vector dimension: {dims}. Expected 2, 3, or 4." }; - } - - Vector4 vec = ManageVfxCommon.ParseVector4(@params["value"]); - Undo.RecordObject(vfx, $"Set VFX {param}"); - - switch (dims) - { - case 2: vfx.SetVector2(param, new Vector2(vec.x, vec.y)); break; - case 3: vfx.SetVector3(param, new Vector3(vec.x, vec.y, vec.z)); break; - case 4: vfx.SetVector4(param, vec); break; - } - - EditorUtility.SetDirty(vfx); - return new { success = true, message = $"Set {param}" }; - } - - public static object SetColor(JObject @params) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - string param = @params["parameter"]?.ToString(); - if (string.IsNullOrEmpty(param)) - { - return new { success = false, message = "Parameter name required" }; - } - - Color color = ManageVfxCommon.ParseColor(@params["value"]); - Undo.RecordObject(vfx, $"Set VFX Color {param}"); - vfx.SetVector4(param, new Vector4(color.r, color.g, color.b, color.a)); - EditorUtility.SetDirty(vfx); - - return new { success = true, message = $"Set color {param}" }; - } - - public static object SetGradient(JObject @params) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - string param = @params["parameter"]?.ToString(); - if (string.IsNullOrEmpty(param)) - { - return new { success = false, message = "Parameter name required" }; - } - - Gradient gradient = ManageVfxCommon.ParseGradient(@params["gradient"]); - Undo.RecordObject(vfx, $"Set VFX Gradient {param}"); - vfx.SetGradient(param, gradient); - EditorUtility.SetDirty(vfx); - - return new { success = true, message = $"Set gradient {param}" }; - } - - public static object SetTexture(JObject @params) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - string param = @params["parameter"]?.ToString(); - string path = @params["texturePath"]?.ToString(); - if (string.IsNullOrEmpty(param) || string.IsNullOrEmpty(path)) - { - return new { success = false, message = "Parameter and texturePath required" }; - } - - var findInst = new JObject { ["find"] = path }; - Texture tex = ObjectResolver.Resolve(findInst, typeof(Texture)) as Texture; - if (tex == null) - { - return new { success = false, message = $"Texture not found: {path}" }; - } - - Undo.RecordObject(vfx, $"Set VFX Texture {param}"); - vfx.SetTexture(param, tex); - EditorUtility.SetDirty(vfx); - - return new { success = true, message = $"Set texture {param} = {tex.name}" }; - } - - public static object SetMesh(JObject @params) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - string param = @params["parameter"]?.ToString(); - string path = @params["meshPath"]?.ToString(); - if (string.IsNullOrEmpty(param) || string.IsNullOrEmpty(path)) - { - return new { success = false, message = "Parameter and meshPath required" }; - } - - var findInst = new JObject { ["find"] = path }; - Mesh mesh = ObjectResolver.Resolve(findInst, typeof(Mesh)) as Mesh; - if (mesh == null) - { - return new { success = false, message = $"Mesh not found: {path}" }; - } - - Undo.RecordObject(vfx, $"Set VFX Mesh {param}"); - vfx.SetMesh(param, mesh); - EditorUtility.SetDirty(vfx); - - return new { success = true, message = $"Set mesh {param} = {mesh.name}" }; - } - - public static object SetCurve(JObject @params) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - string param = @params["parameter"]?.ToString(); - if (string.IsNullOrEmpty(param)) - { - return new { success = false, message = "Parameter name required" }; - } - - AnimationCurve curve = ManageVfxCommon.ParseAnimationCurve(@params["curve"], 1f); - Undo.RecordObject(vfx, $"Set VFX Curve {param}"); - vfx.SetAnimationCurve(param, curve); - EditorUtility.SetDirty(vfx); - - return new { success = true, message = $"Set curve {param}" }; - } - - public static object SendEvent(JObject @params) - { - VisualEffect vfx = VfxGraphCommon.FindVisualEffect(@params); - if (vfx == null) - { - return new { success = false, message = "VisualEffect not found" }; - } - - string eventName = @params["eventName"]?.ToString(); - if (string.IsNullOrEmpty(eventName)) - { - return new { success = false, message = "Event name required" }; - } - - VFXEventAttribute attr = vfx.CreateVFXEventAttribute(); - if (@params["position"] != null) - { - attr.SetVector3("position", ManageVfxCommon.ParseVector3(@params["position"])); - } - if (@params["velocity"] != null) - { - attr.SetVector3("velocity", ManageVfxCommon.ParseVector3(@params["velocity"])); - } - if (@params["color"] != null) - { - var c = ManageVfxCommon.ParseColor(@params["color"]); - attr.SetVector3("color", new Vector3(c.r, c.g, c.b)); - } - if (@params["size"] != null) - { - float? sizeValue = @params["size"].Value(); - if (sizeValue.HasValue) - { - attr.SetFloat("size", sizeValue.Value); - } - } - if (@params["lifetime"] != null) - { - float? lifetimeValue = @params["lifetime"].Value(); - if (lifetimeValue.HasValue) - { - attr.SetFloat("lifetime", lifetimeValue.Value); - } - } - - vfx.SendEvent(eventName, attr); - return new { success = true, message = $"Sent event '{eventName}'" }; - } -#endif - } -} diff --git a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphWrite.cs.meta b/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphWrite.cs.meta deleted file mode 100644 index 7aed20a..0000000 --- a/Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphWrite.cs.meta +++ /dev/null @@ -1,18 +0,0 @@ -fileFormatVersion: 2 -guid: 7516cdde6a4b648c9a2def6c26103cc4 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 329908 - packageName: MCP for Unity | AI Driven Development - packageVersion: 9.0.3 - assetPath: Assets/MCPForUnity/Editor/Tools/Vfx/VfxGraphWrite.cs - uploadId: 855486 diff --git a/Assets/MCPForUnity/Editor/Windows.meta b/Assets/MCPForUnity/Editor/Windows.meta deleted file mode 100644 index eda016e..0000000 --- a/Assets/MCPForUnity/Editor/Windows.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: d2ee39f5d4171184eb208e865c1ef4c1 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Windows/Components.meta b/Assets/MCPForUnity/Editor/Windows/Components.meta deleted file mode 100644 index 716e0fa..0000000 --- a/Assets/MCPForUnity/Editor/Windows/Components.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 82074be914aefa84cb557c599d2319b3 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Windows/Components/Advanced.meta b/Assets/MCPForUnity/Editor/Windows/Components/Advanced.meta deleted file mode 100644 index 9c3d024..0000000 --- a/Assets/MCPForUnity/Editor/Windows/Components/Advanced.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 7723ed5eaaccb104e93acb9fd2d8cd32 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs b/Assets/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs deleted file mode 100644 index 8898fcf..0000000 --- a/Assets/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs +++ /dev/null @@ -1,467 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; -using UnityEditor; -using UnityEngine; -using UnityEngine.UIElements; - -namespace MCPForUnity.Editor.Windows.Components.Advanced -{ - /// - /// Controller for the Advanced Settings section. - /// Handles path overrides, server source configuration, dev mode, and package deployment. - /// - public class McpAdvancedSection - { - // UI Elements - private TextField uvxPathOverride; - private Button browseUvxButton; - private Button clearUvxButton; - private VisualElement uvxPathStatus; - private TextField gitUrlOverride; - private Button browseGitUrlButton; - private Button clearGitUrlButton; - private Toggle debugLogsToggle; - private Toggle devModeForceRefreshToggle; - private Toggle useBetaServerToggle; - private TextField deploySourcePath; - private Button browseDeploySourceButton; - private Button clearDeploySourceButton; - private Button deployButton; - private Button deployRestoreButton; - private Label deployTargetLabel; - private Label deployBackupLabel; - private Label deployStatusLabel; - private VisualElement healthIndicator; - private Label healthStatus; - private Button testConnectionButton; - - // Events - public event Action OnGitUrlChanged; - public event Action OnHttpServerCommandUpdateRequested; - public event Action OnTestConnectionRequested; - public event Action OnBetaModeChanged; - - public VisualElement Root { get; private set; } - - public McpAdvancedSection(VisualElement root) - { - Root = root; - CacheUIElements(); - InitializeUI(); - RegisterCallbacks(); - } - - private void CacheUIElements() - { - uvxPathOverride = Root.Q("uv-path-override"); - browseUvxButton = Root.Q