diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml
index 9cb8e1cd3..bdbac9886 100644
--- a/.github/workflows/bump-version.yml
+++ b/.github/workflows/bump-version.yml
@@ -67,11 +67,11 @@ jobs:
jq ".version = \"${NEW_VERSION}\"" MCPForUnity/package.json > MCPForUnity/package.json.tmp
mv MCPForUnity/package.json.tmp MCPForUnity/package.json
- echo "Updating MCPForUnity/UnityMcpServer~/src/pyproject.toml to $NEW_VERSION"
- sed -i '0,/^version = ".*"/s//version = "'"$NEW_VERSION"'"/' "MCPForUnity/UnityMcpServer~/src/pyproject.toml"
+ echo "Updating Server/pyproject.toml to $NEW_VERSION"
+ sed -i '0,/^version = ".*"/s//version = "'"$NEW_VERSION"'"/' "Server/pyproject.toml"
- echo "Updating MCPForUnity/UnityMcpServer~/src/server_version.txt to $NEW_VERSION"
- echo "$NEW_VERSION" > "MCPForUnity/UnityMcpServer~/src/server_version.txt"
+ echo "Updating Server/server_version.txt to $NEW_VERSION"
+ echo "$NEW_VERSION" > "Server/server_version.txt"
- name: Commit and push changes
env:
@@ -81,7 +81,7 @@ jobs:
set -euo pipefail
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
- git add MCPForUnity/package.json "MCPForUnity/UnityMcpServer~/src/pyproject.toml" "MCPForUnity/UnityMcpServer~/src/server_version.txt"
+ git add MCPForUnity/package.json "Server/pyproject.toml" "Server/server_version.txt"
if git diff --cached --quiet; then
echo "No version changes to commit."
else
diff --git a/.gitignore b/.gitignore
index d56cf6cc3..81f0d6684 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,8 +16,6 @@ build/
dist/
wheels/
*.egg-info
-UnityMcpServer/**/*.meta
-UnityMcpServer.meta
# Virtual environments
.venv
diff --git a/MCPForUnity/Editor/Data/PythonToolsAsset.cs b/MCPForUnity/Editor/Data/PythonToolsAsset.cs
deleted file mode 100644
index 22719a57c..000000000
--- a/MCPForUnity/Editor/Data/PythonToolsAsset.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using UnityEngine;
-
-namespace MCPForUnity.Editor.Data
-{
- ///
- /// Registry of Python tool files to sync to the MCP server.
- /// Add your Python files here - they can be stored anywhere in your project.
- ///
- [CreateAssetMenu(fileName = "PythonTools", menuName = "MCP For Unity/Python Tools")]
- public class PythonToolsAsset : ScriptableObject
- {
- [Tooltip("Add Python files (.py) to sync to the MCP server. Files can be located anywhere in your project.")]
- public List pythonFiles = new List();
-
- [Header("Sync Options")]
- [Tooltip("Use content hashing to detect changes (recommended). If false, always copies on startup.")]
- public bool useContentHashing = true;
-
- [Header("Sync State (Read-only)")]
- [Tooltip("Internal tracking - do not modify")]
- public List fileStates = new List();
-
- ///
- /// Gets all valid Python files (filters out null/missing references)
- ///
- public IEnumerable GetValidFiles()
- {
- return pythonFiles.Where(f => f != null);
- }
-
- ///
- /// Checks if a file needs syncing
- ///
- public bool NeedsSync(TextAsset file, string currentHash)
- {
- if (!useContentHashing) return true; // Always sync if hashing disabled
-
- var state = fileStates.FirstOrDefault(s => s.assetGuid == GetAssetGuid(file));
- return state == null || state.contentHash != currentHash;
- }
-
- ///
- /// Records that a file was synced
- ///
- public void RecordSync(TextAsset file, string hash)
- {
- string guid = GetAssetGuid(file);
- var state = fileStates.FirstOrDefault(s => s.assetGuid == guid);
-
- if (state == null)
- {
- state = new PythonFileState { assetGuid = guid };
- fileStates.Add(state);
- }
-
- state.contentHash = hash;
- state.lastSyncTime = DateTime.UtcNow;
- state.fileName = file.name;
- }
-
- ///
- /// Removes state entries for files no longer in the list
- ///
- public void CleanupStaleStates()
- {
- var validGuids = new HashSet(GetValidFiles().Select(GetAssetGuid));
- fileStates.RemoveAll(s => !validGuids.Contains(s.assetGuid));
- }
-
- private string GetAssetGuid(TextAsset asset)
- {
- return UnityEditor.AssetDatabase.AssetPathToGUID(UnityEditor.AssetDatabase.GetAssetPath(asset));
- }
-
- ///
- /// Called when the asset is modified in the Inspector
- /// Triggers sync to handle file additions/removals
- ///
- private void OnValidate()
- {
- // Cleanup stale states immediately
- CleanupStaleStates();
-
- // Trigger sync after a delay to handle file removals
- // Delay ensures the asset is saved before sync runs
- UnityEditor.EditorApplication.delayCall += () =>
- {
- if (this != null) // Check if asset still exists
- {
- MCPForUnity.Editor.Helpers.PythonToolSyncProcessor.SyncAllTools();
- }
- };
- }
- }
-
- [Serializable]
- public class PythonFileState
- {
- public string assetGuid;
- public string fileName;
- public string contentHash;
- public DateTime lastSyncTime;
- }
-}
\ No newline at end of file
diff --git a/MCPForUnity/Editor/Dependencies/DependencyManager.cs b/MCPForUnity/Editor/Dependencies/DependencyManager.cs
index ce6efef23..cdb0e7bbc 100644
--- a/MCPForUnity/Editor/Dependencies/DependencyManager.cs
+++ b/MCPForUnity/Editor/Dependencies/DependencyManager.cs
@@ -60,10 +60,6 @@ public static DependencyCheckResult CheckAllDependencies()
var uvStatus = detector.DetectUV();
result.Dependencies.Add(uvStatus);
- // Check MCP Server
- var serverStatus = detector.DetectMCPServer();
- result.Dependencies.Add(serverStatus);
-
// Generate summary and recommendations
result.GenerateSummary();
GenerateRecommendations(result, detector);
diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs
index 7fba58f92..773b9db65 100644
--- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs
+++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/IPlatformDetector.cs
@@ -27,11 +27,6 @@ public interface IPlatformDetector
///
DependencyStatus DetectUV();
- ///
- /// Detect MCP server installation on this platform
- ///
- DependencyStatus DetectMCPServer();
-
///
/// Get platform-specific installation recommendations
///
diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs
index 98044f17e..5c5446090 100644
--- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs
+++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/PlatformDetectorBase.cs
@@ -3,6 +3,7 @@
using System.IO;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
@@ -29,15 +30,15 @@ public virtual DependencyStatus DetectUV()
try
{
// Use existing UV detection from ServerInstaller
- string uvPath = ServerInstaller.FindUvPath();
- if (!string.IsNullOrEmpty(uvPath))
+ string uvxPath = MCPServiceLocator.Paths.GetUvxPath(verifyPath: false);
+ if (!string.IsNullOrEmpty(uvxPath))
{
- if (TryValidateUV(uvPath, out string version))
+ if (TryValidateUvx(uvxPath, out string version))
{
status.IsAvailable = true;
status.Version = version;
- status.Path = uvPath;
- status.Details = $"Found UV {version} at {uvPath}";
+ status.Path = uvxPath;
+ status.Details = $"Found UV {version} at {uvxPath}";
return status;
}
}
@@ -53,55 +54,7 @@ public virtual DependencyStatus DetectUV()
return status;
}
- public virtual DependencyStatus DetectMCPServer()
- {
- var status = new DependencyStatus("MCP Server", isRequired: false);
-
- try
- {
- // Check if server is installed
- string serverPath = ServerInstaller.GetServerPath();
- string serverPy = Path.Combine(serverPath, "server.py");
-
- if (File.Exists(serverPy))
- {
- status.IsAvailable = true;
- status.Path = serverPath;
-
- // Try to get version
- string versionFile = Path.Combine(serverPath, "server_version.txt");
- if (File.Exists(versionFile))
- {
- status.Version = File.ReadAllText(versionFile).Trim();
- }
-
- status.Details = $"MCP Server found at {serverPath}";
- }
- else
- {
- // Check for embedded server
- if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath))
- {
- status.IsAvailable = true;
- status.Path = embeddedPath;
- status.Details = "MCP Server available (embedded in package)";
- }
- else
- {
- status.ErrorMessage = "MCP Server not found";
- status.Details = "Server will be installed automatically when needed";
- }
- }
- }
- catch (Exception ex)
- {
- status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}";
- }
-
- return status;
- }
-
- protected bool TryValidateUV(string uvPath, out string version)
+ protected bool TryValidateUvx(string uvxPath, out string version)
{
version = null;
@@ -109,7 +62,7 @@ protected bool TryValidateUV(string uvPath, out string version)
{
var psi = new ProcessStartInfo
{
- FileName = uvPath,
+ FileName = uvxPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
diff --git a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs
index dac1facfd..a3620c96a 100644
--- a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs
+++ b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs
@@ -4,6 +4,7 @@
using UnityEditor;
using UnityEngine;
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
+using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Helpers
{
@@ -136,7 +137,50 @@ public static JObject GetPackageJson()
}
///
- /// Gets the version string from the package.json file.
+ /// Gets the uvx command with the correct package version for running the MCP server
+ ///
+ /// Uvx command string, or "uvx" if version is unknown
+ public static string GetUvxCommand()
+ {
+ string version = GetPackageVersion();
+ if (version == "unknown")
+ {
+ return "uvx";
+ }
+
+ return $"uvx --from git+https://github.com/CoplayDev/unity-mcp@v{version}#subdirectory=Server";
+ }
+
+ ///
+ /// Gets just the git URL part for the MCP server package
+ ///
+ /// Git URL string, or empty string if version is unknown
+ public static string GetMcpServerGitUrl()
+ {
+ string version = GetPackageVersion();
+ if (version == "unknown")
+ {
+ return "";
+ }
+
+ return $"git+https://github.com/CoplayDev/unity-mcp@v{version}#subdirectory=Server";
+ }
+
+ ///
+ /// 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() ?? "uvx";
+ string fromUrl = GetMcpServerGitUrl();
+ string packageName = "mcp-for-unity";
+
+ return (uvxPath, fromUrl, packageName);
+ }
+
+ ///
+ /// Gets the package version from package.json
///
/// Version string, or "unknown" if not found
public static string GetPackageVersion()
diff --git a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
index a4728901c..a41eb6ee2 100644
--- a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
+++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
@@ -14,36 +14,37 @@ namespace MCPForUnity.Editor.Helpers
///
public static class CodexConfigHelper
{
- public static bool IsCodexConfigured(string pythonDir)
+ public static string BuildCodexServerBlock(string uvPath)
{
- try
- {
- string basePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
- if (string.IsNullOrEmpty(basePath)) return false;
-
- string configPath = Path.Combine(basePath, ".codex", "config.toml");
- if (!File.Exists(configPath)) return false;
-
- string toml = File.ReadAllText(configPath);
- if (!TryParseCodexServer(toml, out _, out var args)) return false;
-
- string dir = McpConfigurationHelper.ExtractDirectoryArg(args);
- if (string.IsNullOrEmpty(dir)) return false;
+ var table = new TomlTable();
+ var mcpServers = new TomlTable();
- return McpConfigurationHelper.PathsEqual(dir, pythonDir);
- }
- catch
+ // Use structured uvx command parts for proper TOML formatting
+ var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
+
+ var unityMCP = new TomlTable();
+ unityMCP["command"] = uvxPath;
+
+ var args = new TomlArray();
+ if (!string.IsNullOrEmpty(fromUrl))
{
- return false;
+ args.Add(new TomlString { Value = "--from" });
+ args.Add(new TomlString { Value = fromUrl });
}
- }
+ args.Add(new TomlString { Value = packageName });
+
+ unityMCP["args"] = args;
- public static string BuildCodexServerBlock(string uvPath, string serverSrc)
- {
- var table = new TomlTable();
- var mcpServers = new TomlTable();
+ // Add Windows-specific environment configuration, see: https://github.com/CoplayDev/unity-mcp/issues/315
+ var platformService = MCPServiceLocator.Platform;
+ if (platformService.IsWindows())
+ {
+ var envTable = new TomlTable { IsInline = true };
+ envTable["SystemRoot"] = new TomlString { Value = platformService.GetSystemRoot() };
+ unityMCP["env"] = envTable;
+ }
- mcpServers["unityMCP"] = CreateUnityMcpTable(uvPath, serverSrc);
+ mcpServers["unityMCP"] = unityMCP;
table["mcp_servers"] = mcpServers;
using var writer = new StringWriter();
@@ -51,7 +52,7 @@ public static string BuildCodexServerBlock(string uvPath, string serverSrc)
return writer.ToString();
}
- public static string UpsertCodexServerBlock(string existingToml, string uvPath, string serverSrc)
+ public static string UpsertCodexServerBlock(string existingToml, string uvPath)
{
// Parse existing TOML or create new root table
var root = TryParseToml(existingToml) ?? new TomlTable();
@@ -64,7 +65,7 @@ public static string UpsertCodexServerBlock(string existingToml, string uvPath,
var mcpServers = root["mcp_servers"] as TomlTable;
// Create or update unityMCP table
- mcpServers["unityMCP"] = CreateUnityMcpTable(uvPath, serverSrc);
+ mcpServers["unityMCP"] = CreateUnityMcpTable(uvPath);
// Serialize back to TOML
using var writer = new StringWriter();
@@ -126,18 +127,22 @@ private static TomlTable TryParseToml(string toml)
///
/// Creates a TomlTable for the unityMCP server configuration
///
- /// Path to uv executable
- /// Path to server source directory
- private static TomlTable CreateUnityMcpTable(string uvPath, string serverSrc)
+ /// Path to uv executable (used as fallback if uvx is not available)
+ private static TomlTable CreateUnityMcpTable(string uvPath)
{
var unityMCP = new TomlTable();
- unityMCP["command"] = new TomlString { Value = uvPath };
+
+ // Use structured uvx command parts for proper TOML formatting
+ var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
+ unityMCP["command"] = new TomlString { Value = uvxPath };
var argsArray = new TomlArray();
- argsArray.Add(new TomlString { Value = "run" });
- argsArray.Add(new TomlString { Value = "--directory" });
- argsArray.Add(new TomlString { Value = serverSrc });
- argsArray.Add(new TomlString { Value = "server.py" });
+ if (!string.IsNullOrEmpty(fromUrl))
+ {
+ argsArray.Add(new TomlString { Value = "--from" });
+ argsArray.Add(new TomlString { Value = fromUrl });
+ }
+ argsArray.Add(new TomlString { Value = packageName });
unityMCP["args"] = argsArray;
// Add Windows-specific environment configuration, see: https://github.com/CoplayDev/unity-mcp/issues/315
diff --git a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
index 5889e4f6b..fe311456e 100644
--- a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
+++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
@@ -1,12 +1,15 @@
using Newtonsoft.Json;
+using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Models;
+using MCPForUnity.Editor.Helpers;
+using UnityEditor;
namespace MCPForUnity.Editor.Helpers
{
public static class ConfigJsonBuilder
{
- public static string BuildManualConfigJson(string uvPath, string pythonDir, McpClient client)
+ public static string BuildManualConfigJson(string uvPath, McpClient client)
{
var root = new JObject();
bool isVSCode = client?.mcpType == McpTypes.VSCode;
@@ -21,20 +24,20 @@ public static string BuildManualConfigJson(string uvPath, string pythonDir, McpC
}
var unity = new JObject();
- PopulateUnityNode(unity, uvPath, pythonDir, client, isVSCode);
+ PopulateUnityNode(unity, uvPath, client, isVSCode);
container["unityMCP"] = unity;
return root.ToString(Formatting.Indented);
}
- public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, string serverSrc, McpClient client)
+ public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, McpClient client)
{
if (root == null) root = new JObject();
bool isVSCode = client?.mcpType == McpTypes.VSCode;
JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
JObject unity = container["unityMCP"] as JObject ?? new JObject();
- PopulateUnityNode(unity, uvPath, serverSrc, client, isVSCode);
+ PopulateUnityNode(unity, uvPath, client, isVSCode);
container["unityMCP"] = unity;
return root;
@@ -42,66 +45,63 @@ public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPa
///
/// Centralized builder that applies all caveats consistently.
- /// - Sets command/args with provided directory
+ /// - Sets command/args with uvx and package version
/// - Ensures env exists
- /// - Adds type:"stdio" for VSCode
+ /// - Adds transport configuration (HTTP or stdio)
/// - Adds disabled:false for Windsurf/Kiro only when missing
///
- private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode)
+ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient client, bool isVSCode)
{
- unity["command"] = uvPath;
-
- // For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners
- string effectiveDir = directory;
-#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
- bool isCursor = !isVSCode && (client == null || client.mcpType != McpTypes.VSCode);
- if (isCursor && !string.IsNullOrEmpty(directory))
+ // Get transport preference (default to HTTP)
+ bool useHttpTransport = EditorPrefs.GetBool("MCPForUnity.UseHttpTransport", true);
+
+ if (useHttpTransport)
{
- // Replace canonical path segment with the symlink path if present
- const string canonical = "/Library/Application Support/";
- const string symlinkSeg = "/Library/AppSupport/";
- try
+ // HTTP mode: Use URL, no command
+ string httpUrl = EditorPrefs.GetString("MCPForUnity.HttpUrl", "http://localhost:8080");
+ unity["url"] = httpUrl;
+
+ // Remove command/args if they exist from previous config
+ if (unity["command"] != null) unity.Remove("command");
+ if (unity["args"] != null) unity.Remove("args");
+
+ if (isVSCode)
{
- // Normalize to full path style
- if (directory.Contains(canonical))
- {
- var candidate = directory.Replace(canonical, symlinkSeg).Replace('\\', '/');
- if (System.IO.Directory.Exists(candidate))
- {
- effectiveDir = candidate;
- }
- }
- else
- {
- // If installer returned XDG-style on macOS, map to canonical symlink
- string norm = directory.Replace('\\', '/');
- int idx = norm.IndexOf("/.local/share/UnityMCP/", System.StringComparison.Ordinal);
- if (idx >= 0)
- {
- string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) ?? string.Empty;
- string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
- string candidate = System.IO.Path.Combine(home, "Library", "AppSupport", suffix).Replace('\\', '/');
- if (System.IO.Directory.Exists(candidate))
- {
- effectiveDir = candidate;
- }
- }
- }
+ unity["type"] = "http";
}
- catch { /* fallback to original directory on any error */ }
}
-#endif
-
- unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" });
-
- if (isVSCode)
+ else
{
- unity["type"] = "stdio";
+ // Stdio mode: Use uvx command
+ var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
+
+ unity["command"] = uvxPath;
+
+ var args = new List { packageName };
+ if (!string.IsNullOrEmpty(fromUrl))
+ {
+ args.Insert(0, fromUrl);
+ args.Insert(0, "--from");
+ }
+
+ args.Add("--transport");
+ args.Add("stdio");
+
+ unity["args"] = JArray.FromObject(args.ToArray());
+
+ // Remove url if it exists from previous config
+ if (unity["url"] != null) unity.Remove("url");
+
+ if (isVSCode)
+ {
+ unity["type"] = "stdio";
+ }
}
- else
+
+ // Remove type for non-VSCode clients
+ if (!isVSCode && unity["type"] != null)
{
- // Remove type if it somehow exists from previous clients
- if (unity["type"] != null) unity.Remove("type");
+ unity.Remove("type");
}
if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro))
diff --git a/MCPForUnity/Editor/Helpers/CustomToolRegistrationProcessor.cs b/MCPForUnity/Editor/Helpers/CustomToolRegistrationProcessor.cs
new file mode 100644
index 000000000..adced50f3
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/CustomToolRegistrationProcessor.cs
@@ -0,0 +1,184 @@
+using System.Threading.Tasks;
+using UnityEditor;
+using UnityEngine;
+using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Services;
+
+namespace MCPForUnity.Editor.Helpers
+{
+ ///
+ /// Handles automatic registration of custom tools with the MCP server
+ ///
+ public static class CustomToolRegistrationProcessor
+ {
+ private static bool _isRegistrationEnabled = true;
+ private static bool _hasRegisteredOnStartup = false;
+
+ static CustomToolRegistrationProcessor()
+ {
+ // Load saved preference
+ _isRegistrationEnabled = EditorPrefs.GetBool("MCPForUnity.CustomToolRegistrationEnabled", true);
+ }
+
+ ///
+ /// Enable or disable automatic tool registration
+ ///
+ public static bool IsRegistrationEnabled
+ {
+ get => _isRegistrationEnabled;
+ set
+ {
+ _isRegistrationEnabled = value;
+ EditorPrefs.SetBool("MCPForUnity.CustomToolRegistrationEnabled", value);
+ }
+ }
+
+ ///
+ /// Register all discovered tools with the MCP server
+ ///
+ public static async void RegisterAllTools()
+ {
+ if (!_isRegistrationEnabled)
+ {
+ McpLog.Info("Custom tool registration is disabled");
+ return;
+ }
+
+ try
+ {
+ McpLog.Info("Starting custom tool registration...");
+
+ var registrationService = MCPServiceLocator.CustomToolRegistration;
+ bool success = await registrationService.RegisterAllToolsAsync();
+
+ if (success)
+ {
+ McpLog.Info("Custom tool registration completed successfully");
+ }
+ else
+ {
+ McpLog.Warn("Custom tool registration failed - check server logs for details");
+ }
+ }
+ catch (System.Exception ex)
+ {
+ McpLog.Error($"Error during custom tool registration: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Register all tools synchronously
+ ///
+ public static void RegisterAllToolsSync()
+ {
+ if (!_isRegistrationEnabled)
+ {
+ McpLog.Info("Custom tool registration is disabled");
+ return;
+ }
+
+ try
+ {
+ McpLog.Info("Starting custom tool registration (sync)...");
+
+ var registrationService = MCPServiceLocator.CustomToolRegistration;
+ bool success = registrationService.RegisterAllTools();
+
+ if (success)
+ {
+ McpLog.Info("Custom tool registration completed successfully");
+ }
+ else
+ {
+ McpLog.Warn("Custom tool registration failed - check server logs for details");
+ }
+ }
+ catch (System.Exception ex)
+ {
+ McpLog.Error($"Error during custom tool registration: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Called when Unity editor starts up
+ ///
+ [InitializeOnLoadMethod]
+ private static void OnEditorStartup()
+ {
+ // Delay registration to allow editor to fully initialize
+ EditorApplication.delayCall += () =>
+ {
+ if (!_hasRegisteredOnStartup && _isRegistrationEnabled)
+ {
+ // Wait a bit more for MCP server to potentially start
+ EditorApplication.delayCall += () =>
+ {
+ RegisterAllTools();
+ _hasRegisteredOnStartup = true;
+ };
+ }
+ };
+ }
+
+ ///
+ /// Called when scripts are reloaded
+ ///
+ [UnityEditor.Callbacks.DidReloadScripts]
+ private static void OnScriptsReloaded()
+ {
+ // Invalidate discovery cache to pick up new tools
+ var discoveryService = MCPServiceLocator.ToolDiscovery;
+ discoveryService.InvalidateCache();
+
+ // Re-register tools after a delay
+ if (_isRegistrationEnabled)
+ {
+ EditorApplication.delayCall += RegisterAllTools;
+ }
+ }
+
+ ///
+ /// Force re-registration of all tools
+ ///
+ public static void ForceReregistration()
+ {
+ McpLog.Info("Force re-registering custom tools...");
+
+ // Invalidate cache
+ var discoveryService = MCPServiceLocator.ToolDiscovery;
+ discoveryService.InvalidateCache();
+
+ // Re-register
+ RegisterAllTools();
+ }
+
+ ///
+ /// Get information about discovered tools
+ ///
+ public static string GetToolInfo()
+ {
+ try
+ {
+ var discoveryService = MCPServiceLocator.ToolDiscovery;
+ var tools = discoveryService.DiscoverAllTools();
+
+ if (tools.Count == 0)
+ {
+ return "No custom tools discovered";
+ }
+
+ var info = $"Discovered {tools.Count} custom tools:\n";
+ foreach (var tool in tools)
+ {
+ info += $" - {tool.Name}: {tool.Description}\n";
+ }
+
+ return info;
+ }
+ catch (System.Exception ex)
+ {
+ return $"Error getting tool info: {ex.Message}";
+ }
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs.meta b/MCPForUnity/Editor/Helpers/CustomToolRegistrationProcessor.cs.meta
similarity index 83%
rename from MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs.meta
rename to MCPForUnity/Editor/Helpers/CustomToolRegistrationProcessor.cs.meta
index f1e14f70a..f9d9bad46 100644
--- a/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs.meta
+++ b/MCPForUnity/Editor/Helpers/CustomToolRegistrationProcessor.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: c40bd28f2310d463c8cd00181202cbe4
+guid: 80f36b8b3f86a45299c7d816c099c98b
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/MCPForUnity/Editor/Helpers/ExecPath.cs b/MCPForUnity/Editor/Helpers/ExecPath.cs
index 20c1200b9..924408fce 100644
--- a/MCPForUnity/Editor/Helpers/ExecPath.cs
+++ b/MCPForUnity/Editor/Helpers/ExecPath.cs
@@ -157,12 +157,6 @@ internal static void ClearClaudeCliPath()
catch { }
}
- // Use existing UV resolver; returns absolute path or null.
- internal static string ResolveUv()
- {
- return ServerInstaller.FindUvPath();
- }
-
internal static bool TryRun(
string file,
string args,
diff --git a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs
index 96ad7ec29..3037f70b9 100644
--- a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs
+++ b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs
@@ -10,6 +10,7 @@
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
+using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Helpers
{
@@ -25,7 +26,7 @@ public static class McpConfigurationHelper
/// Writes MCP configuration to the specified path using sophisticated logic
/// that preserves existing configuration and only writes when necessary
///
- public static string WriteMcpConfiguration(string pythonDir, string configPath, McpClient mcpClient = null)
+ public static string WriteMcpConfiguration(string configPath, McpClient mcpClient = null)
{
// 0) Respect explicit lock (hidden pref or UI toggle)
try
@@ -94,19 +95,8 @@ public static string WriteMcpConfiguration(string pythonDir, string configPath,
catch { }
// 1) Start from existing, only fill gaps (prefer trusted resolver)
- string uvPath = ServerInstaller.FindUvPath();
- // Optionally trust existingCommand if it looks like uv/uv.exe
- try
- {
- var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
- if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
- {
- uvPath = existingCommand;
- }
- }
- catch { }
- if (uvPath == null) return "UV package manager not found. Please install UV first.";
- string serverSrc = ResolveServerDirectory(pythonDir, existingArgs);
+ string uvxPath = MCPServiceLocator.Paths.GetUvxPath(verifyPath: true);
+ if (uvxPath == null) return "UV package manager not found. Please install UV first.";
// Ensure containers exist and write back configuration
JObject existingRoot;
@@ -115,7 +105,7 @@ public static string WriteMcpConfiguration(string pythonDir, string configPath,
else
existingRoot = JObject.FromObject(existingConfig);
- existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient);
+ existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvxPath, mcpClient);
string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
@@ -124,8 +114,7 @@ public static string WriteMcpConfiguration(string pythonDir, string configPath,
try
{
- if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
- EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
+ if (File.Exists(uvxPath)) EditorPrefs.SetString("MCPForUnity.UvxPath", uvxPath);
}
catch { }
@@ -135,7 +124,7 @@ public static string WriteMcpConfiguration(string pythonDir, string configPath,
///
/// Configures a Codex client with sophisticated TOML handling
///
- public static string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient)
+ public static string ConfigureCodexClient(string configPath, McpClient mcpClient)
{
try
{
@@ -165,66 +154,26 @@ public static string ConfigureCodexClient(string pythonDir, string configPath, M
CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs);
}
- string uvPath = ServerInstaller.FindUvPath();
- try
- {
- var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
- if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
- {
- uvPath = existingCommand;
- }
- }
- catch { }
-
- if (uvPath == null)
+ string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
+ if (uvxPath == null)
{
return "UV package manager not found. Please install UV first.";
}
- string serverSrc = ResolveServerDirectory(pythonDir, existingArgs);
-
- string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvPath, serverSrc);
+ string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvxPath);
EnsureConfigDirectoryExists(configPath);
WriteAtomicFile(configPath, updatedToml);
try
{
- if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
- EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
+ if (File.Exists(uvxPath)) EditorPrefs.SetString("MCPForUnity.UvxPath", uvxPath);
}
catch { }
return "Configured successfully";
}
- ///
- /// Validates UV binary by running --version command
- ///
- private static bool IsValidUvBinary(string path)
- {
- try
- {
- if (!File.Exists(path)) return false;
- var psi = new System.Diagnostics.ProcessStartInfo
- {
- FileName = path,
- Arguments = "--version",
- UseShellExecute = false,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- CreateNoWindow = true
- };
- using var p = System.Diagnostics.Process.Start(psi);
- if (p == null) return false;
- if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
- if (p.ExitCode != 0) return false;
- string output = p.StandardOutput.ReadToEnd().Trim();
- return output.StartsWith("uv ");
- }
- catch { return false; }
- }
-
///
/// Gets the appropriate config file path for the given MCP client based on OS
///
@@ -258,12 +207,12 @@ public static void EnsureConfigDirectoryExists(string configPath)
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
}
- public static string ExtractDirectoryArg(string[] args)
+ 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], "--directory", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(args[i], "--from", StringComparison.OrdinalIgnoreCase))
{
return args[i + 1];
}
@@ -290,58 +239,6 @@ public static bool PathsEqual(string a, string b)
}
}
- ///
- /// Resolves the server directory to use for MCP tools, preferring
- /// existing config values and falling back to installed/embedded copies.
- ///
- public static string ResolveServerDirectory(string pythonDir, string[] existingArgs)
- {
- string serverSrc = ExtractDirectoryArg(existingArgs);
- bool serverValid = !string.IsNullOrEmpty(serverSrc)
- && File.Exists(Path.Combine(serverSrc, "server.py"));
- if (!serverValid)
- {
- if (!string.IsNullOrEmpty(pythonDir)
- && File.Exists(Path.Combine(pythonDir, "server.py")))
- {
- serverSrc = pythonDir;
- }
- else
- {
- serverSrc = ResolveServerSource();
- }
- }
-
- try
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !string.IsNullOrEmpty(serverSrc))
- {
- string norm = serverSrc.Replace('\\', '/');
- int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal);
- if (idx >= 0)
- {
- string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
- string suffix = norm.Substring(idx + "/.local/share/".Length);
- serverSrc = Path.Combine(home, "Library", "Application Support", suffix);
- }
- }
- }
- catch
- {
- // Ignore failures and fall back to the original path.
- }
-
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
- && !string.IsNullOrEmpty(serverSrc)
- && serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
- && !EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
- {
- serverSrc = ServerInstaller.GetServerPath();
- }
-
- return serverSrc;
- }
-
public static void WriteAtomicFile(string path, string contents)
{
string tmp = path + ".tmp";
@@ -393,39 +290,5 @@ public static void WriteAtomicFile(string path, string contents)
try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { }
}
}
-
- public static string ResolveServerSource()
- {
- try
- {
- string remembered = EditorPrefs.GetString("MCPForUnity.ServerSrc", string.Empty);
- if (!string.IsNullOrEmpty(remembered)
- && File.Exists(Path.Combine(remembered, "server.py")))
- {
- return remembered;
- }
-
- ServerInstaller.EnsureServerInstalled();
- string installed = ServerInstaller.GetServerPath();
- if (File.Exists(Path.Combine(installed, "server.py")))
- {
- return installed;
- }
-
- bool useEmbedded = EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false);
- if (useEmbedded
- && ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)
- && File.Exists(Path.Combine(embedded, "server.py")))
- {
- return embedded;
- }
-
- return installed;
- }
- catch
- {
- return ServerInstaller.GetServerPath();
- }
- }
}
}
diff --git a/MCPForUnity/Editor/Helpers/McpPathResolver.cs b/MCPForUnity/Editor/Helpers/McpPathResolver.cs
deleted file mode 100644
index 04082a945..000000000
--- a/MCPForUnity/Editor/Helpers/McpPathResolver.cs
+++ /dev/null
@@ -1,123 +0,0 @@
-using System;
-using System.IO;
-using UnityEngine;
-using UnityEditor;
-using MCPForUnity.Editor.Helpers;
-
-namespace MCPForUnity.Editor.Helpers
-{
- ///
- /// Shared helper for resolving MCP server directory paths with support for
- /// development mode, embedded servers, and installed packages
- ///
- public static class McpPathResolver
- {
- private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer";
-
- ///
- /// Resolves the MCP server directory path with comprehensive logic
- /// including development mode support and fallback mechanisms
- ///
- public static string FindPackagePythonDirectory(bool debugLogsEnabled = false)
- {
- string pythonDir = McpConfigurationHelper.ResolveServerSource();
-
- try
- {
- // Only check dev paths if we're using a file-based package (development mode)
- bool isDevelopmentMode = IsDevelopmentMode();
- if (isDevelopmentMode)
- {
- string currentPackagePath = Path.GetDirectoryName(Application.dataPath);
- string[] devPaths = {
- Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"),
- Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"),
- };
-
- foreach (string devPath in devPaths)
- {
- if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py")))
- {
- if (debugLogsEnabled)
- {
- Debug.Log($"Currently in development mode. Package: {devPath}");
- }
- return devPath;
- }
- }
- }
-
- // Resolve via shared helper (handles local registry and older fallback) only if dev override on
- if (EditorPrefs.GetBool(USE_EMBEDDED_SERVER_KEY, false))
- {
- if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
- {
- return embedded;
- }
- }
-
- // Log only if the resolved path does not actually contain server.py
- if (debugLogsEnabled)
- {
- bool hasServer = false;
- try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { }
- if (!hasServer)
- {
- Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path");
- }
- }
- }
- catch (Exception e)
- {
- Debug.LogError($"Error finding package path: {e.Message}");
- }
-
- return pythonDir;
- }
-
- ///
- /// Checks if the current Unity project is in development mode
- /// (i.e., the package is referenced as a local file path in manifest.json)
- ///
- private static bool IsDevelopmentMode()
- {
- try
- {
- // Only treat as development if manifest explicitly references a local file path for the package
- string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json");
- if (!File.Exists(manifestPath)) return false;
-
- string manifestContent = File.ReadAllText(manifestPath);
- // Look specifically for our package dependency set to a file: URL
- // This avoids auto-enabling dev mode just because a repo exists elsewhere on disk
- if (manifestContent.IndexOf("\"com.coplaydev.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0)
- {
- int idx = manifestContent.IndexOf("com.coplaydev.unity-mcp", StringComparison.OrdinalIgnoreCase);
- // Crude but effective: check for "file:" in the same line/value
- if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0
- && manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
- return false;
- }
- catch
- {
- return false;
- }
- }
-
- ///
- /// Gets the appropriate PATH prepend for the current platform when running external processes
- ///
- public static string GetPathPrepend()
- {
- if (Application.platform == RuntimePlatform.OSXEditor)
- return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
- else if (Application.platform == RuntimePlatform.LinuxEditor)
- return "/usr/local/bin:/usr/bin:/bin";
- return null;
- }
- }
-}
diff --git a/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs b/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs
deleted file mode 100644
index 02e482c29..000000000
--- a/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs
+++ /dev/null
@@ -1,240 +0,0 @@
-using System.IO;
-using UnityEditor;
-using UnityEngine;
-
-namespace MCPForUnity.Editor.Helpers
-{
- ///
- /// Manages package lifecycle events including first-time installation,
- /// version updates, and legacy installation detection.
- /// Consolidates the functionality of PackageInstaller and PackageDetector.
- ///
- [InitializeOnLoad]
- public static class PackageLifecycleManager
- {
- private const string VersionKeyPrefix = "MCPForUnity.InstalledVersion:";
- private const string LegacyInstallFlagKey = "MCPForUnity.ServerInstalled"; // For migration
- private const string InstallErrorKeyPrefix = "MCPForUnity.InstallError:"; // Stores last installation error
-
- static PackageLifecycleManager()
- {
- // Schedule the check for after Unity is fully loaded
- EditorApplication.delayCall += CheckAndInstallServer;
- }
-
- private static void CheckAndInstallServer()
- {
- try
- {
- string currentVersion = GetPackageVersion();
- string versionKey = VersionKeyPrefix + currentVersion;
- bool hasRunForThisVersion = EditorPrefs.GetBool(versionKey, false);
-
- // Check for conditions that require installation/verification
- bool isFirstTimeInstall = !EditorPrefs.HasKey(LegacyInstallFlagKey) && !hasRunForThisVersion;
- bool legacyPresent = LegacyRootsExist();
- bool canonicalMissing = !File.Exists(
- Path.Combine(ServerInstaller.GetServerPath(), "server.py")
- );
-
- // Run if: first install, version update, legacy detected, or canonical missing
- if (isFirstTimeInstall || !hasRunForThisVersion || legacyPresent || canonicalMissing)
- {
- PerformInstallation(currentVersion, versionKey, isFirstTimeInstall);
- }
- }
- catch (System.Exception ex)
- {
- McpLog.Info($"Package lifecycle check failed: {ex.Message}. Open Window > MCP For Unity if needed.", always: false);
- }
- }
-
- private static void PerformInstallation(string version, string versionKey, bool isFirstTimeInstall)
- {
- string error = null;
-
- try
- {
- ServerInstaller.EnsureServerInstalled();
-
- // Mark as installed for this version
- EditorPrefs.SetBool(versionKey, true);
-
- // Migrate legacy flag if this is first time
- if (isFirstTimeInstall)
- {
- EditorPrefs.SetBool(LegacyInstallFlagKey, true);
- }
-
- // Clean up old version keys (keep only current version)
- CleanupOldVersionKeys(version);
-
- // Clean up legacy preference keys
- CleanupLegacyPrefs();
-
- // Only log success if server was actually embedded and copied
- if (ServerInstaller.HasEmbeddedServer() && isFirstTimeInstall)
- {
- McpLog.Info("MCP server installation completed successfully.");
- }
- }
- catch (System.Exception ex)
- {
- error = ex.Message;
-
- // Store the error for display in the UI, but don't mark as handled
- // This allows the user to manually rebuild via the "Rebuild Server" button
- string errorKey = InstallErrorKeyPrefix + version;
- EditorPrefs.SetString(errorKey, ex.Message ?? "Unknown error");
-
- // Don't mark as installed - user needs to manually rebuild
- }
-
- if (!string.IsNullOrEmpty(error))
- {
- McpLog.Info($"Server installation failed: {error}. Use Window > MCP For Unity > Rebuild Server to retry.", always: false);
- }
- }
-
- private static string GetPackageVersion()
- {
- try
- {
- var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(
- typeof(PackageLifecycleManager).Assembly
- );
- if (info != null && !string.IsNullOrEmpty(info.version))
- {
- return info.version;
- }
- }
- catch { }
-
- // Fallback to embedded server version
- return GetEmbeddedServerVersion();
- }
-
- private static string GetEmbeddedServerVersion()
- {
- try
- {
- if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
- {
- var versionPath = Path.Combine(embeddedSrc, "server_version.txt");
- if (File.Exists(versionPath))
- {
- return File.ReadAllText(versionPath)?.Trim() ?? "unknown";
- }
- }
- }
- catch { }
- return "unknown";
- }
-
- private static bool LegacyRootsExist()
- {
- try
- {
- string home = System.Environment.GetFolderPath(
- System.Environment.SpecialFolder.UserProfile
- ) ?? string.Empty;
-
- string[] legacyRoots =
- {
- Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
- Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
- };
-
- foreach (var root in legacyRoots)
- {
- try
- {
- if (File.Exists(Path.Combine(root, "server.py")))
- {
- return true;
- }
- }
- catch { }
- }
- }
- catch { }
- return false;
- }
-
- private static void CleanupOldVersionKeys(string currentVersion)
- {
- try
- {
- // Get all EditorPrefs keys that start with our version prefix
- // Note: Unity doesn't provide a way to enumerate all keys, so we can only
- // clean up known legacy keys. Future versions will be cleaned up when
- // a newer version runs.
- // This is a best-effort cleanup.
- }
- catch { }
- }
-
- private static void CleanupLegacyPrefs()
- {
- try
- {
- // Clean up old preference keys that are no longer used
- string[] legacyKeys =
- {
- "MCPForUnity.ServerSrc",
- "MCPForUnity.PythonDirOverride",
- "MCPForUnity.LegacyDetectLogged" // Old prefix without version
- };
-
- foreach (var key in legacyKeys)
- {
- try
- {
- if (EditorPrefs.HasKey(key))
- {
- EditorPrefs.DeleteKey(key);
- }
- }
- catch { }
- }
- }
- catch { }
- }
-
- ///
- /// Gets the last installation error for the current package version, if any.
- /// Returns null if there was no error or the error has been cleared.
- ///
- public static string GetLastInstallError()
- {
- try
- {
- string currentVersion = GetPackageVersion();
- string errorKey = InstallErrorKeyPrefix + currentVersion;
- if (EditorPrefs.HasKey(errorKey))
- {
- return EditorPrefs.GetString(errorKey, null);
- }
- }
- catch { }
- return null;
- }
-
- ///
- /// Clears the last installation error. Should be called after a successful manual rebuild.
- ///
- public static void ClearLastInstallError()
- {
- try
- {
- string currentVersion = GetPackageVersion();
- string errorKey = InstallErrorKeyPrefix + currentVersion;
- if (EditorPrefs.HasKey(errorKey))
- {
- EditorPrefs.DeleteKey(errorKey);
- }
- }
- catch { }
- }
- }
-}
diff --git a/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs b/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs
deleted file mode 100644
index de6167a77..000000000
--- a/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs
+++ /dev/null
@@ -1,188 +0,0 @@
-using System.IO;
-using System.Linq;
-using MCPForUnity.Editor.Data;
-using MCPForUnity.Editor.Services;
-using UnityEditor;
-using UnityEngine;
-
-namespace MCPForUnity.Editor.Helpers
-{
- ///
- /// Automatically syncs Python tools to the MCP server when:
- /// - PythonToolsAsset is modified
- /// - Python files are imported/reimported
- /// - Unity starts up
- ///
- [InitializeOnLoad]
- public class PythonToolSyncProcessor : AssetPostprocessor
- {
- private const string SyncEnabledKey = "MCPForUnity.AutoSyncEnabled";
- private static bool _isSyncing = false;
-
- static PythonToolSyncProcessor()
- {
- // Sync on Unity startup
- EditorApplication.delayCall += () =>
- {
- if (IsAutoSyncEnabled())
- {
- SyncAllTools();
- }
- };
- }
-
- ///
- /// Called after any assets are imported, deleted, or moved
- ///
- private static void OnPostprocessAllAssets(
- string[] importedAssets,
- string[] deletedAssets,
- string[] movedAssets,
- string[] movedFromAssetPaths)
- {
- // Prevent infinite loop - don't process if we're currently syncing
- if (_isSyncing || !IsAutoSyncEnabled())
- return;
-
- bool needsSync = false;
-
- // Only check for .py file changes, not PythonToolsAsset changes
- // (PythonToolsAsset changes are internal state updates from syncing)
- foreach (string path in importedAssets.Concat(movedAssets))
- {
- // Check if any .py files were modified
- if (path.EndsWith(".py"))
- {
- needsSync = true;
- break;
- }
- }
-
- // Check if any .py files were deleted
- if (!needsSync && deletedAssets.Any(path => path.EndsWith(".py")))
- {
- needsSync = true;
- }
-
- if (needsSync)
- {
- SyncAllTools();
- }
- }
-
- ///
- /// Syncs all Python tools from all PythonToolsAsset instances to the MCP server
- ///
- public static void SyncAllTools()
- {
- // Prevent re-entrant calls
- if (_isSyncing)
- {
- McpLog.Warn("Sync already in progress, skipping...");
- return;
- }
-
- _isSyncing = true;
- try
- {
- if (!ServerPathResolver.TryFindEmbeddedServerSource(out string srcPath))
- {
- McpLog.Warn("Cannot sync Python tools: MCP server source not found");
- return;
- }
-
- string toolsDir = Path.Combine(srcPath, "tools", "custom");
-
- var result = MCPServiceLocator.ToolSync.SyncProjectTools(toolsDir);
-
- if (result.Success)
- {
- if (result.CopiedCount > 0 || result.SkippedCount > 0)
- {
- McpLog.Info($"Python tools synced: {result.CopiedCount} copied, {result.SkippedCount} skipped");
- }
- }
- else
- {
- McpLog.Error($"Python tool sync failed with {result.ErrorCount} errors");
- foreach (var msg in result.Messages)
- {
- McpLog.Error($" - {msg}");
- }
- }
- }
- catch (System.Exception ex)
- {
- McpLog.Error($"Python tool sync exception: {ex.Message}");
- }
- finally
- {
- _isSyncing = false;
- }
- }
-
- ///
- /// Checks if auto-sync is enabled (default: true)
- ///
- public static bool IsAutoSyncEnabled()
- {
- return EditorPrefs.GetBool(SyncEnabledKey, true);
- }
-
- ///
- /// Enables or disables auto-sync
- ///
- public static void SetAutoSyncEnabled(bool enabled)
- {
- EditorPrefs.SetBool(SyncEnabledKey, enabled);
- McpLog.Info($"Python tool auto-sync {(enabled ? "enabled" : "disabled")}");
- }
-
- ///
- /// Reimport all Python files in the project
- ///
- public static void ReimportPythonFiles()
- {
- // Find all Python files (imported as TextAssets by PythonFileImporter)
- var pythonGuids = AssetDatabase.FindAssets("t:TextAsset", new[] { "Assets" })
- .Select(AssetDatabase.GUIDToAssetPath)
- .Where(path => path.EndsWith(".py", System.StringComparison.OrdinalIgnoreCase))
- .ToArray();
-
- foreach (string path in pythonGuids)
- {
- AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
- }
-
- int count = pythonGuids.Length;
- McpLog.Info($"Reimported {count} Python files");
- AssetDatabase.Refresh();
- }
-
- ///
- /// Manually trigger sync
- ///
- public static void ManualSync()
- {
- McpLog.Info("Manually syncing Python tools...");
- SyncAllTools();
- }
-
- ///
- /// Toggle auto-sync
- ///
- public static void ToggleAutoSync()
- {
- SetAutoSyncEnabled(!IsAutoSyncEnabled());
- }
-
- ///
- /// Validate menu item (shows checkmark when enabled)
- ///
- public static bool ToggleAutoSyncValidate()
- {
- Menu.SetChecked("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", IsAutoSyncEnabled());
- return true;
- }
- }
-}
diff --git a/MCPForUnity/Editor/Helpers/ServerInstaller.cs b/MCPForUnity/Editor/Helpers/ServerInstaller.cs
deleted file mode 100644
index 2e149bb44..000000000
--- a/MCPForUnity/Editor/Helpers/ServerInstaller.cs
+++ /dev/null
@@ -1,1001 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
-using System.Net;
-using System.Runtime.InteropServices;
-using UnityEditor;
-using UnityEngine;
-
-namespace MCPForUnity.Editor.Helpers
-{
- public static class ServerInstaller
- {
- private const string RootFolder = "UnityMCP";
- private const string ServerFolder = "UnityMcpServer";
- private const string VersionFileName = "server_version.txt";
-
- ///
- /// Ensures the mcp-for-unity-server is installed locally by copying from the embedded package source.
- /// No network calls or Git operations are performed.
- ///
- public static void EnsureServerInstalled()
- {
- try
- {
- string saveLocation = GetSaveLocation();
- TryCreateMacSymlinkForAppSupport();
- string destRoot = Path.Combine(saveLocation, ServerFolder);
- string destSrc = Path.Combine(destRoot, "src");
-
- // Detect legacy installs and version state (logs)
- DetectAndLogLegacyInstallStates(destRoot);
-
- // Resolve embedded source and versions
- if (!TryGetEmbeddedServerSource(out string embeddedSrc))
- {
- // Asset Store install - no embedded server
- // Check if server was already downloaded
- if (File.Exists(Path.Combine(destSrc, "server.py")))
- {
- McpLog.Info("Using previously downloaded MCP server.", always: false);
- }
- else
- {
- McpLog.Info("MCP server not found. Download via Window > MCP For Unity > Open MCP Window.", always: false);
- }
- return; // Graceful exit - no exception
- }
-
- string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
- string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName));
-
- bool destHasServer = File.Exists(Path.Combine(destSrc, "server.py"));
- bool needOverwrite = !destHasServer
- || string.IsNullOrEmpty(installedVer)
- || (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0);
-
- // Ensure destination exists
- Directory.CreateDirectory(destRoot);
-
- if (needOverwrite)
- {
- // Copy the entire UnityMcpServer folder (parent of src)
- string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
- CopyDirectoryRecursive(embeddedRoot, destRoot);
-
- // Write/refresh version file
- try { File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer ?? "unknown"); } catch { }
- McpLog.Info($"Installed/updated server to {destRoot} (version {embeddedVer}).");
- }
-
- // Cleanup legacy installs that are missing version or older than embedded
- foreach (var legacyRoot in GetLegacyRootsForDetection())
- {
- try
- {
- string legacySrc = Path.Combine(legacyRoot, "src");
- if (!File.Exists(Path.Combine(legacySrc, "server.py"))) continue;
- string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName));
- bool legacyOlder = string.IsNullOrEmpty(legacyVer)
- || (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0);
- if (legacyOlder)
- {
- TryKillUvForPath(legacySrc);
- if (DeleteDirectoryWithRetry(legacyRoot))
- {
- McpLog.Info($"Removed legacy server at '{legacyRoot}'.");
- }
- else
- {
- McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}' (files may be in use)");
- }
- }
- }
- catch { }
- }
-
- // Clear overrides that might point at legacy locations
- try
- {
- EditorPrefs.DeleteKey("MCPForUnity.ServerSrc");
- EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride");
- }
- catch { }
- return;
- }
- catch (Exception ex)
- {
- // If a usable server is already present (installed or embedded), don't fail hard—just warn.
- bool hasInstalled = false;
- try { hasInstalled = File.Exists(Path.Combine(GetServerPath(), "server.py")); } catch { }
-
- if (hasInstalled || TryGetEmbeddedServerSource(out _))
- {
- McpLog.Warn($"Using existing server; skipped install. Details: {ex.Message}");
- return;
- }
-
- McpLog.Error($"Failed to ensure server installation: {ex.Message}");
- }
- }
-
- public static string GetServerPath()
- {
- return Path.Combine(GetSaveLocation(), ServerFolder, "src");
- }
-
- ///
- /// Gets the platform-specific save location for the server.
- ///
- private static string GetSaveLocation()
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- // Use per-user LocalApplicationData for canonical install location
- var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
- ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local");
- return Path.Combine(localAppData, RootFolder);
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
- {
- var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
- if (string.IsNullOrEmpty(xdg))
- {
- xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty,
- ".local", "share");
- }
- return Path.Combine(xdg, RootFolder);
- }
- else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
- {
- // On macOS, use LocalApplicationData (~/Library/Application Support)
- var localAppSupport = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
- // Unity/Mono may map LocalApplicationData to ~/.local/share on macOS; normalize to Application Support
- bool looksLikeXdg = !string.IsNullOrEmpty(localAppSupport) && localAppSupport.Replace('\\', '/').Contains("/.local/share");
- if (string.IsNullOrEmpty(localAppSupport) || looksLikeXdg)
- {
- // Fallback: construct from $HOME
- var home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
- localAppSupport = Path.Combine(home, "Library", "Application Support");
- }
- TryCreateMacSymlinkForAppSupport();
- return Path.Combine(localAppSupport, RootFolder);
- }
- throw new Exception("Unsupported operating system");
- }
-
- ///
- /// On macOS, create a no-spaces symlink ~/Library/AppSupport -> ~/Library/Application Support
- /// to mitigate arg parsing and quoting issues in some MCP clients.
- /// Safe to call repeatedly.
- ///
- private static void TryCreateMacSymlinkForAppSupport()
- {
- try
- {
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return;
- string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
- if (string.IsNullOrEmpty(home)) return;
-
- string canonical = Path.Combine(home, "Library", "Application Support");
- string symlink = Path.Combine(home, "Library", "AppSupport");
-
- // If symlink exists already, nothing to do
- if (Directory.Exists(symlink) || File.Exists(symlink)) return;
-
- // Create symlink only if canonical exists
- if (!Directory.Exists(canonical)) return;
-
- // Use 'ln -s' to create a directory symlink (macOS)
- var psi = new ProcessStartInfo
- {
- FileName = "/bin/ln",
- Arguments = $"-s \"{canonical}\" \"{symlink}\"",
- UseShellExecute = false,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- CreateNoWindow = true
- };
- using var p = Process.Start(psi);
- p?.WaitForExit(2000);
- }
- catch { /* best-effort */ }
- }
-
- private static bool IsDirectoryWritable(string path)
- {
- try
- {
- File.Create(Path.Combine(path, "test.txt")).Dispose();
- File.Delete(Path.Combine(path, "test.txt"));
- return true;
- }
- catch
- {
- return false;
- }
- }
-
- ///
- /// Checks if the server is installed at the specified location.
- ///
- private static bool IsServerInstalled(string location)
- {
- return Directory.Exists(location)
- && File.Exists(Path.Combine(location, ServerFolder, "src", "server.py"));
- }
-
- ///
- /// Detects legacy installs or older versions and logs findings (no deletion yet).
- ///
- private static void DetectAndLogLegacyInstallStates(string canonicalRoot)
- {
- try
- {
- string canonicalSrc = Path.Combine(canonicalRoot, "src");
- // Normalize canonical root for comparisons
- string normCanonicalRoot = NormalizePathSafe(canonicalRoot);
- string embeddedSrc = null;
- TryGetEmbeddedServerSource(out embeddedSrc);
-
- string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc ?? string.Empty, VersionFileName));
- string installedVer = ReadVersionFile(Path.Combine(canonicalSrc, VersionFileName));
-
- // Legacy paths (macOS/Linux .config; Windows roaming as example)
- foreach (var legacyRoot in GetLegacyRootsForDetection())
- {
- // Skip logging for the canonical root itself
- if (PathsEqualSafe(legacyRoot, normCanonicalRoot))
- continue;
- string legacySrc = Path.Combine(legacyRoot, "src");
- bool hasServer = File.Exists(Path.Combine(legacySrc, "server.py"));
- string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName));
-
- if (hasServer)
- {
- // Case 1: No version file
- if (string.IsNullOrEmpty(legacyVer))
- {
- McpLog.Info("Detected legacy install without version file at: " + legacyRoot, always: false);
- }
-
- // Case 2: Lives in legacy path
- McpLog.Info("Detected legacy install path: " + legacyRoot, always: false);
-
- // Case 3: Has version but appears older than embedded
- if (!string.IsNullOrEmpty(embeddedVer) && !string.IsNullOrEmpty(legacyVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0)
- {
- McpLog.Info($"Legacy install version {legacyVer} is older than embedded {embeddedVer}", always: false);
- }
- }
- }
-
- // Also log if canonical is missing version (treated as older)
- if (Directory.Exists(canonicalRoot))
- {
- if (string.IsNullOrEmpty(installedVer))
- {
- McpLog.Info("Canonical install missing version file (treat as older). Path: " + canonicalRoot, always: false);
- }
- else if (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0)
- {
- McpLog.Info($"Canonical install version {installedVer} is older than embedded {embeddedVer}", always: false);
- }
- }
- }
- catch (Exception ex)
- {
- McpLog.Warn("Detect legacy/version state failed: " + ex.Message);
- }
- }
-
- private static string NormalizePathSafe(string path)
- {
- try { return string.IsNullOrEmpty(path) ? path : Path.GetFullPath(path.Trim()); }
- catch { return path; }
- }
-
- private static bool PathsEqualSafe(string a, string b)
- {
- if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
- string na = NormalizePathSafe(a);
- string nb = NormalizePathSafe(b);
- try
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
- }
- return string.Equals(na, nb, StringComparison.Ordinal);
- }
- catch { return false; }
- }
-
- private static IEnumerable GetLegacyRootsForDetection()
- {
- var roots = new List();
- string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
- // macOS/Linux legacy
- roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer"));
- roots.Add(Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer"));
- // Windows roaming example
- try
- {
- string roaming = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
- if (!string.IsNullOrEmpty(roaming))
- roots.Add(Path.Combine(roaming, "UnityMCP", "UnityMcpServer"));
- // Windows legacy: early installers/dev scripts used %LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer
- // Detect this location so we can clean up older copies during install/update.
- string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
- if (!string.IsNullOrEmpty(localAppData))
- roots.Add(Path.Combine(localAppData, "Programs", "UnityMCP", "UnityMcpServer"));
- }
- catch { }
- return roots;
- }
-
- ///
- /// Attempts to kill UV and Python processes associated with a specific server path.
- /// This is necessary on Windows because the OS blocks file deletion when processes
- /// have open file handles, unlike macOS/Linux which allow unlinking open files.
- ///
- private static void TryKillUvForPath(string serverSrcPath)
- {
- try
- {
- if (string.IsNullOrEmpty(serverSrcPath)) return;
-
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- KillWindowsUvProcesses(serverSrcPath);
- return;
- }
-
- // Unix: use pgrep to find processes by command line
- var psi = new ProcessStartInfo
- {
- FileName = "/usr/bin/pgrep",
- Arguments = $"-f \"uv .*--directory {serverSrcPath}\"",
- UseShellExecute = false,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- CreateNoWindow = true
- };
- using var p = Process.Start(psi);
- if (p == null) return;
- string outp = p.StandardOutput.ReadToEnd();
- p.WaitForExit(1500);
- if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
- {
- foreach (var line in outp.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
- {
- if (int.TryParse(line.Trim(), out int pid))
- {
- try { Process.GetProcessById(pid).Kill(); } catch { }
- }
- }
- }
- }
- catch { }
- }
-
- ///
- /// Kills Windows processes running from the virtual environment directory.
- /// Uses WMIC (Windows Management Instrumentation) to safely query only processes
- /// with executables in the .venv path, avoiding the need to iterate all system processes.
- /// This prevents accidentally killing IDE processes or other critical system processes.
- ///
- /// Why this is needed on Windows:
- /// - Windows blocks file/directory deletion when ANY process has an open file handle
- /// - UV creates a virtual environment with python.exe and other executables
- /// - These processes may hold locks on DLLs, .pyd files, or the executables themselves
- /// - macOS/Linux allow deletion of open files (unlink), but Windows does not
- ///
- private static void KillWindowsUvProcesses(string serverSrcPath)
- {
- try
- {
- if (string.IsNullOrEmpty(serverSrcPath)) return;
-
- string venvPath = Path.Combine(serverSrcPath, ".venv");
- if (!Directory.Exists(venvPath)) return;
-
- string normalizedVenvPath = Path.GetFullPath(venvPath).ToLowerInvariant();
-
- // Use WMIC to find processes with executables in the .venv directory
- // This is much safer than iterating all processes
- var psi = new ProcessStartInfo
- {
- FileName = "wmic",
- Arguments = $"process where \"ExecutablePath like '%{normalizedVenvPath.Replace("\\", "\\\\")}%'\" get ProcessId",
- UseShellExecute = false,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- CreateNoWindow = true
- };
-
- using var proc = Process.Start(psi);
- if (proc == null) return;
-
- string output = proc.StandardOutput.ReadToEnd();
- proc.WaitForExit(5000);
-
- if (proc.ExitCode != 0) return;
-
- // Parse PIDs from WMIC output
- var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
- foreach (var line in lines)
- {
- string trimmed = line.Trim();
- if (trimmed.Equals("ProcessId", StringComparison.OrdinalIgnoreCase)) continue;
- if (string.IsNullOrWhiteSpace(trimmed)) continue;
-
- if (int.TryParse(trimmed, out int pid))
- {
- try
- {
- using var p = Process.GetProcessById(pid);
- // Double-check it's not a critical process
- string name = p.ProcessName.ToLowerInvariant();
- if (name == "unity" || name == "code" || name == "devenv" || name == "rider64")
- {
- continue; // Skip IDE processes
- }
- p.Kill();
- p.WaitForExit(2000);
- }
- catch { }
- }
- }
-
- // Give processes time to fully exit
- System.Threading.Thread.Sleep(500);
- }
- catch { }
- }
-
- ///
- /// Attempts to delete a directory with retry logic to handle Windows file locking issues.
- ///
- /// Why retries are necessary on Windows:
- /// - Even after killing processes, Windows may take time to release file handles
- /// - Antivirus, Windows Defender, or indexing services may temporarily lock files
- /// - File Explorer previews can hold locks on certain file types
- /// - Readonly attributes on files (common in .venv) block deletion
- ///
- /// This method handles these cases by:
- /// - Retrying deletion after a delay to allow handle release
- /// - Clearing readonly attributes that block deletion
- /// - Distinguishing between temporary locks (retry) and permanent failures
- ///
- private static bool DeleteDirectoryWithRetry(string path, int maxRetries = 3, int delayMs = 500)
- {
- for (int i = 0; i < maxRetries; i++)
- {
- try
- {
- if (!Directory.Exists(path)) return true;
-
- Directory.Delete(path, recursive: true);
- return true;
- }
- catch (UnauthorizedAccessException)
- {
- if (i < maxRetries - 1)
- {
- // Wait for file handles to be released
- System.Threading.Thread.Sleep(delayMs);
-
- // Try to clear readonly attributes
- try
- {
- foreach (var file in Directory.GetFiles(path, "*", SearchOption.AllDirectories))
- {
- try
- {
- var attrs = File.GetAttributes(file);
- if ((attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
- {
- File.SetAttributes(file, attrs & ~FileAttributes.ReadOnly);
- }
- }
- catch { }
- }
- }
- catch { }
- }
- }
- catch (IOException)
- {
- if (i < maxRetries - 1)
- {
- // File in use, wait and retry
- System.Threading.Thread.Sleep(delayMs);
- }
- }
- catch
- {
- return false;
- }
- }
- return false;
- }
-
- private static string ReadVersionFile(string path)
- {
- try
- {
- if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null;
- string v = File.ReadAllText(path).Trim();
- return string.IsNullOrEmpty(v) ? null : v;
- }
- catch { return null; }
- }
-
- private static int CompareSemverSafe(string a, string b)
- {
- try
- {
- if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return 0;
- var ap = a.Split('.');
- var bp = b.Split('.');
- for (int i = 0; i < Math.Max(ap.Length, bp.Length); i++)
- {
- int ai = (i < ap.Length && int.TryParse(ap[i], out var t1)) ? t1 : 0;
- int bi = (i < bp.Length && int.TryParse(bp[i], out var t2)) ? t2 : 0;
- if (ai != bi) return ai.CompareTo(bi);
- }
- return 0;
- }
- catch { return 0; }
- }
-
- ///
- /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
- /// or common development locations.
- ///
- private static bool TryGetEmbeddedServerSource(out string srcPath)
- {
- return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath);
- }
-
- private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" };
-
- private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
- {
- Directory.CreateDirectory(destinationDir);
-
- foreach (string filePath in Directory.GetFiles(sourceDir))
- {
- string fileName = Path.GetFileName(filePath);
- string destFile = Path.Combine(destinationDir, fileName);
- File.Copy(filePath, destFile, overwrite: true);
- }
-
- foreach (string dirPath in Directory.GetDirectories(sourceDir))
- {
- string dirName = Path.GetFileName(dirPath);
- foreach (var skip in _skipDirs)
- {
- if (dirName.Equals(skip, StringComparison.OrdinalIgnoreCase))
- goto NextDir;
- }
- try { if ((File.GetAttributes(dirPath) & FileAttributes.ReparsePoint) != 0) continue; } catch { }
- string destSubDir = Path.Combine(destinationDir, dirName);
- CopyDirectoryRecursive(dirPath, destSubDir);
- NextDir:;
- }
- }
-
- public static bool RebuildMcpServer()
- {
- try
- {
- // Find embedded source
- if (!TryGetEmbeddedServerSource(out string embeddedSrc))
- {
- McpLog.Error("RebuildMcpServer: Could not find embedded server source.");
- return false;
- }
-
- string saveLocation = GetSaveLocation();
- string destRoot = Path.Combine(saveLocation, ServerFolder);
- string destSrc = Path.Combine(destRoot, "src");
-
- // Kill any running uv processes for this server
- TryKillUvForPath(destSrc);
-
- // Delete the entire installed server directory
- if (Directory.Exists(destRoot))
- {
- if (!DeleteDirectoryWithRetry(destRoot, maxRetries: 5, delayMs: 1000))
- {
- McpLog.Error($"Failed to delete existing server at {destRoot}. Please close any applications using the Python virtual environment and try again.");
- return false;
- }
- McpLog.Info($"Deleted existing server at {destRoot}");
- }
-
- // Re-copy from embedded source
- string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc;
- Directory.CreateDirectory(destRoot);
- CopyDirectoryRecursive(embeddedRoot, destRoot);
-
- // Write version file
- string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
- try
- {
- File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer);
- }
- catch (Exception ex)
- {
- McpLog.Warn($"Failed to write version file: {ex.Message}");
- }
-
- McpLog.Info($"Server rebuilt successfully at {destRoot} (version {embeddedVer})");
-
- // Clear any previous installation error
-
- PackageLifecycleManager.ClearLastInstallError();
-
-
- return true;
- }
- catch (Exception ex)
- {
- McpLog.Error($"RebuildMcpServer failed: {ex.Message}");
- return false;
- }
- }
-
- internal static string FindUvPath()
- {
- // Allow user override via EditorPrefs
- try
- {
- string overridePath = EditorPrefs.GetString("MCPForUnity.UvPath", string.Empty);
- if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
- {
- if (ValidateUvBinary(overridePath)) return overridePath;
- }
- }
- catch { }
-
- string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
-
- // Platform-specific candidate lists
- string[] candidates;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
- string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty;
- string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
-
- // Fast path: resolve from PATH first
- try
- {
- var wherePsi = new ProcessStartInfo
- {
- FileName = "where",
- Arguments = "uv.exe",
- UseShellExecute = false,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- CreateNoWindow = true
- };
- using var wp = Process.Start(wherePsi);
- string output = wp.StandardOutput.ReadToEnd().Trim();
- wp.WaitForExit(1500);
- if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
- {
- foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
- {
- string path = line.Trim();
- if (File.Exists(path) && ValidateUvBinary(path)) return path;
- }
- }
- }
- catch { }
-
- // Windows Store (PythonSoftwareFoundation) install location probe
- // Example: %LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.13_*\LocalCache\local-packages\Python313\Scripts\uv.exe
- try
- {
- string pkgsRoot = Path.Combine(localAppData, "Packages");
- if (Directory.Exists(pkgsRoot))
- {
- var pythonPkgs = Directory.GetDirectories(pkgsRoot, "PythonSoftwareFoundation.Python.*", SearchOption.TopDirectoryOnly)
- .OrderByDescending(p => p, StringComparer.OrdinalIgnoreCase);
- foreach (var pkg in pythonPkgs)
- {
- string localCache = Path.Combine(pkg, "LocalCache", "local-packages");
- if (!Directory.Exists(localCache)) continue;
- var pyRoots = Directory.GetDirectories(localCache, "Python*", SearchOption.TopDirectoryOnly)
- .OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase);
- foreach (var pyRoot in pyRoots)
- {
- string uvExe = Path.Combine(pyRoot, "Scripts", "uv.exe");
- if (File.Exists(uvExe) && ValidateUvBinary(uvExe)) return uvExe;
- }
- }
- }
- }
- catch { }
-
- candidates = new[]
- {
- // Preferred: WinGet Links shims (stable entrypoints)
- // Per-user shim (LOCALAPPDATA) → machine-wide shim (Program Files\WinGet\Links)
- Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"),
- Path.Combine(programFiles, "WinGet", "Links", "uv.exe"),
-
- // Common per-user installs
- Path.Combine(localAppData, @"Programs\Python\Python314\Scripts\uv.exe"),
- Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"),
- Path.Combine(localAppData, @"Programs\Python\Python312\Scripts\uv.exe"),
- Path.Combine(localAppData, @"Programs\Python\Python311\Scripts\uv.exe"),
- Path.Combine(localAppData, @"Programs\Python\Python310\Scripts\uv.exe"),
- Path.Combine(appData, @"Python\Python314\Scripts\uv.exe"),
- Path.Combine(appData, @"Python\Python313\Scripts\uv.exe"),
- Path.Combine(appData, @"Python\Python312\Scripts\uv.exe"),
- Path.Combine(appData, @"Python\Python311\Scripts\uv.exe"),
- Path.Combine(appData, @"Python\Python310\Scripts\uv.exe"),
-
- // Program Files style installs (if a native installer was used)
- Path.Combine(programFiles, @"uv\uv.exe"),
-
- // Try simple name resolution later via PATH
- "uv.exe",
- "uv"
- };
- }
- else
- {
- candidates = new[]
- {
- "/opt/homebrew/bin/uv",
- "/usr/local/bin/uv",
- "/usr/bin/uv",
- "/opt/local/bin/uv",
- Path.Combine(home, ".local", "bin", "uv"),
- "/opt/homebrew/opt/uv/bin/uv",
- // Framework Python installs
- "/Library/Frameworks/Python.framework/Versions/3.14/bin/uv",
- "/Library/Frameworks/Python.framework/Versions/3.13/bin/uv",
- "/Library/Frameworks/Python.framework/Versions/3.12/bin/uv",
- "/Library/Frameworks/Python.framework/Versions/3.11/bin/uv",
- "/Library/Frameworks/Python.framework/Versions/3.10/bin/uv",
- // Fallback to PATH resolution by name
- "uv"
- };
- }
-
- foreach (string c in candidates)
- {
- try
- {
- if (File.Exists(c) && ValidateUvBinary(c)) return c;
- }
- catch { /* ignore */ }
- }
-
- // Use platform-appropriate which/where to resolve from PATH (non-Windows handled here; Windows tried earlier)
- try
- {
- if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- var whichPsi = new ProcessStartInfo
- {
- FileName = "/usr/bin/which",
- Arguments = "uv",
- UseShellExecute = false,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- CreateNoWindow = true
- };
- try
- {
- // Prepend common user-local and package manager locations so 'which' can see them in Unity's GUI env
- string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
- string prepend = string.Join(":", new[]
- {
- Path.Combine(homeDir, ".local", "bin"),
- "/opt/homebrew/bin",
- "/usr/local/bin",
- "/usr/bin",
- "/bin"
- });
- string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
- whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath);
- }
- catch { }
- using var wp = Process.Start(whichPsi);
- string output = wp.StandardOutput.ReadToEnd().Trim();
- wp.WaitForExit(3000);
- if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
- {
- if (ValidateUvBinary(output)) return output;
- }
- }
- }
- catch { }
-
- // Manual PATH scan
- try
- {
- string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
- string[] parts = pathEnv.Split(Path.PathSeparator);
- foreach (string part in parts)
- {
- try
- {
- // Check both uv and uv.exe
- string candidateUv = Path.Combine(part, "uv");
- string candidateUvExe = Path.Combine(part, "uv.exe");
- if (File.Exists(candidateUv) && ValidateUvBinary(candidateUv)) return candidateUv;
- if (File.Exists(candidateUvExe) && ValidateUvBinary(candidateUvExe)) return candidateUvExe;
- }
- catch { }
- }
- }
- catch { }
-
- return null;
- }
-
- private static bool ValidateUvBinary(string uvPath)
- {
- try
- {
- var psi = new ProcessStartInfo
- {
- FileName = uvPath,
- Arguments = "--version",
- UseShellExecute = false,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- CreateNoWindow = true
- };
- using var p = Process.Start(psi);
- if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; }
- if (p.ExitCode == 0)
- {
- string output = p.StandardOutput.ReadToEnd().Trim();
- return output.StartsWith("uv ");
- }
- }
- catch { }
- return false;
- }
-
- ///
- /// Download and install server from GitHub release (Asset Store workflow)
- ///
- public static bool DownloadAndInstallServer()
- {
- string packageVersion = AssetPathUtility.GetPackageVersion();
- if (packageVersion == "unknown")
- {
- McpLog.Error("Cannot determine package version for download.");
- return false;
- }
-
- string downloadUrl = $"https://github.com/CoplayDev/unity-mcp/releases/download/v{packageVersion}/mcp-for-unity-server-v{packageVersion}.zip";
- string tempZip = Path.Combine(Path.GetTempPath(), $"mcp-server-v{packageVersion}.zip");
- string destRoot = Path.Combine(GetSaveLocation(), ServerFolder);
-
- try
- {
- EditorUtility.DisplayProgressBar("MCP for Unity", "Downloading server...", 0.3f);
-
- // Download
- using (var client = new WebClient())
- {
- client.DownloadFile(downloadUrl, tempZip);
- }
-
- EditorUtility.DisplayProgressBar("MCP for Unity", "Extracting server...", 0.7f);
-
- // Kill any running UV processes
- string destSrc = Path.Combine(destRoot, "src");
- TryKillUvForPath(destSrc);
-
- // Delete old installation
- if (Directory.Exists(destRoot))
- {
- if (!DeleteDirectoryWithRetry(destRoot, maxRetries: 5, delayMs: 1000))
- {
- McpLog.Warn($"Could not fully delete old server (files may be in use)");
- }
- }
-
- // Extract to temp location first
- string tempExtractDir = Path.Combine(Path.GetTempPath(), $"mcp-server-extract-{Guid.NewGuid()}");
- Directory.CreateDirectory(tempExtractDir);
-
- try
- {
- ZipFile.ExtractToDirectory(tempZip, tempExtractDir);
-
- // The ZIP contains UnityMcpServer~ folder, find it and move its contents
- string extractedServerFolder = Path.Combine(tempExtractDir, "UnityMcpServer~");
- Directory.CreateDirectory(destRoot);
- CopyDirectoryRecursive(extractedServerFolder, destRoot);
- }
- finally
- {
- // Cleanup temp extraction directory
- try
- {
- if (Directory.Exists(tempExtractDir))
- {
- Directory.Delete(tempExtractDir, recursive: true);
- }
- }
- catch (Exception ex)
- {
- McpLog.Warn($"Could not fully delete temp extraction directory: {ex.Message}");
- }
- }
-
- EditorUtility.ClearProgressBar();
- McpLog.Info($"Server v{packageVersion} downloaded and installed successfully!");
- return true;
- }
- catch (Exception ex)
- {
- EditorUtility.ClearProgressBar();
- McpLog.Error($"Failed to download server: {ex.Message}");
- EditorUtility.DisplayDialog(
- "Download Failed",
- $"Could not download server from GitHub.\n\n{ex.Message}\n\nPlease check your internet connection or try again later.",
- "OK"
- );
- return false;
- }
- finally
- {
- try
- {
- if (File.Exists(tempZip)) File.Delete(tempZip);
- }
- catch (Exception ex)
- {
- McpLog.Warn($"Could not delete temp zip file: {ex.Message}");
- }
- }
- }
-
- ///
- /// Check if the package has an embedded server (Git install vs Asset Store)
- ///
- public static bool HasEmbeddedServer()
- {
- return TryGetEmbeddedServerSource(out _);
- }
-
- ///
- /// Get the installed server version from the local installation
- ///
- public static string GetInstalledServerVersion()
- {
- try
- {
- string destRoot = Path.Combine(GetSaveLocation(), ServerFolder);
- string versionPath = Path.Combine(destRoot, "src", VersionFileName);
- if (File.Exists(versionPath))
- {
- return File.ReadAllText(versionPath)?.Trim() ?? string.Empty;
- }
- }
- catch (Exception ex)
- {
- McpLog.Warn($"Could not read version file: {ex.Message}");
- }
- return string.Empty;
- }
- }
-}
diff --git a/MCPForUnity/Editor/Helpers/ServerInstaller.cs.meta b/MCPForUnity/Editor/Helpers/ServerInstaller.cs.meta
deleted file mode 100644
index dfd9023b7..000000000
--- a/MCPForUnity/Editor/Helpers/ServerInstaller.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: 5862c6a6d0a914f4d83224f8d039cf7b
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/MCPForUnity/Editor/Helpers/ServerPathResolver.cs b/MCPForUnity/Editor/Helpers/ServerPathResolver.cs
deleted file mode 100644
index 0e4629458..000000000
--- a/MCPForUnity/Editor/Helpers/ServerPathResolver.cs
+++ /dev/null
@@ -1,141 +0,0 @@
-using System;
-using System.IO;
-using UnityEditor;
-using UnityEngine;
-
-namespace MCPForUnity.Editor.Helpers
-{
- public static class ServerPathResolver
- {
- ///
- /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
- /// or common development locations. Returns true if found and sets srcPath to the folder
- /// containing server.py.
- ///
- public static bool TryFindEmbeddedServerSource(out string srcPath)
- {
- // 1) Repo development layouts commonly used alongside this package
- try
- {
- string projectRoot = Path.GetDirectoryName(Application.dataPath);
- string[] devCandidates =
- {
- Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
- Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
- };
- foreach (string candidate in devCandidates)
- {
- string full = Path.GetFullPath(candidate);
- if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
- {
- srcPath = full;
- return true;
- }
- }
- }
- catch { /* ignore */ }
-
- // 2) Resolve via local package info (no network). Fall back to Client.List on older editors.
- try
- {
-#if UNITY_2021_2_OR_NEWER
- // Primary: the package that owns this assembly
- var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly);
- if (owner != null)
- {
- if (TryResolveWithinPackage(owner, out srcPath))
- {
- return true;
- }
- }
-
- // Secondary: scan all registered packages locally
- foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
- {
- if (TryResolveWithinPackage(p, out srcPath))
- {
- return true;
- }
- }
-#else
- // Older Unity versions: use Package Manager Client.List as a fallback
- var list = UnityEditor.PackageManager.Client.List();
- while (!list.IsCompleted) { }
- if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
- {
- foreach (var pkg in list.Result)
- {
- if (TryResolveWithinPackage(pkg, out srcPath))
- {
- return true;
- }
- }
- }
-#endif
- }
- catch { /* ignore */ }
-
- // 3) Fallback to previous common install locations
- try
- {
- string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
- string[] candidates =
- {
- Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
- Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
- };
- foreach (string candidate in candidates)
- {
- if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
- {
- srcPath = candidate;
- return true;
- }
- }
- }
- catch { /* ignore */ }
-
- srcPath = null;
- return false;
- }
-
- private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath)
- {
- const string CurrentId = "com.coplaydev.unity-mcp";
-
- srcPath = null;
- if (p == null || p.name != CurrentId)
- {
- return false;
- }
-
- string packagePath = p.resolvedPath;
-
- // Preferred tilde folder (embedded but excluded from import)
- string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src");
- if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py")))
- {
- srcPath = embeddedTilde;
- return true;
- }
-
- // Legacy non-tilde folder
- string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
- if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
- {
- srcPath = embedded;
- return true;
- }
-
- // Dev-linked sibling of the package folder
- string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
- if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
- {
- srcPath = sibling;
- return true;
- }
-
- return false;
- }
- }
-}
diff --git a/MCPForUnity/Editor/Helpers/ServerPathResolver.cs.meta b/MCPForUnity/Editor/Helpers/ServerPathResolver.cs.meta
deleted file mode 100644
index d02df608b..000000000
--- a/MCPForUnity/Editor/Helpers/ServerPathResolver.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
\ No newline at end of file
diff --git a/MCPForUnity/Editor/MCPForUnity.Editor.asmdef b/MCPForUnity/Editor/MCPForUnity.Editor.asmdef
index 88448922b..50e0897de 100644
--- a/MCPForUnity/Editor/MCPForUnity.Editor.asmdef
+++ b/MCPForUnity/Editor/MCPForUnity.Editor.asmdef
@@ -11,7 +11,9 @@
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
- "precompiledReferences": [],
+ "precompiledReferences": [
+ "Newtonsoft.Json.dll"
+ ],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
diff --git a/MCPForUnity/Editor/MCPForUnityBridge.cs b/MCPForUnity/Editor/MCPForUnityBridge.cs
index 23537b817..48ded904d 100644
--- a/MCPForUnity/Editor/MCPForUnityBridge.cs
+++ b/MCPForUnity/Editor/MCPForUnityBridge.cs
@@ -408,7 +408,7 @@ public static void Start()
isRunning = true;
isAutoConnectMode = false;
string platform = Application.platform.ToString();
- string serverVer = ReadInstalledServerVersionSafe();
+ string serverVer = AssetPathUtility.GetPackageVersion();
McpLog.Info($"MCPForUnityBridge started on port {currentUnityPort}. (OS={platform}, server={serverVer})");
// Start background listener with cooperative cancellation
cts = new CancellationTokenSource();
@@ -1259,22 +1259,6 @@ private static void WriteHeartbeat(bool reloading, string reason = null)
}
}
- private static string ReadInstalledServerVersionSafe()
- {
- try
- {
- string serverSrc = ServerInstaller.GetServerPath();
- string verFile = Path.Combine(serverSrc, "server_version.txt");
- if (File.Exists(verFile))
- {
- string v = File.ReadAllText(verFile)?.Trim();
- if (!string.IsNullOrEmpty(v)) return v;
- }
- }
- catch { }
- return "unknown";
- }
-
private static string ComputeProjectHash(string input)
{
try
diff --git a/MCPForUnity/Editor/MCPForUnityMenu.cs b/MCPForUnity/Editor/MCPForUnityMenu.cs
index 714e48535..db82a8c98 100644
--- a/MCPForUnity/Editor/MCPForUnityMenu.cs
+++ b/MCPForUnity/Editor/MCPForUnityMenu.cs
@@ -1,7 +1,9 @@
using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Setup;
using MCPForUnity.Editor.Windows;
using UnityEditor;
+using UnityEngine;
namespace MCPForUnity.Editor
{
@@ -31,45 +33,42 @@ public static void OpenMCPWindow()
{
MCPForUnityEditorWindow.ShowWindow();
}
-
+
// ========================================
- // Tool Sync Menu Items
+ // Maintenance Menu Items
// ========================================
-
- ///
- /// Reimport all Python files in the project
- ///
- [MenuItem("Window/MCP For Unity/Tool Sync/Reimport Python Files", priority = 99)]
- public static void ReimportPythonFiles()
- {
- PythonToolSyncProcessor.ReimportPythonFiles();
- }
-
- ///
- /// Manually sync Python tools to the MCP server
- ///
- [MenuItem("Window/MCP For Unity/Tool Sync/Sync Python Tools", priority = 100)]
- public static void SyncPythonTools()
- {
- PythonToolSyncProcessor.ManualSync();
- }
-
- ///
- /// Toggle auto-sync for Python tools
- ///
- [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", priority = 101)]
- public static void ToggleAutoSync()
- {
- PythonToolSyncProcessor.ToggleAutoSync();
- }
-
+
///
- /// Validate menu item (shows checkmark when auto-sync is enabled)
+ /// Clear the local uvx cache for the MCP server
///
- [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", true, priority = 101)]
- public static bool ToggleAutoSyncValidate()
+ [MenuItem("Window/MCP For Unity/Maintenance/Clear UVX Cache", priority = 200)]
+ public static void ClearUvxCache()
{
- return PythonToolSyncProcessor.ToggleAutoSyncValidate();
+ if (EditorUtility.DisplayDialog(
+ "Clear UVX Cache",
+ "This will clear the local uvx cache for the MCP server package. " +
+ "The server will be re-downloaded on next launch.\n\n" +
+ "Continue?",
+ "Clear Cache",
+ "Cancel"))
+ {
+ bool success = MCPServiceLocator.Cache.ClearUvxCache();
+
+ if (success)
+ {
+ EditorUtility.DisplayDialog(
+ "Success",
+ "UVX cache cleared successfully. The server will be re-downloaded on next launch.",
+ "OK");
+ }
+ else
+ {
+ EditorUtility.DisplayDialog(
+ "Error",
+ "Failed to clear UVX cache. Check the console for details.",
+ "OK");
+ }
+ }
}
}
}
diff --git a/TestProjects/UnityMCPTests/Assets/Temp.meta b/MCPForUnity/Editor/MenuItems.meta
similarity index 77%
rename from TestProjects/UnityMCPTests/Assets/Temp.meta
rename to MCPForUnity/Editor/MenuItems.meta
index 30148f258..ad5fb5e60 100644
--- a/TestProjects/UnityMCPTests/Assets/Temp.meta
+++ b/MCPForUnity/Editor/MenuItems.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 02a6714b521ec47868512a8db433975c
+guid: 9e7f37616736f4d3cbd8bdbc626f5ab9
folderAsset: yes
DefaultImporter:
externalObjects: {}
diff --git a/MCPForUnity/Editor/MenuItems/CustomToolsMenuItems.cs b/MCPForUnity/Editor/MenuItems/CustomToolsMenuItems.cs
new file mode 100644
index 000000000..1cebdbdda
--- /dev/null
+++ b/MCPForUnity/Editor/MenuItems/CustomToolsMenuItems.cs
@@ -0,0 +1,62 @@
+using UnityEditor;
+using UnityEngine;
+using MCPForUnity.Editor.Helpers;
+
+namespace MCPForUnity.Editor.MenuItems
+{
+ ///
+ /// Menu items for custom tool management
+ ///
+ public static class CustomToolsMenuItems
+ {
+ [MenuItem("Window/MCP For Unity/Custom Tools/Register All Tools")]
+ public static void RegisterAllTools()
+ {
+ CustomToolRegistrationProcessor.RegisterAllToolsSync();
+ }
+
+ [MenuItem("Window/MCP For Unity/Custom Tools/Force Re-registration")]
+ public static void ForceReregistration()
+ {
+ CustomToolRegistrationProcessor.ForceReregistration();
+ }
+
+ [MenuItem("Window/MCP For Unity/Custom Tools/Show Tool Info")]
+ public static void ShowToolInfo()
+ {
+ string info = CustomToolRegistrationProcessor.GetToolInfo();
+ Debug.Log($"MCP Custom Tools Info:\n{info}");
+
+ // Also show in dialog
+ EditorUtility.DisplayDialog("MCP Custom Tools", info, "OK");
+ }
+
+ [MenuItem("Window/MCP For Unity/Custom Tools/Enable Registration")]
+ public static void EnableRegistration()
+ {
+ CustomToolRegistrationProcessor.IsRegistrationEnabled = true;
+ EditorPrefs.SetBool("MCPForUnity.CustomToolRegistrationEnabled", true);
+ Debug.Log("MCP Custom Tool Registration enabled");
+ }
+
+ [MenuItem("Window/MCP For Unity/Custom Tools/Disable Registration")]
+ public static void DisableRegistration()
+ {
+ CustomToolRegistrationProcessor.IsRegistrationEnabled = false;
+ EditorPrefs.SetBool("MCPForUnity.CustomToolRegistrationEnabled", false);
+ Debug.Log("MCP Custom Tool Registration disabled");
+ }
+
+ [MenuItem("Window/MCP For Unity/Custom Tools/Enable Registration", true)]
+ public static bool EnableRegistrationValidate()
+ {
+ return !CustomToolRegistrationProcessor.IsRegistrationEnabled;
+ }
+
+ [MenuItem("Window/MCP For Unity/Custom Tools/Disable Registration", true)]
+ public static bool DisableRegistrationValidate()
+ {
+ return CustomToolRegistrationProcessor.IsRegistrationEnabled;
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Data/PythonToolsAsset.cs.meta b/MCPForUnity/Editor/MenuItems/CustomToolsMenuItems.cs.meta
similarity index 83%
rename from MCPForUnity/Editor/Data/PythonToolsAsset.cs.meta
rename to MCPForUnity/Editor/MenuItems/CustomToolsMenuItems.cs.meta
index bfe30d9f5..f0869e318 100644
--- a/MCPForUnity/Editor/Data/PythonToolsAsset.cs.meta
+++ b/MCPForUnity/Editor/MenuItems/CustomToolsMenuItems.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 1ad9865b38bcc4efe85d4970c6d3a997
+guid: d14bd1fefa6944e97a66138e14887cde
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/MCPForUnity/Editor/Services/BridgeControlService.cs b/MCPForUnity/Editor/Services/BridgeControlService.cs
index a462e68b3..744c72dc0 100644
--- a/MCPForUnity/Editor/Services/BridgeControlService.cs
+++ b/MCPForUnity/Editor/Services/BridgeControlService.cs
@@ -3,34 +3,114 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
+using UnityEditor;
+using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Services
{
///
/// Implementation of bridge control service
+ /// Supports both HTTP and TCP socket (stdio) transports
///
public class BridgeControlService : IBridgeControlService
{
- public bool IsRunning => MCPForUnityBridge.IsRunning;
+ private HttpMcpClient _httpClient;
+ private bool _useHttpTransport;
+ public bool IsRunning
+ {
+ get
+ {
+ if (_useHttpTransport)
+ {
+ return _httpClient != null && _httpClient.IsConnected;
+ }
+ return MCPForUnityBridge.IsRunning;
+ }
+ }
public int CurrentPort => MCPForUnityBridge.GetCurrentPort();
public bool IsAutoConnectMode => MCPForUnityBridge.IsAutoConnectMode();
public void Start()
{
- // If server is installed, use auto-connect mode
- // Otherwise use standard mode
- string serverPath = MCPServiceLocator.Paths.GetMcpServerPath();
- if (!string.IsNullOrEmpty(serverPath) && File.Exists(Path.Combine(serverPath, "server.py")))
+ // Check transport mode from EditorPrefs
+ _useHttpTransport = EditorPrefs.GetBool("MCPForUnity.UseHttpTransport", true);
+
+ if (_useHttpTransport)
{
- MCPForUnityBridge.StartAutoConnect();
+ StartHttpTransport();
}
else
{
- MCPForUnityBridge.Start();
+ StartStdioTransport();
}
}
+ private void StartHttpTransport()
+ {
+ try
+ {
+ string httpUrl = EditorPrefs.GetString("MCPForUnity.HttpUrl", "http://localhost:8080");
+ McpLog.Info($"Starting HTTP transport to {httpUrl}");
+
+ // Dispose existing client if any
+ _httpClient?.Dispose();
+
+ // Create new HTTP client
+ _httpClient = new HttpMcpClient(httpUrl);
+
+ // Test connection asynchronously
+ System.Threading.Tasks.Task.Run(async () =>
+ {
+ bool connected = await _httpClient.TestConnectionAsync();
+ if (connected)
+ {
+ McpLog.Info("HTTP transport connected successfully");
+ }
+ else
+ {
+ McpLog.Warn("HTTP transport connection test failed - server may not be running");
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"Failed to start HTTP transport: {ex.Message}");
+ }
+ }
+
+ private void StartStdioTransport()
+ {
+ McpLog.Info("Starting stdio (TCP socket) transport");
+ MCPForUnityBridge.StartAutoConnect();
+ }
+
public void Stop()
+ {
+ if (_useHttpTransport)
+ {
+ StopHttpTransport();
+ }
+ else
+ {
+ StopStdioTransport();
+ }
+ }
+
+ private void StopHttpTransport()
+ {
+ try
+ {
+ _httpClient?.Dispose();
+ _httpClient = null;
+ McpLog.Info("HTTP transport stopped");
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"Error stopping HTTP transport: {ex.Message}");
+ }
+ }
+
+ private void StopStdioTransport()
{
MCPForUnityBridge.Stop();
}
@@ -106,7 +186,7 @@ private static void WriteFrame(NetworkStream stream, byte[] payload, int timeout
{
if (payload == null) throw new ArgumentNullException(nameof(payload));
if (payload.LongLength < 1) throw new IOException("Zero-length frames are not allowed");
-
+
byte[] header = new byte[8];
ulong len = (ulong)payload.LongLength;
header[0] = (byte)(len >> 56);
diff --git a/MCPForUnity/Editor/Services/CacheManagementService.cs b/MCPForUnity/Editor/Services/CacheManagementService.cs
new file mode 100644
index 000000000..92d989bdf
--- /dev/null
+++ b/MCPForUnity/Editor/Services/CacheManagementService.cs
@@ -0,0 +1,54 @@
+using System;
+using MCPForUnity.Editor.Helpers;
+using UnityEditor;
+using UnityEngine;
+
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Service for managing cache operations
+ ///
+ public class CacheManagementService : ICacheManagementService
+ {
+ ///
+ /// Clear the local uvx cache for the MCP server package
+ ///
+ /// True if successful, false otherwise
+ public bool ClearUvxCache()
+ {
+ try
+ {
+ var pathService = MCPServiceLocator.Paths;
+ string uvxPath = pathService.GetUvxPath();
+
+ if (string.IsNullOrEmpty(uvxPath))
+ {
+ McpLog.Error("UVX not found. Please install UV/UVX first.");
+ return false;
+ }
+
+ // Get the package name
+ string packageName = "mcp-for-unity";
+
+ // Run uvx cache clean command
+ string args = $"cache clean {packageName}";
+
+ if (ExecPath.TryRun(uvxPath, args, Application.dataPath, out var stdout, out var stderr, 30000))
+ {
+ McpLog.Info($"UVX cache cleared successfully: {stdout}");
+ return true;
+ }
+ else
+ {
+ McpLog.Warn($"Failed to clear uvx cache: {stderr}");
+ return false;
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"Error clearing uvx cache: {ex.Message}");
+ return false;
+ }
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs.meta b/MCPForUnity/Editor/Services/CacheManagementService.cs.meta
similarity index 83%
rename from MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs.meta
rename to MCPForUnity/Editor/Services/CacheManagementService.cs.meta
index d3a3719ca..ff1d90325 100644
--- a/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs.meta
+++ b/MCPForUnity/Editor/Services/CacheManagementService.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 4bdcf382960c842aab0a08c90411ab43
+guid: 7cee3887f299d4a9aa9f2f0ffea593f0
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs b/MCPForUnity/Editor/Services/ClientConfigurationService.cs
index 8a9c4cafd..b2c2f0e40 100644
--- a/MCPForUnity/Editor/Services/ClientConfigurationService.cs
+++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs
@@ -20,38 +20,24 @@ public class ClientConfigurationService : IClientConfigurationService
public void ConfigureClient(McpClient client)
{
- try
- {
- string configPath = McpConfigurationHelper.GetClientConfigPath(client);
- McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
-
- string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath();
-
- if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py")))
- {
- throw new InvalidOperationException("Server not found. Please use manual configuration or set server path in Advanced Settings.");
- }
+ var pathService = MCPServiceLocator.Paths;
+ string uvxPath = pathService.GetUvxPath();
+
+ string configPath = McpConfigurationHelper.GetClientConfigPath(client);
+ McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
- string result = client.mcpType == McpTypes.Codex
- ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client)
- : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
+ string result = client.mcpType == McpTypes.Codex
+ ? McpConfigurationHelper.ConfigureCodexClient(configPath, client)
+ : McpConfigurationHelper.WriteMcpConfiguration(configPath, client);
- if (result == "Configured successfully")
- {
- client.SetStatus(McpStatus.Configured);
- Debug.Log($"MCP-FOR-UNITY: {client.name} configured successfully");
- }
- else
- {
- Debug.LogWarning($"Configuration completed with message: {result}");
- }
-
- CheckClientStatus(client);
+ if (result == "Configured successfully")
+ {
+ client.SetStatus(McpStatus.Configured);
}
- catch (Exception ex)
+ else
{
- Debug.LogError($"Failed to configure {client.name}: {ex.Message}");
- throw;
+ client.SetStatus(McpStatus.NotConfigured);
+ throw new InvalidOperationException($"Configuration failed: {result}");
}
}
@@ -89,11 +75,11 @@ public ClientConfigurationSummary ConfigureAllDetectedClients()
}
else
{
- // Other clients require UV
- if (!pathService.IsUvDetected())
+ // Other clients require UVX
+ if (!pathService.IsUvxDetected())
{
summary.SkippedCount++;
- summary.Messages.Add($"➜ {client.name}: UV not found");
+ summary.Messages.Add($"➜ {client.name}: UVX not found");
continue;
}
@@ -134,8 +120,6 @@ public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true)
}
string configJson = File.ReadAllText(configPath);
- string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath();
-
// Check configuration based on client type
string[] args = null;
bool configExists = false;
@@ -176,9 +160,10 @@ public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true)
if (configExists)
{
- string configuredDir = McpConfigurationHelper.ExtractDirectoryArg(args);
- bool matches = !string.IsNullOrEmpty(configuredDir) &&
- McpConfigurationHelper.PathsEqual(configuredDir, pythonDir);
+ string expectedUvxUrl = AssetPathUtility.GetMcpServerGitUrl();
+ string configuredUvxUrl = McpConfigurationHelper.ExtractUvxUrl(args);
+ bool matches = !string.IsNullOrEmpty(configuredUvxUrl) &&
+ McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl);
if (matches)
{
@@ -190,15 +175,15 @@ public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true)
try
{
string rewriteResult = client.mcpType == McpTypes.Codex
- ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client)
- : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
+ ? McpConfigurationHelper.ConfigureCodexClient(configPath, client)
+ : McpConfigurationHelper.WriteMcpConfiguration(configPath, client);
if (rewriteResult == "Configured successfully")
{
bool debugLogsEnabled = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
if (debugLogsEnabled)
{
- McpLog.Info($"Auto-updated MCP config for '{client.name}' to new path: {pythonDir}", always: false);
+ McpLog.Info($"Auto-updated MCP config for '{client.name}' to new version: {expectedUvxUrl}", always: false);
}
client.SetStatus(McpStatus.Configured);
}
@@ -233,21 +218,16 @@ public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true)
public void RegisterClaudeCode()
{
var pathService = MCPServiceLocator.Paths;
- string pythonDir = pathService.GetMcpServerPath();
-
- if (string.IsNullOrEmpty(pythonDir))
- {
- throw new InvalidOperationException("Cannot register: Python directory not found");
- }
-
string claudePath = pathService.GetClaudeCliPath();
if (string.IsNullOrEmpty(claudePath))
{
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
}
- string uvPath = pathService.GetUvPath() ?? "uv";
- string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py";
+ // Use structured uvx command parts for proper quoting
+ var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
+
+ string args = $"mcp add UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" {packageName}";
string projectDir = Path.GetDirectoryName(Application.dataPath);
string pathPrepend = null;
@@ -278,7 +258,7 @@ public void RegisterClaudeCode()
string combined = ($"{stdout}\n{stderr}") ?? string.Empty;
if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0)
{
- Debug.Log("MCP-FOR-UNITY: MCP for Unity already registered with Claude Code.");
+ McpLog.Info("MCP for Unity already registered with Claude Code.");
}
else
{
@@ -287,7 +267,7 @@ public void RegisterClaudeCode()
return;
}
- Debug.Log("MCP-FOR-UNITY: Successfully registered with Claude Code.");
+ McpLog.Info("Successfully registered with Claude Code.");
// Update status
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
@@ -301,7 +281,7 @@ public void UnregisterClaudeCode()
{
var pathService = MCPServiceLocator.Paths;
string claudePath = pathService.GetClaudeCliPath();
-
+
if (string.IsNullOrEmpty(claudePath))
{
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
@@ -323,14 +303,14 @@ public void UnregisterClaudeCode()
{
claudeClient.SetStatus(McpStatus.NotConfigured);
}
- Debug.Log("MCP-FOR-UNITY: No MCP for Unity server found - already unregistered.");
+ McpLog.Info("No MCP for Unity server found - already unregistered.");
return;
}
// Remove the server
if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
{
- Debug.Log("MCP-FOR-UNITY: MCP server successfully unregistered from Claude Code.");
+ McpLog.Info("MCP server successfully unregistered from Claude Code.");
}
else
{
@@ -366,19 +346,19 @@ public string GetConfigPath(McpClient client)
public string GenerateConfigJson(McpClient client)
{
- string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath();
- string uvPath = MCPServiceLocator.Paths.GetUvPath();
+ string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
// Claude Code uses CLI commands, not JSON config
if (client.mcpType == McpTypes.ClaudeCode)
{
- if (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath))
+ if (string.IsNullOrEmpty(uvxPath))
{
return "# Error: Configuration not available - check paths in Advanced Settings";
}
// Show the actual command that RegisterClaudeCode() uses
- string registerCommand = $"claude mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py";
+ string gitUrl = AssetPathUtility.GetMcpServerGitUrl();
+ string registerCommand = $"claude mcp add UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" mcp-for-unity";
return "# Register the MCP server with Claude Code:\n" +
$"{registerCommand}\n\n" +
@@ -388,19 +368,18 @@ public string GenerateConfigJson(McpClient client)
"claude mcp list # Only works when claude is run in the project's directory";
}
- if (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath))
+ if (string.IsNullOrEmpty(uvxPath))
return "{ \"error\": \"Configuration not available - check paths in Advanced Settings\" }";
try
{
if (client.mcpType == McpTypes.Codex)
{
- return CodexConfigHelper.BuildCodexServerBlock(uvPath,
- McpConfigurationHelper.ResolveServerDirectory(pythonDir, null));
+ return CodexConfigHelper.BuildCodexServerBlock(uvxPath);
}
else
{
- return ConfigJsonBuilder.BuildManualConfigJson(uvPath, pythonDir, client);
+ return ConfigJsonBuilder.BuildManualConfigJson(uvxPath, client);
}
}
catch (Exception ex)
diff --git a/MCPForUnity/Editor/Services/CustomToolRegistrationService.cs b/MCPForUnity/Editor/Services/CustomToolRegistrationService.cs
new file mode 100644
index 000000000..6db12c076
--- /dev/null
+++ b/MCPForUnity/Editor/Services/CustomToolRegistrationService.cs
@@ -0,0 +1,195 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using MCPForUnity.Editor.Helpers;
+using UnityEditor;
+using UnityEngine;
+
+namespace MCPForUnity.Editor.Services
+{
+ public class CustomToolRegistrationService : ICustomToolRegistrationService
+ {
+ private readonly IToolDiscoveryService _discoveryService;
+
+ public CustomToolRegistrationService(IToolDiscoveryService discoveryService = null)
+ {
+ _discoveryService = discoveryService ?? new ToolDiscoveryService();
+ }
+
+ public async Task RegisterAllToolsAsync()
+ {
+ try
+ {
+ string projectId = GetProjectId();
+
+ // Discover tools via reflection
+ var tools = _discoveryService.DiscoverAllTools();
+
+ if (tools.Count == 0)
+ {
+ McpLog.Info("No tools found, skipping registration");
+ return true;
+ }
+
+ // Convert to registration format
+ var toolDefinitions = tools.Select(t => new
+ {
+ name = t.Name,
+ description = t.Description,
+ structured_output = t.StructuredOutput,
+ parameters = t.Parameters.Select(p => new
+ {
+ name = p.Name,
+ description = p.Description,
+ type = p.Type,
+ required = p.Required,
+ default_value = p.DefaultValue
+ }).ToList()
+ }).ToList();
+
+ // Call the FastMCP tool registration endpoint
+ var result = await CallMcpToolAsync("register_custom_tools", new
+ {
+ project_id = projectId,
+ tools = toolDefinitions
+ });
+
+ if (result != null && result.success == true)
+ {
+ McpLog.Info($"Successfully registered {result.registered?.Count ?? 0} tools with MCP server");
+ return true;
+ }
+ else
+ {
+ McpLog.Error($"Failed to register tools: {result?.error ?? "Unknown error"}");
+ return false;
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"Error registering tools: {ex.Message}");
+ return false;
+ }
+ }
+
+ public bool RegisterAllTools()
+ {
+ return RegisterAllToolsAsync().GetAwaiter().GetResult();
+ }
+
+ private string GetProjectId()
+ {
+ // Use project name + path hash as unique identifier
+ string projectName = Application.productName;
+ string projectPath = Application.dataPath;
+ string combined = $"{projectName}:{projectPath}";
+
+ using (var sha256 = System.Security.Cryptography.SHA256.Create())
+ {
+ byte[] hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(combined));
+ return BitConverter.ToString(hash).Replace("-", "").Substring(0, 16);
+ }
+ }
+
+ private async Task CallMcpToolAsync(string toolName, object parameters)
+ {
+ try
+ {
+ // For HTTP transport mode, we can make direct HTTP calls to FastMCP
+ // For stdio transport mode, we need to use the bridge
+ bool isHttpTransport = EditorPrefs.GetBool("MCPForUnity.UseHttpTransport", false);
+
+ if (isHttpTransport)
+ {
+ // Make direct HTTP call to FastMCP HTTP endpoint
+ return await CallFastMcpHttpAsync(toolName, parameters);
+ }
+ else
+ {
+ // Use the existing MCP bridge for stdio transport
+ return await CallMcpBridgeAsync(toolName, parameters);
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"Error calling MCP tool {toolName}: {ex.Message}");
+ return new { success = false, error = ex.Message };
+ }
+ }
+
+ private async Task CallFastMcpHttpAsync(string toolName, object parameters)
+ {
+ try
+ {
+ string httpUrl = EditorPrefs.GetString("MCPForUnity.HttpUrl", "http://localhost:8080");
+ // Ensure URL doesn't have trailing slash before adding path
+ httpUrl = httpUrl.TrimEnd('/');
+ string url = $"{httpUrl}/tools/call";
+
+ using (var client = new System.Net.Http.HttpClient())
+ {
+ client.Timeout = TimeSpan.FromSeconds(30);
+
+ var requestBody = new
+ {
+ name = toolName,
+ arguments = parameters
+ };
+
+ string jsonContent = Newtonsoft.Json.JsonConvert.SerializeObject(requestBody);
+ var content = new System.Net.Http.StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
+
+ var response = await client.PostAsync(url, content);
+ string responseText = await response.Content.ReadAsStringAsync();
+
+ if (response.IsSuccessStatusCode)
+ {
+ return Newtonsoft.Json.JsonConvert.DeserializeObject(responseText);
+ }
+ else
+ {
+ McpLog.Error($"HTTP call failed: {response.StatusCode} - {responseText}");
+ return new { success = false, error = $"HTTP {response.StatusCode}: {responseText}" };
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"HTTP call exception: {ex.Message}");
+ return new { success = false, error = ex.Message };
+ }
+ }
+
+ private async Task CallMcpBridgeAsync(string toolName, object parameters)
+ {
+ // Use the existing MCP bridge for stdio transport
+ // This would send the tool call through the Unity socket bridge
+ try
+ {
+ // For stdio mode, we need to send through the bridge
+ // The bridge service would handle the MCP protocol communication
+ var bridgeService = MCPServiceLocator.Bridge;
+
+ if (!bridgeService.IsRunning)
+ {
+ return new { success = false, error = "Bridge is not running" };
+ }
+
+ // Serialize the tool call
+ string jsonParams = Newtonsoft.Json.JsonConvert.SerializeObject(parameters);
+
+ // Send via bridge (this is a simplified version - actual implementation may vary)
+ // For now, return a placeholder indicating stdio mode needs the server running
+ await Task.Delay(100);
+ return new { success = true, registered = new List(), message = "Stdio mode: Tools will be registered when server starts" };
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"Bridge call exception: {ex.Message}");
+ return new { success = false, error = ex.Message };
+ }
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Helpers/McpPathResolver.cs.meta b/MCPForUnity/Editor/Services/CustomToolRegistrationService.cs.meta
similarity index 83%
rename from MCPForUnity/Editor/Helpers/McpPathResolver.cs.meta
rename to MCPForUnity/Editor/Services/CustomToolRegistrationService.cs.meta
index 38f19973a..eda96cfad 100644
--- a/MCPForUnity/Editor/Helpers/McpPathResolver.cs.meta
+++ b/MCPForUnity/Editor/Services/CustomToolRegistrationService.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 2c76f0c7ff138ba4a952481e04bc3974
+guid: 6bab1a9bfedfc496b873c20f450f70bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/MCPForUnity/Editor/Services/HttpMcpClient.cs b/MCPForUnity/Editor/Services/HttpMcpClient.cs
new file mode 100644
index 000000000..d05f40d37
--- /dev/null
+++ b/MCPForUnity/Editor/Services/HttpMcpClient.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using MCPForUnity.Editor.Helpers;
+using UnityEditor;
+
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Simple HTTP client for connecting to FastMCP server
+ /// Uses REST API for tool execution
+ ///
+ public class HttpMcpClient : IDisposable
+ {
+ private readonly HttpClient _httpClient;
+ private readonly string _baseUrl;
+ private bool _isConnected;
+
+ public bool IsConnected => _isConnected;
+ public string BaseUrl => _baseUrl;
+
+ public HttpMcpClient(string baseUrl)
+ {
+ if (string.IsNullOrEmpty(baseUrl))
+ {
+ throw new ArgumentException("Base URL cannot be null or empty", nameof(baseUrl));
+ }
+
+ _baseUrl = baseUrl.TrimEnd('/');
+ _httpClient = new HttpClient
+ {
+ Timeout = TimeSpan.FromSeconds(30)
+ };
+ _isConnected = false;
+ }
+
+ ///
+ /// Test connection to the MCP server
+ ///
+ public async Task TestConnectionAsync()
+ {
+ try
+ {
+ // Try to ping the server
+ var response = await _httpClient.GetAsync($"{_baseUrl}/health");
+ _isConnected = response.IsSuccessStatusCode;
+ return _isConnected;
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"HTTP connection test failed: {ex.Message}");
+ _isConnected = false;
+ return false;
+ }
+ }
+
+ ///
+ /// Execute a tool on the MCP server via HTTP
+ ///
+ public async Task ExecuteToolAsync(string toolName, JObject parameters)
+ {
+ try
+ {
+ var requestBody = new
+ {
+ name = toolName,
+ arguments = parameters
+ };
+
+ string jsonContent = JsonConvert.SerializeObject(requestBody);
+ var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
+
+ var response = await _httpClient.PostAsync($"{_baseUrl}/tools/call", content);
+ string responseText = await response.Content.ReadAsStringAsync();
+
+ if (response.IsSuccessStatusCode)
+ {
+ return JObject.Parse(responseText);
+ }
+ else
+ {
+ McpLog.Error($"HTTP tool execution failed: {response.StatusCode} - {responseText}");
+ return new JObject
+ {
+ ["success"] = false,
+ ["error"] = $"HTTP {response.StatusCode}: {responseText}"
+ };
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"HTTP tool execution exception: {ex.Message}");
+ return new JObject
+ {
+ ["success"] = false,
+ ["error"] = ex.Message
+ };
+ }
+ }
+
+ ///
+ /// Send a command to Unity via the MCP server
+ /// This is the main method for Unity plugin communication
+ ///
+ public async Task SendCommandAsync(string commandJson)
+ {
+ try
+ {
+ // Parse the command to extract tool name and parameters
+ var command = JObject.Parse(commandJson);
+ string toolName = command["tool"]?.ToString();
+ var parameters = command["parameters"] as JObject ?? new JObject();
+
+ if (string.IsNullOrEmpty(toolName))
+ {
+ return JsonConvert.SerializeObject(new { success = false, error = "Tool name is required" });
+ }
+
+ // Execute the tool
+ var result = await ExecuteToolAsync(toolName, parameters);
+ return result.ToString();
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"HTTP send command exception: {ex.Message}");
+ return JsonConvert.SerializeObject(new { success = false, error = ex.Message });
+ }
+ }
+
+ public void Dispose()
+ {
+ _httpClient?.Dispose();
+ _isConnected = false;
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Services/HttpMcpClient.cs.meta b/MCPForUnity/Editor/Services/HttpMcpClient.cs.meta
new file mode 100644
index 000000000..49a949e85
--- /dev/null
+++ b/MCPForUnity/Editor/Services/HttpMcpClient.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6a6f33fa3b16e41c7a2535354f1b8fdc
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/ICacheManagementService.cs b/MCPForUnity/Editor/Services/ICacheManagementService.cs
new file mode 100644
index 000000000..d8ac0fd07
--- /dev/null
+++ b/MCPForUnity/Editor/Services/ICacheManagementService.cs
@@ -0,0 +1,14 @@
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Interface for cache management operations
+ ///
+ public interface ICacheManagementService
+ {
+ ///
+ /// Clear the local uvx cache for the MCP server package
+ ///
+ /// True if successful, false otherwise
+ bool ClearUvxCache();
+ }
+}
diff --git a/MCPForUnity/Editor/Services/ICacheManagementService.cs.meta b/MCPForUnity/Editor/Services/ICacheManagementService.cs.meta
new file mode 100644
index 000000000..7f5307c90
--- /dev/null
+++ b/MCPForUnity/Editor/Services/ICacheManagementService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 056c7877da3584a1395a97fe26f13382
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/ICustomToolRegistrationService.cs b/MCPForUnity/Editor/Services/ICustomToolRegistrationService.cs
new file mode 100644
index 000000000..7265d955e
--- /dev/null
+++ b/MCPForUnity/Editor/Services/ICustomToolRegistrationService.cs
@@ -0,0 +1,20 @@
+using System.Threading.Tasks;
+
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Service for registering custom tools with MCP server via HTTP
+ ///
+ public interface ICustomToolRegistrationService
+ {
+ ///
+ /// Registers all discovered tools with the MCP server
+ ///
+ Task RegisterAllToolsAsync();
+
+ ///
+ /// Registers all discovered tools with the MCP server (synchronous)
+ ///
+ bool RegisterAllTools();
+ }
+}
diff --git a/MCPForUnity/Editor/Services/ICustomToolRegistrationService.cs.meta b/MCPForUnity/Editor/Services/ICustomToolRegistrationService.cs.meta
new file mode 100644
index 000000000..a205a229c
--- /dev/null
+++ b/MCPForUnity/Editor/Services/ICustomToolRegistrationService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 85ff4cd02f04f4bd6bb4d7ccb80290be
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/IPathResolverService.cs b/MCPForUnity/Editor/Services/IPathResolverService.cs
index 9968af656..c69e1f3f3 100644
--- a/MCPForUnity/Editor/Services/IPathResolverService.cs
+++ b/MCPForUnity/Editor/Services/IPathResolverService.cs
@@ -4,18 +4,12 @@ namespace MCPForUnity.Editor.Services
/// Service for resolving paths to required tools and supporting user overrides
///
public interface IPathResolverService
- {
+ {
///
- /// Gets the MCP server path (respects override if set)
+ /// Gets the UVX package manager path (respects override if set)
///
- /// Path to the MCP server directory containing server.py, or null if not found
- string GetMcpServerPath();
-
- ///
- /// Gets the UV package manager path (respects override if set)
- ///
- /// Path to the uv executable, or null if not found
- string GetUvPath();
+ /// Path to the uvx executable, or null if not found
+ string GetUvxPath(bool verifyPath = true);
///
/// Gets the Claude CLI path (respects override if set)
@@ -30,10 +24,10 @@ public interface IPathResolverService
bool IsPythonDetected();
///
- /// Checks if UV is detected on the system
+ /// Checks if UVX is detected on the system
///
- /// True if UV is found
- bool IsUvDetected();
+ /// True if UVX is found
+ bool IsUvxDetected();
///
/// Checks if Claude CLI is detected on the system
@@ -42,16 +36,10 @@ public interface IPathResolverService
bool IsClaudeCliDetected();
///
- /// Sets an override for the MCP server path
- ///
- /// Path to override with
- void SetMcpServerOverride(string path);
-
- ///
- /// Sets an override for the UV path
+ /// Sets an override for the UVX path
///
/// Path to override with
- void SetUvPathOverride(string path);
+ void SetUvxPathOverride(string path);
///
/// Sets an override for the Claude CLI path
@@ -60,14 +48,9 @@ public interface IPathResolverService
void SetClaudeCliPathOverride(string path);
///
- /// Clears the MCP server path override
+ /// Clears the UVX path override
///
- void ClearMcpServerOverride();
-
- ///
- /// Clears the UV path override
- ///
- void ClearUvPathOverride();
+ void ClearUvxPathOverride();
///
/// Clears the Claude CLI path override
@@ -75,18 +58,19 @@ public interface IPathResolverService
void ClearClaudeCliPathOverride();
///
- /// Gets whether a MCP server path override is active
+ /// Gets whether a UVX path override is active
///
- bool HasMcpServerOverride { get; }
+ bool HasUvxPathOverride { get; }
///
- /// Gets whether a UV path override is active
+ /// Gets whether a Claude CLI path override is active
///
- bool HasUvPathOverride { get; }
+ bool HasClaudeCliPathOverride { get; }
///
- /// Gets whether a Claude CLI path override is active
+ /// Gets the source path of the uvx-installed unity-mcp package
///
- bool HasClaudeCliPathOverride { get; }
+ /// The path to the source directory, or null if not found
+ string GetUvxPackageSourcePath();
}
}
diff --git a/MCPForUnity/Editor/Services/IPythonToolRegistryService.cs b/MCPForUnity/Editor/Services/IPythonToolRegistryService.cs
deleted file mode 100644
index dde40d101..000000000
--- a/MCPForUnity/Editor/Services/IPythonToolRegistryService.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Collections.Generic;
-using UnityEngine;
-using MCPForUnity.Editor.Data;
-
-namespace MCPForUnity.Editor.Services
-{
- public interface IPythonToolRegistryService
- {
- IEnumerable GetAllRegistries();
- bool NeedsSync(PythonToolsAsset registry, TextAsset file);
- void RecordSync(PythonToolsAsset registry, TextAsset file);
- string ComputeHash(TextAsset file);
- }
-}
diff --git a/MCPForUnity/Editor/Services/IPythonToolRegistryService.cs.meta b/MCPForUnity/Editor/Services/IPythonToolRegistryService.cs.meta
deleted file mode 100644
index 3f4835fce..000000000
--- a/MCPForUnity/Editor/Services/IPythonToolRegistryService.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: a2487319df5cc47baa2c635b911038c5
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/IToolDiscoveryService.cs b/MCPForUnity/Editor/Services/IToolDiscoveryService.cs
new file mode 100644
index 000000000..ccd17aba1
--- /dev/null
+++ b/MCPForUnity/Editor/Services/IToolDiscoveryService.cs
@@ -0,0 +1,50 @@
+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; }
+ }
+
+ ///
+ /// 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);
+
+ ///
+ /// Invalidates the tool discovery cache
+ ///
+ void InvalidateCache();
+ }
+}
diff --git a/MCPForUnity/Editor/Services/IToolDiscoveryService.cs.meta b/MCPForUnity/Editor/Services/IToolDiscoveryService.cs.meta
new file mode 100644
index 000000000..a25b7492b
--- /dev/null
+++ b/MCPForUnity/Editor/Services/IToolDiscoveryService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 497592a93fd994b2cb9803e7c8636ff7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/IToolSyncService.cs b/MCPForUnity/Editor/Services/IToolSyncService.cs
deleted file mode 100644
index 3a62fdfba..000000000
--- a/MCPForUnity/Editor/Services/IToolSyncService.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Collections.Generic;
-
-namespace MCPForUnity.Editor.Services
-{
- public class ToolSyncResult
- {
- public int CopiedCount { get; set; }
- public int SkippedCount { get; set; }
- public int ErrorCount { get; set; }
- public List Messages { get; set; } = new List();
- public bool Success => ErrorCount == 0;
- }
-
- public interface IToolSyncService
- {
- ToolSyncResult SyncProjectTools(string destToolsDir);
- }
-}
diff --git a/MCPForUnity/Editor/Services/IToolSyncService.cs.meta b/MCPForUnity/Editor/Services/IToolSyncService.cs.meta
deleted file mode 100644
index 028282851..000000000
--- a/MCPForUnity/Editor/Services/IToolSyncService.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: b9627dbaa92d24783a9f20e42efcea18
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/MCPServiceLocator.cs b/MCPForUnity/Editor/Services/MCPServiceLocator.cs
index a743d4ce8..de34ed1b1 100644
--- a/MCPForUnity/Editor/Services/MCPServiceLocator.cs
+++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs
@@ -1,4 +1,5 @@
using System;
+using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Services
{
@@ -10,20 +11,22 @@ public static class MCPServiceLocator
private static IBridgeControlService _bridgeService;
private static IClientConfigurationService _clientService;
private static IPathResolverService _pathService;
- private static IPythonToolRegistryService _pythonToolRegistryService;
private static ITestRunnerService _testRunnerService;
- private static IToolSyncService _toolSyncService;
private static IPackageUpdateService _packageUpdateService;
private static IPlatformService _platformService;
+ private static IToolDiscoveryService _toolDiscoveryService;
+ private static ICustomToolRegistrationService _customToolRegistrationService;
+ private static ICacheManagementService _cacheManagementService;
public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService();
public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService();
public static IPathResolverService Paths => _pathService ??= new PathResolverService();
- public static IPythonToolRegistryService PythonToolRegistry => _pythonToolRegistryService ??= new PythonToolRegistryService();
public static ITestRunnerService Tests => _testRunnerService ??= new TestRunnerService();
- public static IToolSyncService ToolSync => _toolSyncService ??= new ToolSyncService();
public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService();
public static IPlatformService Platform => _platformService ??= new PlatformService();
+ public static IToolDiscoveryService ToolDiscovery => _toolDiscoveryService ??= new ToolDiscoveryService();
+ public static ICustomToolRegistrationService CustomToolRegistration => _customToolRegistrationService ??= new CustomToolRegistrationService();
+ public static ICacheManagementService Cache => _cacheManagementService ??= new CacheManagementService();
///
/// Registers a custom implementation for a service (useful for testing)
@@ -38,16 +41,18 @@ public static void Register(T implementation) where T : class
_clientService = c;
else if (implementation is IPathResolverService p)
_pathService = p;
- else if (implementation is IPythonToolRegistryService ptr)
- _pythonToolRegistryService = ptr;
else if (implementation is ITestRunnerService t)
_testRunnerService = t;
- else if (implementation is IToolSyncService ts)
- _toolSyncService = ts;
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 ICustomToolRegistrationService ctr)
+ _customToolRegistrationService = ctr;
+ else if (implementation is ICacheManagementService cm)
+ _cacheManagementService = cm;
}
///
@@ -58,20 +63,22 @@ public static void Reset()
(_bridgeService as IDisposable)?.Dispose();
(_clientService as IDisposable)?.Dispose();
(_pathService as IDisposable)?.Dispose();
- (_pythonToolRegistryService as IDisposable)?.Dispose();
(_testRunnerService as IDisposable)?.Dispose();
- (_toolSyncService as IDisposable)?.Dispose();
(_packageUpdateService as IDisposable)?.Dispose();
(_platformService as IDisposable)?.Dispose();
+ (_toolDiscoveryService as IDisposable)?.Dispose();
+ (_customToolRegistrationService as IDisposable)?.Dispose();
+ (_cacheManagementService as IDisposable)?.Dispose();
_bridgeService = null;
_clientService = null;
_pathService = null;
- _pythonToolRegistryService = null;
_testRunnerService = null;
- _toolSyncService = null;
_packageUpdateService = null;
_platformService = null;
+ _toolDiscoveryService = null;
+ _customToolRegistrationService = null;
+ _cacheManagementService = null;
}
}
}
diff --git a/MCPForUnity/Editor/Services/PathResolverService.cs b/MCPForUnity/Editor/Services/PathResolverService.cs
index 083115f76..c797c6b48 100644
--- a/MCPForUnity/Editor/Services/PathResolverService.cs
+++ b/MCPForUnity/Editor/Services/PathResolverService.cs
@@ -1,6 +1,8 @@
using System;
using System.Diagnostics;
using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
@@ -12,201 +14,347 @@ namespace MCPForUnity.Editor.Services
///
public class PathResolverService : IPathResolverService
{
- private const string PythonDirOverrideKey = "MCPForUnity.PythonDirOverride";
- private const string UvPathOverrideKey = "MCPForUnity.UvPath";
+ private const string UvxPathOverrideKey = "MCPForUnity.UvxPath";
private const string ClaudeCliPathOverrideKey = "MCPForUnity.ClaudeCliPath";
- public bool HasMcpServerOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(PythonDirOverrideKey, null));
- public bool HasUvPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(UvPathOverrideKey, null));
+ public bool HasUvxPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(UvxPathOverrideKey, null));
public bool HasClaudeCliPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(ClaudeCliPathOverrideKey, null));
- public string GetMcpServerPath()
+ public string GetUvxPath(bool verifyPath = true)
{
- // Check for override first
- string overridePath = EditorPrefs.GetString(PythonDirOverrideKey, null);
- if (!string.IsNullOrEmpty(overridePath) && File.Exists(Path.Combine(overridePath, "server.py")))
- {
- return overridePath;
- }
-
- // Fall back to automatic detection
- return McpPathResolver.FindPackagePythonDirectory(false);
- }
-
- public string GetUvPath()
- {
- // Check for override first
- string overridePath = EditorPrefs.GetString(UvPathOverrideKey, null);
- if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
- {
- return overridePath;
- }
-
- // Fall back to automatic detection
+ // If the user overrided the path in EditorPrefs, use it
try
{
- return ServerInstaller.FindUvPath();
- }
- catch
- {
- return null;
- }
- }
-
- public string GetClaudeCliPath()
- {
- // Check for override first
- string overridePath = EditorPrefs.GetString(ClaudeCliPathOverrideKey, null);
- if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
- {
- return overridePath;
+ string overridePath = EditorPrefs.GetString(UvxPathOverrideKey, string.Empty);
+ if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
+ {
+ if (verifyPath && VerifyUvxPath(overridePath)) return overridePath;
+ return overridePath;
+ }
}
+ catch { /* ignore */ }
- // Fall back to automatic detection
- return ExecPath.ResolveClaude();
- }
+ // Get environment variables
+ string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
+ string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty;
+ string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
- public bool IsPythonDetected()
- {
- try
+ // Then let's check if it's available via PATH
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- // Windows-specific Python detection
- if (Application.platform == RuntimePlatform.WindowsEditor)
+ try
{
- // Common Windows Python installation paths
- string[] windowsCandidates =
- {
- @"C:\Python314\python.exe",
- @"C:\Python313\python.exe",
- @"C:\Python312\python.exe",
- @"C:\Python311\python.exe",
- @"C:\Python310\python.exe",
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python314\python.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python313\python.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python312\python.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python311\python.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python310\python.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python314\python.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python313\python.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python312\python.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python311\python.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python310\python.exe"),
- };
-
- foreach (string c in windowsCandidates)
- {
- if (File.Exists(c)) return true;
- }
-
- // Try 'where python' command (Windows equivalent of 'which')
- var psi = new ProcessStartInfo
+ var wherePsi = new ProcessStartInfo
{
FileName = "where",
- Arguments = "python",
+ Arguments = "uvx.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
- using (var p = Process.Start(psi))
+ using var wp = Process.Start(wherePsi);
+ string output = wp.StandardOutput.ReadToEnd().Trim();
+ wp.WaitForExit(1500);
+ if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
{
- string outp = p.StandardOutput.ReadToEnd().Trim();
- p.WaitForExit(2000);
- if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
+ foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
- string[] lines = outp.Split('\n');
- foreach (string line in lines)
- {
- string trimmed = line.Trim();
- if (File.Exists(trimmed)) return true;
- }
+ string path = line.Trim();
+ if (File.Exists(path) && (verifyPath ? VerifyUvxPath(path) : true)) return path;
}
}
}
- else
+ catch { }
+
+ // Try common installation paths
+ string[] candidates = new[]
+ {
+ Path.Combine(programFiles, "uvx", "bin", "uvx.exe"),
+ Path.Combine(localAppData, "uvx", "bin", "uvx.exe"),
+ Path.Combine(localAppData, "Python", "Python310", "Scripts", "uvx.exe"),
+ Path.Combine(localAppData, "Programs", "uvx", "uvx.exe"),
+ Path.Combine(localAppData, "Microsoft", "WindowsApps", "uvx.exe")
+ };
+
+ foreach (var c in candidates)
{
- // macOS/Linux detection
- string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
- string[] candidates =
+ if (File.Exists(c) && (verifyPath ? VerifyUvxPath(c) : true)) return c;
+ }
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ // Check for uvx in common macOS locations
+ string[] candidates = new[]
+ {
+ "/opt/homebrew/bin/uvx",
+ "/usr/local/bin/uvx",
+ Path.Combine(appData, "uvx", "bin", "uvx"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".local", "bin", "uvx")
+ };
+
+ foreach (var c in candidates)
+ {
+ if (File.Exists(c) && (verifyPath ? VerifyUvxPath(c) : true)) return c;
+ }
+
+ // Try 'which uvx' command
+ try
+ {
+ var whichPsi = new ProcessStartInfo
{
- "/opt/homebrew/bin/python3",
- "/usr/local/bin/python3",
- "/usr/bin/python3",
- "/opt/local/bin/python3",
- Path.Combine(home, ".local", "bin", "python3"),
- "/Library/Frameworks/Python.framework/Versions/3.14/bin/python3",
- "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
- "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
- "/Library/Frameworks/Python.framework/Versions/3.11/bin/python3",
- "/Library/Frameworks/Python.framework/Versions/3.10/bin/python3",
+ FileName = "/bin/bash",
+ Arguments = "-c \"which uvx\"",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
};
- foreach (string c in candidates)
+ using var wp = Process.Start(whichPsi);
+ string output = wp.StandardOutput.ReadToEnd().Trim();
+ wp.WaitForExit(1500);
+ if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output) &&
+ (verifyPath ? VerifyUvxPath(output) : true))
{
- if (File.Exists(c)) return true;
+ return output;
}
+ }
+ catch { }
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ // Check for uvx in common Linux locations
+ string[] candidates = new[]
+ {
+ "/usr/bin/uvx",
+ "/usr/local/bin/uvx",
+ Path.Combine(appData, "uvx", "bin", "uvx"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".local", "bin", "uvx")
+ };
- // Try 'which python3'
- var psi = new ProcessStartInfo
+ foreach (var c in candidates)
+ {
+ if (File.Exists(c) && (verifyPath ? VerifyUvxPath(c) : true)) return c;
+ }
+
+ // Try 'which uvx' command
+ try
+ {
+ var whichPsi = new ProcessStartInfo
{
- FileName = "/usr/bin/which",
- Arguments = "python3",
+ FileName = "/bin/bash",
+ Arguments = "-c \"which uvx\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
- using (var p = Process.Start(psi))
+ using var wp = Process.Start(whichPsi);
+ string output = wp.StandardOutput.ReadToEnd().Trim();
+ wp.WaitForExit(1500);
+ if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output) &&
+ (verifyPath ? VerifyUvxPath(output) : true))
{
- string outp = p.StandardOutput.ReadToEnd().Trim();
- p.WaitForExit(2000);
- if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp) && File.Exists(outp)) return true;
+ return output;
}
}
+ catch { }
+ }
+
+ // Fallback: try just "uvx" (may work if in PATH)
+ try
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // On Windows, try uvx.exe directly
+ if (verifyPath ? VerifyUvxPath("uvx.exe") : true) return "uvx.exe";
+ }
+ else
+ {
+ // On Unix-like systems, try uvx
+ if (verifyPath ? VerifyUvxPath("uvx") : true) return "uvx";
+ }
}
catch { }
- return false;
+
+ return null;
+ }
+
+ public string GetClaudeCliPath()
+ {
+ try
+ {
+ string overridePath = EditorPrefs.GetString(ClaudeCliPathOverrideKey, string.Empty);
+ if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
+ {
+ return overridePath;
+ }
+ }
+ catch { /* ignore */ }
+
+ 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.Personal), ".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.Personal), ".local", "bin", "claude")
+ };
+
+ foreach (var c in candidates)
+ {
+ if (File.Exists(c)) return c;
+ }
+ }
+
+ return null;
}
- public bool IsUvDetected()
+ public bool IsPythonDetected()
{
- return !string.IsNullOrEmpty(GetUvPath());
+ try
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "python.exe" : "python3",
+ Arguments = "--version",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+ using var p = Process.Start(psi);
+ p.WaitForExit(2000);
+ return p.ExitCode == 0;
+ }
+ catch
+ {
+ return false;
+ }
}
- public bool IsClaudeCliDetected()
+ public bool IsUvxDetected()
{
- return !string.IsNullOrEmpty(GetClaudeCliPath());
+ return !string.IsNullOrEmpty(GetUvxPath());
}
- public void SetMcpServerOverride(string path)
+ public string GetUvxPackageSourcePath()
{
- if (string.IsNullOrEmpty(path))
+ try
{
- ClearMcpServerOverride();
- return;
- }
+ // Get the uv cache directory
+ var cacheDirProcess = new ProcessStartInfo
+ {
+ FileName = "uv",
+ Arguments = "cache dir",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(cacheDirProcess);
+ if (process == null) return null;
+
+ string cacheDir = process.StandardOutput.ReadToEnd().Trim();
+ process.WaitForExit(5000);
+
+ if (process.ExitCode != 0 || string.IsNullOrEmpty(cacheDir))
+ {
+ McpLog.Error("Failed to get uv cache directory");
+ return null;
+ }
+
+ // Normalize path for cross-platform compatibility
+ cacheDir = Path.GetFullPath(cacheDir);
+
+ // Look for git checkouts directory
+ string gitCheckoutsDir = Path.Combine(cacheDir, "git-v0", "checkouts");
+ if (!Directory.Exists(gitCheckoutsDir))
+ {
+ McpLog.Error($"Git checkouts directory not found: {gitCheckoutsDir}");
+ return null;
+ }
+
+ // Find the unity-mcp checkout directory
+ // The pattern is: git-v0/checkouts/{hash}/{commit-hash}/
+ foreach (var hashDir in Directory.GetDirectories(gitCheckoutsDir))
+ {
+ foreach (var commitDir in Directory.GetDirectories(hashDir))
+ {
+ // Check for the new Server structure
+ string serverPath = Path.Combine(commitDir, "Server");
+ if (Directory.Exists(serverPath))
+ {
+ // Verify it has the expected pyproject.toml
+ string pyprojectPath = Path.Combine(serverPath, "pyproject.toml");
+ if (File.Exists(pyprojectPath))
+ {
+ McpLog.Info($"Found uvx package source at: {serverPath}");
+ return serverPath;
+ }
+ }
+ }
+ }
- if (!File.Exists(Path.Combine(path, "server.py")))
+ var packageVersion = AssetPathUtility.GetPackageVersion();
+ McpLog.Error($"Server package source not found in uv cache. Make sure to run: uvx --from git+https://github.com/CoplayDev/unity-mcp@{packageVersion}#subdirectory=Server mcp-for-unity");
+ return null;
+ }
+ catch (System.Exception ex)
{
- throw new ArgumentException("The selected folder does not contain server.py");
+ McpLog.Error($"Failed to find uvx package source: {ex.Message}");
+ return null;
}
+ }
- EditorPrefs.SetString(PythonDirOverrideKey, path);
+ public bool IsClaudeCliDetected()
+ {
+ return !string.IsNullOrEmpty(GetClaudeCliPath());
}
- public void SetUvPathOverride(string path)
+ public void SetUvxPathOverride(string path)
{
if (string.IsNullOrEmpty(path))
{
- ClearUvPathOverride();
+ ClearUvxPathOverride();
return;
}
if (!File.Exists(path))
{
- throw new ArgumentException("The selected UV executable does not exist");
+ throw new ArgumentException("The selected UVX executable does not exist");
}
- EditorPrefs.SetString(UvPathOverrideKey, path);
+ EditorPrefs.SetString(UvxPathOverrideKey, path);
}
public void SetClaudeCliPathOverride(string path)
@@ -223,23 +371,40 @@ public void SetClaudeCliPathOverride(string path)
}
EditorPrefs.SetString(ClaudeCliPathOverrideKey, path);
- // Also update the ExecPath helper for backwards compatibility
- ExecPath.SetClaudeCliPath(path);
}
- public void ClearMcpServerOverride()
+ public void ClearUvxPathOverride()
{
- EditorPrefs.DeleteKey(PythonDirOverrideKey);
+ EditorPrefs.DeleteKey(UvxPathOverrideKey);
}
- public void ClearUvPathOverride()
+ public void ClearClaudeCliPathOverride()
{
- EditorPrefs.DeleteKey(UvPathOverrideKey);
+ EditorPrefs.DeleteKey(ClaudeCliPathOverrideKey);
}
- public void ClearClaudeCliPathOverride()
+ private static bool VerifyUvxPath(string uvxPath)
{
- EditorPrefs.DeleteKey(ClaudeCliPathOverrideKey);
+ try
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = uvxPath,
+ Arguments = "--version",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+ using var p = Process.Start(psi);
+ string output = p.StandardOutput.ReadToEnd() + p.StandardError.ReadToEnd();
+ p.WaitForExit(2000);
+ return p.ExitCode == 0 && output.Contains("uvx");
+ }
+ catch
+ {
+ return false;
+ }
}
}
}
diff --git a/MCPForUnity/Editor/Services/PythonToolRegistryService.cs b/MCPForUnity/Editor/Services/PythonToolRegistryService.cs
deleted file mode 100644
index 1fab20c8d..000000000
--- a/MCPForUnity/Editor/Services/PythonToolRegistryService.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using UnityEditor;
-using UnityEngine;
-using MCPForUnity.Editor.Data;
-
-namespace MCPForUnity.Editor.Services
-{
- public class PythonToolRegistryService : IPythonToolRegistryService
- {
- public IEnumerable GetAllRegistries()
- {
- // Find all PythonToolsAsset instances in the project
- string[] guids = AssetDatabase.FindAssets("t:PythonToolsAsset");
- foreach (string guid in guids)
- {
- string path = AssetDatabase.GUIDToAssetPath(guid);
- var asset = AssetDatabase.LoadAssetAtPath(path);
- if (asset != null)
- yield return asset;
- }
- }
-
- public bool NeedsSync(PythonToolsAsset registry, TextAsset file)
- {
- if (!registry.useContentHashing) return true;
-
- string currentHash = ComputeHash(file);
- return registry.NeedsSync(file, currentHash);
- }
-
- public void RecordSync(PythonToolsAsset registry, TextAsset file)
- {
- string hash = ComputeHash(file);
- registry.RecordSync(file, hash);
- EditorUtility.SetDirty(registry);
- }
-
- public string ComputeHash(TextAsset file)
- {
- if (file == null || string.IsNullOrEmpty(file.text))
- return string.Empty;
-
- using (var sha256 = SHA256.Create())
- {
- byte[] bytes = System.Text.Encoding.UTF8.GetBytes(file.text);
- byte[] hash = sha256.ComputeHash(bytes);
- return BitConverter.ToString(hash).Replace("-", "").ToLower();
- }
- }
- }
-}
diff --git a/MCPForUnity/Editor/Services/PythonToolRegistryService.cs.meta b/MCPForUnity/Editor/Services/PythonToolRegistryService.cs.meta
deleted file mode 100644
index 9fba1e9f7..000000000
--- a/MCPForUnity/Editor/Services/PythonToolRegistryService.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: 2da2869749c764f16a45e010eefbd679
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs
new file mode 100644
index 000000000..edb4c387e
--- /dev/null
+++ b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+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();
+
+ // Scan all assemblies for [McpForUnityTool] attributes
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies();
+
+ foreach (var assembly in assemblies)
+ {
+ try
+ {
+ var types = assembly.GetTypes();
+
+ foreach (var type in types)
+ {
+ var toolAttr = type.GetCustomAttribute();
+ if (toolAttr == null)
+ continue;
+
+ var metadata = ExtractToolMetadata(type, toolAttr);
+ if (metadata != null)
+ {
+ _cachedTools[metadata.Name] = metadata;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // Skip assemblies that can't be reflected
+ McpLog.Info($"Skipping assembly {assembly.FullName}: {ex.Message}");
+ }
+ }
+
+ McpLog.Info($"Discovered {_cachedTools.Count} MCP tools via reflection");
+ return _cachedTools.Values.ToList();
+ }
+
+ public ToolMetadata GetToolMetadata(string toolName)
+ {
+ if (_cachedTools == null)
+ {
+ DiscoverAllTools();
+ }
+
+ return _cachedTools.TryGetValue(toolName, out var metadata) ? metadata : null;
+ }
+
+ 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);
+
+ return new ToolMetadata
+ {
+ Name = toolName,
+ Description = description,
+ StructuredOutput = toolAttr.StructuredOutput,
+ Parameters = parameters,
+ ClassName = type.Name,
+ Namespace = type.Namespace ?? ""
+ };
+ }
+ 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)
+ {
+ if (string.IsNullOrEmpty(input))
+ return input;
+
+ // Convert PascalCase to snake_case
+ var result = System.Text.RegularExpressions.Regex.Replace(
+ input,
+ "([a-z0-9])([A-Z])",
+ "$1_$2"
+ ).ToLower();
+
+ return result;
+ }
+
+ public void InvalidateCache()
+ {
+ _cachedTools = null;
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs.meta b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs.meta
new file mode 100644
index 000000000..46b740329
--- /dev/null
+++ b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ec81a561be4c14c9cb243855d3273a94
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/ToolSyncService.cs b/MCPForUnity/Editor/Services/ToolSyncService.cs
deleted file mode 100644
index bd17f9966..000000000
--- a/MCPForUnity/Editor/Services/ToolSyncService.cs
+++ /dev/null
@@ -1,134 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using MCPForUnity.Editor.Helpers;
-using UnityEditor;
-
-namespace MCPForUnity.Editor.Services
-{
- public class ToolSyncService : IToolSyncService
- {
- private readonly IPythonToolRegistryService _registryService;
-
- public ToolSyncService(IPythonToolRegistryService registryService = null)
- {
- _registryService = registryService ?? MCPServiceLocator.PythonToolRegistry;
- }
-
- public ToolSyncResult SyncProjectTools(string destToolsDir)
- {
- var result = new ToolSyncResult();
-
- try
- {
- Directory.CreateDirectory(destToolsDir);
-
- // Get all PythonToolsAsset instances in the project
- var registries = _registryService.GetAllRegistries().ToList();
-
- if (!registries.Any())
- {
- McpLog.Info("No PythonToolsAsset found. Create one via Assets > Create > MCP For Unity > Python Tools");
- return result;
- }
-
- var syncedFiles = new HashSet();
-
- // Batch all asset modifications together to minimize reimports
- AssetDatabase.StartAssetEditing();
- try
- {
- foreach (var registry in registries)
- {
- foreach (var file in registry.GetValidFiles())
- {
- try
- {
- // Check if needs syncing (hash-based or always)
- if (_registryService.NeedsSync(registry, file))
- {
- string destPath = Path.Combine(destToolsDir, file.name + ".py");
-
- // Write the Python file content
- File.WriteAllText(destPath, file.text);
-
- // Record sync
- _registryService.RecordSync(registry, file);
-
- result.CopiedCount++;
- syncedFiles.Add(destPath);
- McpLog.Info($"Synced Python tool: {file.name}.py");
- }
- else
- {
- string destPath = Path.Combine(destToolsDir, file.name + ".py");
- syncedFiles.Add(destPath);
- result.SkippedCount++;
- }
- }
- catch (Exception ex)
- {
- result.ErrorCount++;
- result.Messages.Add($"Failed to sync {file.name}: {ex.Message}");
- }
- }
-
- // Cleanup stale states in registry
- registry.CleanupStaleStates();
- EditorUtility.SetDirty(registry);
- }
-
- // Cleanup stale Python files in destination
- CleanupStaleFiles(destToolsDir, syncedFiles);
- }
- finally
- {
- // End batch editing - this triggers a single asset refresh
- AssetDatabase.StopAssetEditing();
- }
-
- // Save all modified registries
- AssetDatabase.SaveAssets();
- }
- catch (Exception ex)
- {
- result.ErrorCount++;
- result.Messages.Add($"Sync failed: {ex.Message}");
- }
-
- return result;
- }
-
- private void CleanupStaleFiles(string destToolsDir, HashSet currentFiles)
- {
- try
- {
- if (!Directory.Exists(destToolsDir)) return;
-
- // Find all .py files in destination that aren't in our current set
- var existingFiles = Directory.GetFiles(destToolsDir, "*.py");
-
- foreach (var file in existingFiles)
- {
- if (!currentFiles.Contains(file))
- {
- try
- {
- File.Delete(file);
- McpLog.Info($"Cleaned up stale tool: {Path.GetFileName(file)}");
- }
- catch (Exception ex)
- {
- McpLog.Warn($"Failed to cleanup {file}: {ex.Message}");
- }
- }
- }
- }
- catch (Exception ex)
- {
- McpLog.Warn($"Failed to cleanup stale files: {ex.Message}");
- }
- }
- }
-}
diff --git a/MCPForUnity/Editor/Services/ToolSyncService.cs.meta b/MCPForUnity/Editor/Services/ToolSyncService.cs.meta
deleted file mode 100644
index 31db4399b..000000000
--- a/MCPForUnity/Editor/Services/ToolSyncService.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: 9ad084cf3b6c04174b9202bf63137bae
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs b/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs
index bb4e0431a..81e964d4d 100644
--- a/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs
+++ b/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs
@@ -3,17 +3,36 @@
namespace MCPForUnity.Editor.Tools
{
///
- /// Marks a class as an MCP tool handler for auto-discovery.
- /// The class must have a public static HandleCommand(JObject) method.
+ /// 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;
+
///
/// 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; }
+ public string CommandName
+ {
+ get => Name;
+ set => Name = value;
+ }
///
/// Create an MCP tool attribute with auto-generated command name.
@@ -22,16 +41,48 @@ public class McpForUnityToolAttribute : Attribute
///
public McpForUnityToolAttribute()
{
- CommandName = null; // Will be auto-generated
+ 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 commandName)
+ /// 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)
{
- CommandName = commandName;
+ Description = description;
}
}
}
diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
index b8fb3c3a5..5c20e7381 100644
--- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
+++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
@@ -16,11 +16,11 @@ namespace MCPForUnity.Editor.Windows
{
public class MCPForUnityEditorWindow : EditorWindow
{
- // Protocol enum for future HTTP support
- private enum ConnectionProtocol
+ // Transport protocol enum
+ private enum TransportProtocol
{
- Stdio,
- // HTTPStreaming // Future
+ HTTP,
+ Stdio
}
// Settings UI Elements
@@ -29,29 +29,23 @@ private enum ConnectionProtocol
private EnumField validationLevelField;
private Label validationDescription;
private Foldout advancedSettingsFoldout;
- private TextField mcpServerPathOverride;
- private TextField uvPathOverride;
- private Button browsePythonButton;
- private Button clearPythonButton;
- private Button browseUvButton;
- private Button clearUvButton;
- private VisualElement mcpServerPathStatus;
- private VisualElement uvPathStatus;
+ private TextField uvxPathOverride;
+ private Button browseUvxButton;
+ private Button clearUvxButton;
+ private VisualElement uvxPathStatus;
// Connection UI Elements
- private EnumField protocolDropdown;
+ private EnumField transportDropdown;
+ private VisualElement httpUrlRow;
+ private TextField httpUrlField;
+ private VisualElement unitySocketPortRow;
private TextField unityPortField;
- private TextField serverPortField;
private VisualElement statusIndicator;
private Label connectionStatusLabel;
private Button connectionToggleButton;
private VisualElement healthIndicator;
private Label healthStatusLabel;
private Button testConnectionButton;
- private VisualElement serverStatusBanner;
- private Label serverStatusMessage;
- private Button downloadServerButton;
- private Button rebuildServerButton;
// Client UI Elements
private DropdownField clientDropdown;
@@ -128,7 +122,6 @@ public void CreateGUI()
// Initial update
UpdateConnectionStatus();
- UpdateServerStatusBanner();
UpdateClientStatus();
UpdatePathOverrides();
// Technically not required to connect, but if we don't do this, the UI will be blank
@@ -197,29 +190,23 @@ private void CacheUIElements()
validationLevelField = rootVisualElement.Q("validation-level");
validationDescription = rootVisualElement.Q
public class MCPToolParameterTests
{
- private static void AssertShaderIsSupported(Shader s)
- {
- Assert.IsNotNull(s, "Shader should not be null");
- // Accept common defaults across pipelines
- var name = s.name;
- bool ok = name == "Universal Render Pipeline/Lit"
- || name == "HDRP/Lit"
- || name == "Standard"
- || name == "Unlit/Color";
- Assert.IsTrue(ok, $"Unexpected shader: {name}");
- }
+ private const string TempDir = "Assets/Temp/MCPToolParameterTests";
+ private const string TempLiveDir = "Assets/Temp/LiveTests";
+
+ private static void AssertColorsEqual(Color expected, Color actual, string message)
+ {
+ const float tolerance = 0.001f;
+ Assert.AreEqual(expected.r, actual.r, tolerance, $"{message} - Red component mismatch");
+ Assert.AreEqual(expected.g, actual.g, tolerance, $"{message} - Green component mismatch");
+ Assert.AreEqual(expected.b, actual.b, tolerance, $"{message} - Blue component mismatch");
+ Assert.AreEqual(expected.a, actual.a, tolerance, $"{message} - Alpha component mismatch");
+ }
+
+ private static void AssertShaderIsSupported(Shader s)
+ {
+ Assert.IsNotNull(s, "Shader should not be null");
+ // Accept common defaults across pipelines
+ var name = s.name;
+ bool ok = name == "Universal Render Pipeline/Lit"
+ || name == "HDRP/Lit"
+ || name == "Standard"
+ || name == "Unlit/Color";
+ Assert.IsTrue(ok, $"Unexpected shader: {name}");
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ // Clean up temp directories after each test
+ if (AssetDatabase.IsValidFolder(TempDir))
+ {
+ AssetDatabase.DeleteAsset(TempDir);
+ }
+
+ if (AssetDatabase.IsValidFolder(TempLiveDir))
+ {
+ AssetDatabase.DeleteAsset(TempLiveDir);
+ }
+
+ // Clean up parent Temp folder if it's empty
+ if (AssetDatabase.IsValidFolder("Assets/Temp"))
+ {
+ var remainingDirs = Directory.GetDirectories("Assets/Temp");
+ var remainingFiles = Directory.GetFiles("Assets/Temp");
+ if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
+ {
+ AssetDatabase.DeleteAsset("Assets/Temp");
+ }
+ }
+ }
[Test]
public void Test_ManageAsset_ShouldAcceptJSONProperties()
{
// Arrange: create temp folder
- const string tempDir = "Assets/Temp/MCPToolParameterTests";
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
{
AssetDatabase.CreateFolder("Assets", "Temp");
}
- if (!AssetDatabase.IsValidFolder(tempDir))
+ if (!AssetDatabase.IsValidFolder(TempDir))
{
AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
}
- var matPath = $"{tempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
+ var matPath = $"{TempDir}/JsonMat_{Guid.NewGuid().ToString("N")}.mat";
// Build params with properties as a JSON string (to be coerced)
var p = new JObject
@@ -61,9 +99,9 @@ public void Test_ManageAsset_ShouldAcceptJSONProperties()
Assert.IsNotNull(result, "Handler should return a JObject result");
Assert.IsTrue(result!.Value("success"), result.ToString());
- var mat = AssetDatabase.LoadAssetAtPath(matPath);
- Assert.IsNotNull(mat, "Material should be created at path");
- AssertShaderIsSupported(mat.shader);
+ var mat = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.IsNotNull(mat, "Material should be created at path");
+ AssertShaderIsSupported(mat.shader);
if (mat.HasProperty("_Color"))
{
Assert.AreEqual(Color.blue, mat.GetColor("_Color"));
@@ -93,7 +131,7 @@ public void Test_ManageGameObject_ShouldAcceptJSONComponentProperties()
["action"] = "create",
["path"] = matPath,
["assetType"] = "Material",
- ["properties"] = new JObject { ["shader"] = "Universal Render Pipeline/Lit", ["color"] = new JArray(0,0,1,1) }
+ ["properties"] = new JObject { ["shader"] = "Universal Render Pipeline/Lit", ["color"] = new JArray(0, 0, 1, 1) }
};
var createMatRes = ManageAsset.HandleCommand(createMat);
var createMatObj = createMatRes as JObject ?? JObject.FromObject(createMatRes);
@@ -128,7 +166,7 @@ public void Test_ManageGameObject_ShouldAcceptJSONComponentProperties()
Assert.IsNotNull(renderer, "MeshRenderer should exist");
var assignedMat = renderer.sharedMaterial;
Assert.IsNotNull(assignedMat, "sharedMaterial should be assigned");
- AssertShaderIsSupported(assignedMat.shader);
+ AssertShaderIsSupported(assignedMat.shader);
var createdMat = AssetDatabase.LoadAssetAtPath(matPath);
Assert.AreEqual(createdMat, assignedMat, "Assigned material should match created material");
}
@@ -185,7 +223,7 @@ public void Test_JSONParsing_ShouldWorkInMCPTools()
// Verify shader on created material
var createdMat = AssetDatabase.LoadAssetAtPath(matPath);
Assert.IsNotNull(createdMat);
- AssertShaderIsSupported(createdMat.shader);
+ AssertShaderIsSupported(createdMat.shader);
}
finally
{
@@ -218,9 +256,9 @@ public void Test_ManageAsset_JSONStringParsing_CreateMaterial()
Assert.IsNotNull(result, "Handler should return a JObject result");
Assert.IsTrue(result!.Value("success"), $"Create failed: {result}");
- var mat = AssetDatabase.LoadAssetAtPath(matPath);
- Assert.IsNotNull(mat, "Material should be created at path");
- AssertShaderIsSupported(mat.shader);
+ var mat = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.IsNotNull(mat, "Material should be created at path");
+ AssertShaderIsSupported(mat.shader);
if (mat.HasProperty("_Color"))
{
Assert.AreEqual(Color.red, mat.GetColor("_Color"), "Material should have red color");
@@ -271,9 +309,9 @@ public void Test_ManageAsset_JSONStringParsing_ModifyMaterial()
Assert.IsNotNull(modifyResult, "Modify should return a result");
Assert.IsTrue(modifyResult!.Value("success"), $"Modify failed: {modifyResult}");
- var mat = AssetDatabase.LoadAssetAtPath(matPath);
- Assert.IsNotNull(mat, "Material should exist");
- AssertShaderIsSupported(mat.shader);
+ var mat = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.IsNotNull(mat, "Material should exist");
+ AssertShaderIsSupported(mat.shader);
if (mat.HasProperty("_Color"))
{
Assert.AreEqual(Color.blue, mat.GetColor("_Color"), "Material should have blue color after modify");
@@ -304,412 +342,414 @@ public void Test_ManageAsset_InvalidJSONString_HandledGracefully()
["properties"] = "{\"invalid\": json, \"missing\": quotes}" // Invalid JSON
};
- try
- {
- LogAssert.Expect(LogType.Warning, new Regex("(failed to parse)|(Could not parse 'properties' JSON string)", RegexOptions.IgnoreCase));
- var raw = ManageAsset.HandleCommand(p);
- var result = raw as JObject ?? JObject.FromObject(raw);
- // Should either succeed with defaults or fail gracefully
- Assert.IsNotNull(result, "Handler should return a result");
- // The result might be success (with defaults) or failure, both are acceptable
- }
- finally
- {
- if (AssetDatabase.LoadAssetAtPath(matPath) != null)
- {
- AssetDatabase.DeleteAsset(matPath);
- }
- AssetDatabase.Refresh();
- }
- }
-
- [Test]
- public void Test_ManageAsset_JSONStringParsing_FloatProperty_Metallic_CreateAndModify()
- {
- // Validate float property handling via JSON string for create and modify
- const string tempDir = "Assets/Temp/MCPToolParameterTests";
- if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
- if (!AssetDatabase.IsValidFolder(tempDir)) AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
- var matPath = $"{tempDir}/JsonFloatTest_{Guid.NewGuid().ToString("N")}.mat";
-
- var createParams = new JObject
- {
- ["action"] = "create",
- ["path"] = matPath,
- ["assetType"] = "Material",
- ["properties"] = "{\"shader\": \"Universal Render Pipeline/Lit\", \"metallic\": 0.75}"
- };
-
- try
- {
- var createRaw = ManageAsset.HandleCommand(createParams);
- var createResult = createRaw as JObject ?? JObject.FromObject(createRaw);
- Assert.IsTrue(createResult!.Value("success"), createResult.ToString());
-
- var mat = AssetDatabase.LoadAssetAtPath(matPath);
- Assert.IsNotNull(mat, "Material should be created at path");
- AssertShaderIsSupported(mat.shader);
- if (mat.HasProperty("_Metallic"))
- {
- Assert.AreEqual(0.75f, mat.GetFloat("_Metallic"), 1e-3f, "Metallic should be ~0.75 after create");
- }
-
- var modifyParams = new JObject
- {
- ["action"] = "modify",
- ["path"] = matPath,
- ["properties"] = "{\"metallic\": 0.1}"
- };
-
- var modifyRaw = ManageAsset.HandleCommand(modifyParams);
- var modifyResult = modifyRaw as JObject ?? JObject.FromObject(modifyRaw);
- Assert.IsTrue(modifyResult!.Value("success"), modifyResult.ToString());
-
- var mat2 = AssetDatabase.LoadAssetAtPath(matPath);
- Assert.IsNotNull(mat2, "Material should still exist");
- if (mat2.HasProperty("_Metallic"))
- {
- Assert.AreEqual(0.1f, mat2.GetFloat("_Metallic"), 1e-3f, "Metallic should be ~0.1 after modify");
- }
- }
- finally
- {
- if (AssetDatabase.LoadAssetAtPath(matPath) != null)
- {
- AssetDatabase.DeleteAsset(matPath);
- }
- AssetDatabase.Refresh();
- }
- }
-
- [Test]
- public void Test_ManageAsset_JSONStringParsing_TextureAssignment_CreateAndModify()
- {
- // Uses flexible direct property assignment to set _BaseMap/_MainTex by path
- const string tempDir = "Assets/Temp/MCPToolParameterTests";
- if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
- if (!AssetDatabase.IsValidFolder(tempDir)) AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
- var matPath = $"{tempDir}/JsonTexTest_{Guid.NewGuid().ToString("N")}.mat";
- var texPath = "Assets/Temp/LiveTests/TempBaseTex.asset"; // created by GenTempTex
-
- // Ensure the texture exists BEFORE creating the material so assignment succeeds during create
- var preTex = AssetDatabase.LoadAssetAtPath(texPath);
- if (preTex == null)
- {
- if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
- if (!AssetDatabase.IsValidFolder("Assets/Temp/LiveTests")) AssetDatabase.CreateFolder("Assets/Temp", "LiveTests");
- var tex2D = new Texture2D(4, 4, TextureFormat.RGBA32, false);
- var pixels = new Color[16];
- for (int i = 0; i < pixels.Length; i++) pixels[i] = Color.white;
- tex2D.SetPixels(pixels);
- tex2D.Apply();
- AssetDatabase.CreateAsset(tex2D, texPath);
- AssetDatabase.SaveAssets();
- AssetDatabase.Refresh();
- }
-
- var createParams = new JObject
- {
- ["action"] = "create",
- ["path"] = matPath,
- ["assetType"] = "Material",
- ["properties"] = new JObject
- {
- ["shader"] = "Universal Render Pipeline/Lit",
- ["_BaseMap"] = texPath // resolves to _BaseMap or _MainTex internally
- }
- };
-
- try
- {
- var createRaw = ManageAsset.HandleCommand(createParams);
- var createResult = createRaw as JObject ?? JObject.FromObject(createRaw);
- Assert.IsTrue(createResult!.Value("success"), createResult.ToString());
-
- var mat = AssetDatabase.LoadAssetAtPath(matPath);
- Assert.IsNotNull(mat, "Material should be created at path");
- AssertShaderIsSupported(mat.shader);
- var tex = AssetDatabase.LoadAssetAtPath(texPath);
- if (tex == null)
- {
- // Create a tiny white texture if missing to make the test self-sufficient
- if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
- if (!AssetDatabase.IsValidFolder("Assets/Temp/LiveTests")) AssetDatabase.CreateFolder("Assets/Temp", "LiveTests");
- var tex2D = new Texture2D(4, 4, TextureFormat.RGBA32, false);
- var pixels = new Color[16];
- for (int i = 0; i < pixels.Length; i++) pixels[i] = Color.white;
- tex2D.SetPixels(pixels);
- tex2D.Apply();
- AssetDatabase.CreateAsset(tex2D, texPath);
- AssetDatabase.SaveAssets();
- AssetDatabase.Refresh();
- tex = AssetDatabase.LoadAssetAtPath(texPath);
- }
- Assert.IsNotNull(tex, "Test texture should exist");
- // Verify either _BaseMap or _MainTex holds the texture
- bool hasTexture = (mat.HasProperty("_BaseMap") && mat.GetTexture("_BaseMap") == tex)
- || (mat.HasProperty("_MainTex") && mat.GetTexture("_MainTex") == tex);
- Assert.IsTrue(hasTexture, "Material should reference the assigned texture");
-
- // Modify by changing to same texture via alternate alias key
- var modifyParams = new JObject
- {
- ["action"] = "modify",
- ["path"] = matPath,
- ["properties"] = new JObject { ["_MainTex"] = texPath }
- };
- var modifyRaw = ManageAsset.HandleCommand(modifyParams);
- var modifyResult = modifyRaw as JObject ?? JObject.FromObject(modifyRaw);
- Assert.IsTrue(modifyResult!.Value("success"), modifyResult.ToString());
-
- var mat2 = AssetDatabase.LoadAssetAtPath(matPath);
- Assert.IsNotNull(mat2);
- bool hasTexture2 = (mat2.HasProperty("_BaseMap") && mat2.GetTexture("_BaseMap") == tex)
- || (mat2.HasProperty("_MainTex") && mat2.GetTexture("_MainTex") == tex);
- Assert.IsTrue(hasTexture2, "Material should keep the assigned texture after modify");
- }
- finally
- {
- if (AssetDatabase.LoadAssetAtPath(matPath) != null)
- {
- AssetDatabase.DeleteAsset(matPath);
- }
- AssetDatabase.Refresh();
- }
- }
-
- [Test]
- public void Test_EndToEnd_PropertyHandling_AllScenarios()
- {
- // Comprehensive end-to-end test of all 10 property handling scenarios
- const string tempDir = "Assets/Temp/LiveTests";
- if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
- if (!AssetDatabase.IsValidFolder(tempDir)) AssetDatabase.CreateFolder("Assets/Temp", "LiveTests");
-
- string guidSuffix = Guid.NewGuid().ToString("N").Substring(0, 8);
- string matPath = $"{tempDir}/Mat_{guidSuffix}.mat";
- string texPath = $"{tempDir}/TempBaseTex.asset";
- string sphereName = $"LiveSphere_{guidSuffix}";
- string badJsonPath = $"{tempDir}/BadJson_{guidSuffix}.mat";
-
- // Ensure clean state from previous runs
- var preSphere = GameObject.Find(sphereName);
- if (preSphere != null) UnityEngine.Object.DestroyImmediate(preSphere);
- if (AssetDatabase.LoadAssetAtPath(matPath) != null) AssetDatabase.DeleteAsset(matPath);
- if (AssetDatabase.LoadAssetAtPath(badJsonPath) != null) AssetDatabase.DeleteAsset(badJsonPath);
- AssetDatabase.Refresh();
-
- try
- {
- // 1. Create material via JSON string
- var createParams = new JObject
- {
- ["action"] = "create",
- ["path"] = matPath,
- ["assetType"] = "Material",
- ["properties"] = "{\"shader\":\"Universal Render Pipeline/Lit\",\"color\":[1,0,0,1]}"
- };
- var createRaw = ManageAsset.HandleCommand(createParams);
- var createResult = createRaw as JObject ?? JObject.FromObject(createRaw);
- Assert.IsTrue(createResult.Value("success"), $"Test 1 failed: {createResult}");
- var mat = AssetDatabase.LoadAssetAtPath(matPath);
- Assert.IsNotNull(mat, "Material should be created");
- var expectedRed = Color.red;
- if (mat.HasProperty("_BaseColor"))
- Assert.AreEqual(expectedRed, mat.GetColor("_BaseColor"), "Test 1: _BaseColor should be red");
- else if (mat.HasProperty("_Color"))
- Assert.AreEqual(expectedRed, mat.GetColor("_Color"), "Test 1: _Color should be red");
- else
- Assert.Inconclusive("Material has neither _BaseColor nor _Color");
-
- // 2. Modify color and metallic (friendly names)
- var modify1 = new JObject
- {
- ["action"] = "modify",
- ["path"] = matPath,
- ["properties"] = "{\"color\":[0,0.5,1,1],\"metallic\":0.6}"
- };
- var modifyRaw1 = ManageAsset.HandleCommand(modify1);
- var modifyResult1 = modifyRaw1 as JObject ?? JObject.FromObject(modifyRaw1);
- Assert.IsTrue(modifyResult1.Value("success"), $"Test 2 failed: {modifyResult1}");
- mat = AssetDatabase.LoadAssetAtPath(matPath);
- var expectedCyan = new Color(0, 0.5f, 1, 1);
- if (mat.HasProperty("_BaseColor"))
- Assert.AreEqual(expectedCyan, mat.GetColor("_BaseColor"), "Test 2: _BaseColor should be cyan");
- else if (mat.HasProperty("_Color"))
- Assert.AreEqual(expectedCyan, mat.GetColor("_Color"), "Test 2: _Color should be cyan");
- else
- Assert.Inconclusive("Material has neither _BaseColor nor _Color");
- Assert.AreEqual(0.6f, mat.GetFloat("_Metallic"), 0.001f, "Test 2: Metallic should be 0.6");
-
- // 3. Modify using structured float block
- var modify2 = new JObject
- {
- ["action"] = "modify",
- ["path"] = matPath,
- ["properties"] = new JObject
- {
- ["float"] = new JObject { ["name"] = "_Metallic", ["value"] = 0.1 }
- }
- };
- var modifyRaw2 = ManageAsset.HandleCommand(modify2);
- var modifyResult2 = modifyRaw2 as JObject ?? JObject.FromObject(modifyRaw2);
- Assert.IsTrue(modifyResult2.Value("success"), $"Test 3 failed: {modifyResult2}");
- mat = AssetDatabase.LoadAssetAtPath(matPath);
- Assert.AreEqual(0.1f, mat.GetFloat("_Metallic"), 0.001f, "Test 3: Metallic should be 0.1");
-
- // 4. Assign texture via direct prop alias (skip if texture doesn't exist)
- if (AssetDatabase.LoadAssetAtPath(texPath) != null)
- {
- var modify3 = new JObject
- {
- ["action"] = "modify",
- ["path"] = matPath,
- ["properties"] = "{\"_BaseMap\":\"" + texPath + "\"}"
- };
- var modifyRaw3 = ManageAsset.HandleCommand(modify3);
- var modifyResult3 = modifyRaw3 as JObject ?? JObject.FromObject(modifyRaw3);
- Assert.IsTrue(modifyResult3.Value("success"), $"Test 4 failed: {modifyResult3}");
- Debug.Log("Test 4: Texture assignment successful");
- }
- else
- {
- Debug.LogWarning("Test 4: Skipped - texture not found at " + texPath);
- }
-
- // 5. Assign texture via structured block (skip if texture doesn't exist)
- if (AssetDatabase.LoadAssetAtPath(texPath) != null)
- {
- var modify4 = new JObject
- {
- ["action"] = "modify",
- ["path"] = matPath,
- ["properties"] = new JObject
- {
- ["texture"] = new JObject { ["name"] = "_MainTex", ["path"] = texPath }
- }
- };
- var modifyRaw4 = ManageAsset.HandleCommand(modify4);
- var modifyResult4 = modifyRaw4 as JObject ?? JObject.FromObject(modifyRaw4);
- Assert.IsTrue(modifyResult4.Value("success"), $"Test 5 failed: {modifyResult4}");
- Debug.Log("Test 5: Structured texture assignment successful");
- }
- else
- {
- Debug.LogWarning("Test 5: Skipped - texture not found at " + texPath);
- }
-
- // 6. Create sphere and assign material via componentProperties JSON string
- var createSphere = new JObject
- {
- ["action"] = "create",
- ["name"] = sphereName,
- ["primitiveType"] = "Sphere"
- };
- var sphereRaw = ManageGameObject.HandleCommand(createSphere);
- var sphereResult = sphereRaw as JObject ?? JObject.FromObject(sphereRaw);
- Assert.IsTrue(sphereResult.Value("success"), $"Test 6 - Create sphere failed: {sphereResult}");
-
- var modifySphere = new JObject
- {
- ["action"] = "modify",
- ["target"] = sphereName,
- ["searchMethod"] = "by_name",
- ["componentProperties"] = "{\"MeshRenderer\":{\"sharedMaterial\":\"" + matPath + "\"}}"
- };
- var sphereModifyRaw = ManageGameObject.HandleCommand(modifySphere);
- var sphereModifyResult = sphereModifyRaw as JObject ?? JObject.FromObject(sphereModifyRaw);
- Assert.IsTrue(sphereModifyResult.Value("success"), $"Test 6 - Assign material failed: {sphereModifyResult}");
- var sphere = GameObject.Find(sphereName);
- Assert.IsNotNull(sphere, "Test 6: Sphere should exist");
- var renderer = sphere.GetComponent();
- Assert.IsNotNull(renderer.sharedMaterial, "Test 6: Material should be assigned");
-
- // 7. Use URP color alias key
- var modify5 = new JObject
- {
- ["action"] = "modify",
- ["path"] = matPath,
- ["properties"] = new JObject
- {
- ["_BaseColor"] = new JArray(0.2, 0.8, 0.3, 1)
- }
- };
- var modifyRaw5 = ManageAsset.HandleCommand(modify5);
- var modifyResult5 = modifyRaw5 as JObject ?? JObject.FromObject(modifyRaw5);
- Assert.IsTrue(modifyResult5.Value("success"), $"Test 7 failed: {modifyResult5}");
- mat = AssetDatabase.LoadAssetAtPath(matPath);
- Color expectedColor = new Color(0.2f, 0.8f, 0.3f, 1f);
- if (mat.HasProperty("_BaseColor"))
- {
- Assert.AreEqual(expectedColor, mat.GetColor("_BaseColor"), "Test 7: _BaseColor should be set");
- }
- else if (mat.HasProperty("_Color"))
- {
- Assert.AreEqual(expectedColor, mat.GetColor("_Color"), "Test 7: Fallback _Color should be set");
- }
-
- // 8. Invalid JSON should warn (don't fail)
- var invalidJson = new JObject
- {
- ["action"] = "create",
- ["path"] = badJsonPath,
- ["assetType"] = "Material",
- ["properties"] = "{\"invalid\": json, \"missing\": quotes}"
- };
- LogAssert.Expect(LogType.Warning, new Regex("(failed to parse)|(Could not parse 'properties' JSON string)", RegexOptions.IgnoreCase));
- var invalidRaw = ManageAsset.HandleCommand(invalidJson);
- var invalidResult = invalidRaw as JObject ?? JObject.FromObject(invalidRaw);
- // Should either succeed with defaults or fail gracefully
- Assert.IsNotNull(invalidResult, "Test 8: Should return a result");
- Debug.Log($"Test 8: Invalid JSON handled - {invalidResult!["success"]}");
-
- // 9. Switch shader pipeline dynamically
- var modify6 = new JObject
- {
- ["action"] = "modify",
- ["path"] = matPath,
- ["properties"] = "{\"shader\":\"Standard\",\"color\":[1,1,0,1]}"
- };
- var modifyRaw6 = ManageAsset.HandleCommand(modify6);
- var modifyResult6 = modifyRaw6 as JObject ?? JObject.FromObject(modifyRaw6);
- Assert.IsTrue(modifyResult6.Value("success"), $"Test 9 failed: {modifyResult6}");
- mat = AssetDatabase.LoadAssetAtPath(matPath);
- Assert.AreEqual("Standard", mat.shader.name, "Test 9: Shader should be Standard");
- var c9 = mat.GetColor("_Color");
- Assert.IsTrue(Mathf.Abs(c9.r - 1f) < 0.02f && Mathf.Abs(c9.g - 1f) < 0.02f && Mathf.Abs(c9.b - 0f) < 0.02f, "Test 9: Color should be near yellow");
-
- // 10. Mixed friendly and alias keys in one go
- var modify7 = new JObject
- {
- ["action"] = "modify",
- ["path"] = matPath,
- ["properties"] = new JObject
- {
- ["metallic"] = 0.8,
- ["smoothness"] = 0.3,
- ["albedo"] = texPath // Texture path if exists
- }
- };
- var modifyRaw7 = ManageAsset.HandleCommand(modify7);
- var modifyResult7 = modifyRaw7 as JObject ?? JObject.FromObject(modifyRaw7);
- Assert.IsTrue(modifyResult7.Value("success"), $"Test 10 failed: {modifyResult7}");
- mat = AssetDatabase.LoadAssetAtPath(matPath);
- Assert.AreEqual(0.8f, mat.GetFloat("_Metallic"), 0.001f, "Test 10: Metallic should be 0.8");
- Assert.AreEqual(0.3f, mat.GetFloat("_Glossiness"), 0.001f, "Test 10: Smoothness should be 0.3");
-
- Debug.Log("All 10 end-to-end property handling tests completed successfully!");
- }
- finally
- {
- // Cleanup
- var sphere = GameObject.Find(sphereName);
- if (sphere != null) UnityEngine.Object.DestroyImmediate(sphere);
- if (AssetDatabase.LoadAssetAtPath(matPath) != null) AssetDatabase.DeleteAsset(matPath);
- if (AssetDatabase.LoadAssetAtPath(badJsonPath) != null) AssetDatabase.DeleteAsset(badJsonPath);
- AssetDatabase.Refresh();
- }
- }
+ try
+ {
+ LogAssert.Expect(LogType.Warning, new Regex("(failed to parse)|(Could not parse 'properties' JSON string)", RegexOptions.IgnoreCase));
+ var raw = ManageAsset.HandleCommand(p);
+ var result = raw as JObject ?? JObject.FromObject(raw);
+ // Should either succeed with defaults or fail gracefully
+ Assert.IsNotNull(result, "Handler should return a result");
+ // The result might be success (with defaults) or failure, both are acceptable
+ }
+ finally
+ {
+ if (AssetDatabase.LoadAssetAtPath(matPath) != null)
+ {
+ AssetDatabase.DeleteAsset(matPath);
+ }
+ AssetDatabase.Refresh();
+ }
+ }
+
+ [Test]
+ public void Test_ManageAsset_JSONStringParsing_FloatProperty_Metallic_CreateAndModify()
+ {
+ // Validate float property handling via JSON string for create and modify
+ const string tempDir = "Assets/Temp/MCPToolParameterTests";
+ if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
+ if (!AssetDatabase.IsValidFolder(tempDir)) AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
+ var matPath = $"{tempDir}/JsonFloatTest_{Guid.NewGuid().ToString("N")}.mat";
+
+ var createParams = new JObject
+ {
+ ["action"] = "create",
+ ["path"] = matPath,
+ ["assetType"] = "Material",
+ ["properties"] = "{\"shader\": \"Universal Render Pipeline/Lit\", \"metallic\": 0.75}"
+ };
+
+ try
+ {
+ var createRaw = ManageAsset.HandleCommand(createParams);
+ var createResult = createRaw as JObject ?? JObject.FromObject(createRaw);
+ Assert.IsTrue(createResult!.Value("success"), createResult.ToString());
+
+ var mat = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.IsNotNull(mat, "Material should be created at path");
+ AssertShaderIsSupported(mat.shader);
+ if (mat.HasProperty("_Metallic"))
+ {
+ Assert.AreEqual(0.75f, mat.GetFloat("_Metallic"), 1e-3f, "Metallic should be ~0.75 after create");
+ }
+
+ var modifyParams = new JObject
+ {
+ ["action"] = "modify",
+ ["path"] = matPath,
+ ["properties"] = "{\"metallic\": 0.1}"
+ };
+
+ var modifyRaw = ManageAsset.HandleCommand(modifyParams);
+ var modifyResult = modifyRaw as JObject ?? JObject.FromObject(modifyRaw);
+ Assert.IsTrue(modifyResult!.Value("success"), modifyResult.ToString());
+
+ var mat2 = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.IsNotNull(mat2, "Material should still exist");
+ if (mat2.HasProperty("_Metallic"))
+ {
+ Assert.AreEqual(0.1f, mat2.GetFloat("_Metallic"), 1e-3f, "Metallic should be ~0.1 after modify");
+ }
+ }
+ finally
+ {
+ if (AssetDatabase.LoadAssetAtPath(matPath) != null)
+ {
+ AssetDatabase.DeleteAsset(matPath);
+ }
+ AssetDatabase.Refresh();
+ }
+ }
+
+ [Test]
+ public void Test_ManageAsset_JSONStringParsing_TextureAssignment_CreateAndModify()
+ {
+ // Uses flexible direct property assignment to set _BaseMap/_MainTex by path
+ const string tempDir = "Assets/Temp/MCPToolParameterTests";
+ if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
+ if (!AssetDatabase.IsValidFolder(tempDir)) AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
+ var matPath = $"{tempDir}/JsonTexTest_{Guid.NewGuid().ToString("N")}.mat";
+ var texPath = "Assets/Temp/LiveTests/TempBaseTex.asset"; // created by GenTempTex
+
+ // Ensure the texture exists BEFORE creating the material so assignment succeeds during create
+ var preTex = AssetDatabase.LoadAssetAtPath(texPath);
+ if (preTex == null)
+ {
+ if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
+ if (!AssetDatabase.IsValidFolder("Assets/Temp/LiveTests")) AssetDatabase.CreateFolder("Assets/Temp", "LiveTests");
+ var tex2D = new Texture2D(4, 4, TextureFormat.RGBA32, false);
+ var pixels = new Color[16];
+ for (int i = 0; i < pixels.Length; i++) pixels[i] = Color.white;
+ tex2D.SetPixels(pixels);
+ tex2D.Apply();
+ AssetDatabase.CreateAsset(tex2D, texPath);
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+ }
+
+ var createParams = new JObject
+ {
+ ["action"] = "create",
+ ["path"] = matPath,
+ ["assetType"] = "Material",
+ ["properties"] = new JObject
+ {
+ ["shader"] = "Universal Render Pipeline/Lit",
+ ["_BaseMap"] = texPath // resolves to _BaseMap or _MainTex internally
+ }
+ };
+
+ try
+ {
+ var createRaw = ManageAsset.HandleCommand(createParams);
+ var createResult = createRaw as JObject ?? JObject.FromObject(createRaw);
+ Assert.IsTrue(createResult!.Value("success"), createResult.ToString());
+
+ var mat = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.IsNotNull(mat, "Material should be created at path");
+ AssertShaderIsSupported(mat.shader);
+ var tex = AssetDatabase.LoadAssetAtPath(texPath);
+ if (tex == null)
+ {
+ // Create a tiny white texture if missing to make the test self-sufficient
+ if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
+ if (!AssetDatabase.IsValidFolder("Assets/Temp/LiveTests")) AssetDatabase.CreateFolder("Assets/Temp", "LiveTests");
+ var tex2D = new Texture2D(4, 4, TextureFormat.RGBA32, false);
+ var pixels = new Color[16];
+ for (int i = 0; i < pixels.Length; i++) pixels[i] = Color.white;
+ tex2D.SetPixels(pixels);
+ tex2D.Apply();
+ AssetDatabase.CreateAsset(tex2D, texPath);
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+ tex = AssetDatabase.LoadAssetAtPath(texPath);
+ }
+ Assert.IsNotNull(tex, "Test texture should exist");
+ // Verify either _BaseMap or _MainTex holds the texture
+ bool hasTexture = (mat.HasProperty("_BaseMap") && mat.GetTexture("_BaseMap") == tex)
+ || (mat.HasProperty("_MainTex") && mat.GetTexture("_MainTex") == tex);
+ Assert.IsTrue(hasTexture, "Material should reference the assigned texture");
+
+ // Modify by changing to same texture via alternate alias key
+ var modifyParams = new JObject
+ {
+ ["action"] = "modify",
+ ["path"] = matPath,
+ ["properties"] = new JObject { ["_MainTex"] = texPath }
+ };
+ var modifyRaw = ManageAsset.HandleCommand(modifyParams);
+ var modifyResult = modifyRaw as JObject ?? JObject.FromObject(modifyRaw);
+ Assert.IsTrue(modifyResult!.Value("success"), modifyResult.ToString());
+
+ var mat2 = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.IsNotNull(mat2);
+ bool hasTexture2 = (mat2.HasProperty("_BaseMap") && mat2.GetTexture("_BaseMap") == tex)
+ || (mat2.HasProperty("_MainTex") && mat2.GetTexture("_MainTex") == tex);
+ Assert.IsTrue(hasTexture2, "Material should keep the assigned texture after modify");
+ }
+ finally
+ {
+ if (AssetDatabase.LoadAssetAtPath(matPath) != null)
+ {
+ AssetDatabase.DeleteAsset(matPath);
+ }
+ AssetDatabase.Refresh();
+ }
+ }
+
+ [Test]
+ public void Test_EndToEnd_PropertyHandling_AllScenarios()
+ {
+ // Comprehensive end-to-end test of all 10 property handling scenarios
+ const string tempDir = "Assets/Temp/LiveTests";
+ if (!AssetDatabase.IsValidFolder("Assets/Temp")) AssetDatabase.CreateFolder("Assets", "Temp");
+ if (!AssetDatabase.IsValidFolder(tempDir)) AssetDatabase.CreateFolder("Assets/Temp", "LiveTests");
+
+ string guidSuffix = Guid.NewGuid().ToString("N").Substring(0, 8);
+ string matPath = $"{tempDir}/Mat_{guidSuffix}.mat";
+ string texPath = $"{tempDir}/TempBaseTex.asset";
+ string sphereName = $"LiveSphere_{guidSuffix}";
+ string badJsonPath = $"{tempDir}/BadJson_{guidSuffix}.mat";
+
+ // Ensure clean state from previous runs
+ var preSphere = GameObject.Find(sphereName);
+ if (preSphere != null) UnityEngine.Object.DestroyImmediate(preSphere);
+ if (AssetDatabase.LoadAssetAtPath(matPath) != null) AssetDatabase.DeleteAsset(matPath);
+ if (AssetDatabase.LoadAssetAtPath(badJsonPath) != null) AssetDatabase.DeleteAsset(badJsonPath);
+ AssetDatabase.Refresh();
+
+ try
+ {
+ // 1. Create material via JSON string
+ var createParams = new JObject
+ {
+ ["action"] = "create",
+ ["path"] = matPath,
+ ["assetType"] = "Material",
+ ["properties"] = "{\"shader\":\"Universal Render Pipeline/Lit\",\"color\":[1,0,0,1]}"
+ };
+ var createRaw = ManageAsset.HandleCommand(createParams);
+ var createResult = createRaw as JObject ?? JObject.FromObject(createRaw);
+ Assert.IsTrue(createResult.Value("success"), $"Test 1 failed: {createResult}");
+ var mat = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.IsNotNull(mat, "Material should be created");
+ var expectedRed = Color.red;
+ if (mat.HasProperty("_BaseColor"))
+ Assert.AreEqual(expectedRed, mat.GetColor("_BaseColor"), "Test 1: _BaseColor should be red");
+ else if (mat.HasProperty("_Color"))
+ Assert.AreEqual(expectedRed, mat.GetColor("_Color"), "Test 1: _Color should be red");
+ else
+ Assert.Inconclusive("Material has neither _BaseColor nor _Color");
+
+ // 2. Modify color and metallic (friendly names)
+ var modify1 = new JObject
+ {
+ ["action"] = "modify",
+ ["path"] = matPath,
+ ["properties"] = "{\"color\":[0,0.5,1,1],\"metallic\":0.6}"
+ };
+ var modifyRaw1 = ManageAsset.HandleCommand(modify1);
+ var modifyResult1 = modifyRaw1 as JObject ?? JObject.FromObject(modifyRaw1);
+ Assert.IsTrue(modifyResult1.Value("success"), $"Test 2 failed: {modifyResult1}");
+ mat = AssetDatabase.LoadAssetAtPath(matPath);
+ var expectedCyan = new Color(0, 0.5f, 1, 1);
+ if (mat.HasProperty("_BaseColor"))
+ Assert.AreEqual(expectedCyan, mat.GetColor("_BaseColor"), "Test 2: _BaseColor should be cyan");
+ else if (mat.HasProperty("_Color"))
+ Assert.AreEqual(expectedCyan, mat.GetColor("_Color"), "Test 2: _Color should be cyan");
+ else
+ Assert.Inconclusive("Material has neither _BaseColor nor _Color");
+ Assert.AreEqual(0.6f, mat.GetFloat("_Metallic"), 0.001f, "Test 2: Metallic should be 0.6");
+
+ // 3. Modify using structured float block
+ var modify2 = new JObject
+ {
+ ["action"] = "modify",
+ ["path"] = matPath,
+ ["properties"] = new JObject
+ {
+ ["float"] = new JObject { ["name"] = "_Metallic", ["value"] = 0.1 }
+ }
+ };
+ var modifyRaw2 = ManageAsset.HandleCommand(modify2);
+ var modifyResult2 = modifyRaw2 as JObject ?? JObject.FromObject(modifyRaw2);
+ Assert.IsTrue(modifyResult2.Value("success"), $"Test 3 failed: {modifyResult2}");
+ mat = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.AreEqual(0.1f, mat.GetFloat("_Metallic"), 0.001f, "Test 3: Metallic should be 0.1");
+
+ // 4. Assign texture via direct prop alias (skip if texture doesn't exist)
+ if (AssetDatabase.LoadAssetAtPath(texPath) != null)
+ {
+ var modify3 = new JObject
+ {
+ ["action"] = "modify",
+ ["path"] = matPath,
+ ["properties"] = "{\"_BaseMap\":\"" + texPath + "\"}"
+ };
+ var modifyRaw3 = ManageAsset.HandleCommand(modify3);
+ var modifyResult3 = modifyRaw3 as JObject ?? JObject.FromObject(modifyRaw3);
+ Assert.IsTrue(modifyResult3.Value("success"), $"Test 4 failed: {modifyResult3}");
+ Debug.Log("Test 4: Texture assignment successful");
+ }
+ else
+ {
+ Debug.LogWarning("Test 4: Skipped - texture not found at " + texPath);
+ }
+
+ // 5. Assign texture via structured block (skip if texture doesn't exist)
+ if (AssetDatabase.LoadAssetAtPath(texPath) != null)
+ {
+ var modify4 = new JObject
+ {
+ ["action"] = "modify",
+ ["path"] = matPath,
+ ["properties"] = new JObject
+ {
+ ["texture"] = new JObject { ["name"] = "_MainTex", ["path"] = texPath }
+ }
+ };
+ var modifyRaw4 = ManageAsset.HandleCommand(modify4);
+ var modifyResult4 = modifyRaw4 as JObject ?? JObject.FromObject(modifyRaw4);
+ Assert.IsTrue(modifyResult4.Value("success"), $"Test 5 failed: {modifyResult4}");
+ Debug.Log("Test 5: Structured texture assignment successful");
+ }
+ else
+ {
+ Debug.LogWarning("Test 5: Skipped - texture not found at " + texPath);
+ }
+
+ // 6. Create sphere and assign material via componentProperties JSON string
+ var createSphere = new JObject
+ {
+ ["action"] = "create",
+ ["name"] = sphereName,
+ ["primitiveType"] = "Sphere"
+ };
+ var sphereRaw = ManageGameObject.HandleCommand(createSphere);
+ var sphereResult = sphereRaw as JObject ?? JObject.FromObject(sphereRaw);
+ Assert.IsTrue(sphereResult.Value("success"), $"Test 6 - Create sphere failed: {sphereResult}");
+
+ var modifySphere = new JObject
+ {
+ ["action"] = "modify",
+ ["target"] = sphereName,
+ ["searchMethod"] = "by_name",
+ ["componentProperties"] = "{\"MeshRenderer\":{\"sharedMaterial\":\"" + matPath + "\"}}"
+ };
+ var sphereModifyRaw = ManageGameObject.HandleCommand(modifySphere);
+ var sphereModifyResult = sphereModifyRaw as JObject ?? JObject.FromObject(sphereModifyRaw);
+ Assert.IsTrue(sphereModifyResult.Value("success"), $"Test 6 - Assign material failed: {sphereModifyResult}");
+ var sphere = GameObject.Find(sphereName);
+ Assert.IsNotNull(sphere, "Test 6: Sphere should exist");
+ var renderer = sphere.GetComponent();
+ Assert.IsNotNull(renderer.sharedMaterial, "Test 6: Material should be assigned");
+
+ // 7. Use URP color alias key
+ var modify5 = new JObject
+ {
+ ["action"] = "modify",
+ ["path"] = matPath,
+ ["properties"] = new JObject
+ {
+ ["_BaseColor"] = new JArray(0.2, 0.8, 0.3, 1)
+ }
+ };
+ var modifyRaw5 = ManageAsset.HandleCommand(modify5);
+ var modifyResult5 = modifyRaw5 as JObject ?? JObject.FromObject(modifyRaw5);
+ Assert.IsTrue(modifyResult5.Value("success"), $"Test 7 failed: {modifyResult5}");
+ mat = AssetDatabase.LoadAssetAtPath(matPath);
+ Color expectedColor = new Color(0.2f, 0.8f, 0.3f, 1f);
+ if (mat.HasProperty("_BaseColor"))
+ {
+ Color actualColor = mat.GetColor("_BaseColor");
+ AssertColorsEqual(expectedColor, actualColor, "Test 7: _BaseColor should be set");
+ }
+ else if (mat.HasProperty("_Color"))
+ {
+ Color actualColor = mat.GetColor("_Color");
+ AssertColorsEqual(expectedColor, actualColor, "Test 7: Fallback _Color should be set");
+ }
+
+ // 8. Invalid JSON should warn (don't fail)
+ var invalidJson = new JObject
+ {
+ ["action"] = "create",
+ ["path"] = badJsonPath,
+ ["assetType"] = "Material",
+ ["properties"] = "{\"invalid\": json, \"missing\": quotes}"
+ };
+ LogAssert.Expect(LogType.Warning, new Regex("(failed to parse)|(Could not parse 'properties' JSON string)", RegexOptions.IgnoreCase));
+ var invalidRaw = ManageAsset.HandleCommand(invalidJson);
+ var invalidResult = invalidRaw as JObject ?? JObject.FromObject(invalidRaw);
+ // Should either succeed with defaults or fail gracefully
+ Assert.IsNotNull(invalidResult, "Test 8: Should return a result");
+ Debug.Log($"Test 8: Invalid JSON handled - {invalidResult!["success"]}");
+
+ // 9. Switch shader pipeline dynamically
+ var modify6 = new JObject
+ {
+ ["action"] = "modify",
+ ["path"] = matPath,
+ ["properties"] = "{\"shader\":\"Standard\",\"color\":[1,1,0,1]}"
+ };
+ var modifyRaw6 = ManageAsset.HandleCommand(modify6);
+ var modifyResult6 = modifyRaw6 as JObject ?? JObject.FromObject(modifyRaw6);
+ Assert.IsTrue(modifyResult6.Value("success"), $"Test 9 failed: {modifyResult6}");
+ mat = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.AreEqual("Standard", mat.shader.name, "Test 9: Shader should be Standard");
+ var c9 = mat.GetColor("_Color");
+ Assert.IsTrue(Mathf.Abs(c9.r - 1f) < 0.02f && Mathf.Abs(c9.g - 1f) < 0.02f && Mathf.Abs(c9.b - 0f) < 0.02f, "Test 9: Color should be near yellow");
+
+ // 10. Mixed friendly and alias keys in one go
+ var modify7 = new JObject
+ {
+ ["action"] = "modify",
+ ["path"] = matPath,
+ ["properties"] = new JObject
+ {
+ ["metallic"] = 0.8,
+ ["smoothness"] = 0.3,
+ ["albedo"] = texPath // Texture path if exists
+ }
+ };
+ var modifyRaw7 = ManageAsset.HandleCommand(modify7);
+ var modifyResult7 = modifyRaw7 as JObject ?? JObject.FromObject(modifyRaw7);
+ Assert.IsTrue(modifyResult7.Value("success"), $"Test 10 failed: {modifyResult7}");
+ mat = AssetDatabase.LoadAssetAtPath(matPath);
+ Assert.AreEqual(0.8f, mat.GetFloat("_Metallic"), 0.001f, "Test 10: Metallic should be 0.8");
+ Assert.AreEqual(0.3f, mat.GetFloat("_Glossiness"), 0.001f, "Test 10: Smoothness should be 0.3");
+
+ Debug.Log("All 10 end-to-end property handling tests completed successfully!");
+ }
+ finally
+ {
+ // Cleanup
+ var sphere = GameObject.Find(sphereName);
+ if (sphere != null) UnityEngine.Object.DestroyImmediate(sphere);
+ if (AssetDatabase.LoadAssetAtPath(matPath) != null) AssetDatabase.DeleteAsset(matPath);
+ if (AssetDatabase.LoadAssetAtPath(badJsonPath) != null) AssetDatabase.DeleteAsset(badJsonPath);
+ AssetDatabase.Refresh();
+ }
+ }
}
}
\ No newline at end of file
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs
deleted file mode 100644
index 1c9f71e15..000000000
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using NUnit.Framework;
-using UnityEngine;
-using MCPForUnity.Editor.Data;
-using MCPForUnity.Editor.Services;
-
-namespace MCPForUnityTests.Editor.Services
-{
- public class PythonToolRegistryServiceTests
- {
- private PythonToolRegistryService _service;
-
- [SetUp]
- public void SetUp()
- {
- _service = new PythonToolRegistryService();
- }
-
- [Test]
- public void GetAllRegistries_ReturnsEmptyList_WhenNoPythonToolsAssetsExist()
- {
- var registries = _service.GetAllRegistries().ToList();
-
- // Note: This might find assets in the test project, so we just verify it doesn't throw
- Assert.IsNotNull(registries, "Should return a non-null list");
- }
-
- [Test]
- public void NeedsSync_ReturnsTrue_WhenHashingDisabled()
- {
- var asset = ScriptableObject.CreateInstance();
- asset.useContentHashing = false;
-
- var textAsset = new TextAsset("print('test')");
-
- bool needsSync = _service.NeedsSync(asset, textAsset);
-
- Assert.IsTrue(needsSync, "Should always need sync when hashing is disabled");
-
- Object.DestroyImmediate(asset);
- }
-
- [Test]
- public void NeedsSync_ReturnsTrue_WhenFileNotPreviouslySynced()
- {
- var asset = ScriptableObject.CreateInstance();
- asset.useContentHashing = true;
-
- var textAsset = new TextAsset("print('test')");
-
- bool needsSync = _service.NeedsSync(asset, textAsset);
-
- Assert.IsTrue(needsSync, "Should need sync for new file");
-
- Object.DestroyImmediate(asset);
- }
-
- [Test]
- public void NeedsSync_ReturnsFalse_WhenHashMatches()
- {
- var asset = ScriptableObject.CreateInstance();
- asset.useContentHashing = true;
-
- var textAsset = new TextAsset("print('test')");
-
- // First sync
- _service.RecordSync(asset, textAsset);
-
- // Check if needs sync again
- bool needsSync = _service.NeedsSync(asset, textAsset);
-
- Assert.IsFalse(needsSync, "Should not need sync when hash matches");
-
- Object.DestroyImmediate(asset);
- }
-
- [Test]
- public void RecordSync_StoresFileState()
- {
- var asset = ScriptableObject.CreateInstance();
- var textAsset = new TextAsset("print('test')");
-
- _service.RecordSync(asset, textAsset);
-
- Assert.AreEqual(1, asset.fileStates.Count, "Should have one file state recorded");
- Assert.IsNotNull(asset.fileStates[0].contentHash, "Hash should be stored");
- Assert.IsNotNull(asset.fileStates[0].assetGuid, "GUID should be stored");
-
- Object.DestroyImmediate(asset);
- }
-
- [Test]
- public void RecordSync_UpdatesExistingState_WhenFileAlreadyRecorded()
- {
- var asset = ScriptableObject.CreateInstance();
- var textAsset = new TextAsset("print('test')");
-
- // Record twice
- _service.RecordSync(asset, textAsset);
- var firstHash = asset.fileStates[0].contentHash;
-
- _service.RecordSync(asset, textAsset);
-
- Assert.AreEqual(1, asset.fileStates.Count, "Should still have only one state");
- Assert.AreEqual(firstHash, asset.fileStates[0].contentHash, "Hash should remain the same");
-
- Object.DestroyImmediate(asset);
- }
-
- [Test]
- public void ComputeHash_ReturnsSameHash_ForSameContent()
- {
- var textAsset1 = new TextAsset("print('hello')");
- var textAsset2 = new TextAsset("print('hello')");
-
- string hash1 = _service.ComputeHash(textAsset1);
- string hash2 = _service.ComputeHash(textAsset2);
-
- Assert.AreEqual(hash1, hash2, "Same content should produce same hash");
- }
-
- [Test]
- public void ComputeHash_ReturnsDifferentHash_ForDifferentContent()
- {
- var textAsset1 = new TextAsset("print('hello')");
- var textAsset2 = new TextAsset("print('world')");
-
- string hash1 = _service.ComputeHash(textAsset1);
- string hash2 = _service.ComputeHash(textAsset2);
-
- Assert.AreNotEqual(hash1, hash2, "Different content should produce different hash");
- }
- }
-}
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs.meta
deleted file mode 100644
index b694a93a7..000000000
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PythonToolRegistryServiceTests.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: fb9be9b99beba4112a7e3182df1d1d10
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs
deleted file mode 100644
index a00c88daf..000000000
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs
+++ /dev/null
@@ -1,72 +0,0 @@
-using System.IO;
-using NUnit.Framework;
-using UnityEngine;
-using MCPForUnity.Editor.Data;
-using MCPForUnity.Editor.Services;
-
-namespace MCPForUnityTests.Editor.Services
-{
- public class ToolSyncServiceTests
- {
- private ToolSyncService _service;
- private string _testToolsDir;
-
- [SetUp]
- public void SetUp()
- {
- _service = new ToolSyncService();
- _testToolsDir = Path.Combine(Path.GetTempPath(), "UnityMCPTests", "tools");
-
- // Clean up any existing test directory
- if (Directory.Exists(_testToolsDir))
- {
- Directory.Delete(_testToolsDir, true);
- }
- }
-
- [TearDown]
- public void TearDown()
- {
- // Clean up test directory
- if (Directory.Exists(_testToolsDir))
- {
- try
- {
- Directory.Delete(_testToolsDir, true);
- }
- catch
- {
- // Ignore cleanup errors
- }
- }
- }
-
- [Test]
- public void SyncProjectTools_CreatesDestinationDirectory()
- {
- _service.SyncProjectTools(_testToolsDir);
-
- Assert.IsTrue(Directory.Exists(_testToolsDir), "Should create destination directory");
- }
-
- [Test]
- public void SyncProjectTools_ReturnsSuccess_WhenNoPythonToolsAssets()
- {
- var result = _service.SyncProjectTools(_testToolsDir);
-
- Assert.IsNotNull(result, "Should return a result");
- Assert.AreEqual(0, result.CopiedCount, "Should not copy any files");
- Assert.AreEqual(0, result.ErrorCount, "Should not have errors");
- }
-
- [Test]
- public void SyncProjectTools_ReportsCorrectCounts()
- {
- var result = _service.SyncProjectTools(_testToolsDir);
-
- Assert.IsTrue(result.CopiedCount >= 0, "Copied count should be non-negative");
- Assert.IsTrue(result.SkippedCount >= 0, "Skipped count should be non-negative");
- Assert.IsTrue(result.ErrorCount >= 0, "Error count should be non-negative");
- }
- }
-}
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs.meta
deleted file mode 100644
index a91f013a4..000000000
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolSyncServiceTests.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: b2c3d4e5f67890123456789012345abc
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs
index a562f6677..95c9adaee 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsTests.cs
@@ -23,16 +23,23 @@ public void SetUp()
public void TearDown()
{
StageUtility.GoToMainStage();
- }
-
- [OneTimeTearDown]
- public void CleanupAll()
- {
- StageUtility.GoToMainStage();
+
+ // Clean up temp directory after each test
if (AssetDatabase.IsValidFolder(TempDirectory))
{
AssetDatabase.DeleteAsset(TempDirectory);
}
+
+ // Clean up parent Temp folder if it's empty
+ if (AssetDatabase.IsValidFolder("Assets/Temp"))
+ {
+ var remainingDirs = Directory.GetDirectories("Assets/Temp");
+ var remainingFiles = Directory.GetFiles("Assets/Temp");
+ if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
+ {
+ AssetDatabase.DeleteAsset("Assets/Temp");
+ }
+ }
}
[Test]
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialDirectPropertiesTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialDirectPropertiesTests.cs
index d92f0f8e0..befc9986d 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialDirectPropertiesTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialDirectPropertiesTests.cs
@@ -50,6 +50,24 @@ public void TearDown()
TryDeleteAsset(_baseMapPath);
TryDeleteAsset(_normalMapPath);
TryDeleteAsset(_occlusionMapPath);
+
+ // Clean up temp directory after each test
+ if (AssetDatabase.IsValidFolder(TempRoot))
+ {
+ AssetDatabase.DeleteAsset(TempRoot);
+ }
+
+ // Clean up parent Temp folder if it's empty
+ if (AssetDatabase.IsValidFolder("Assets/Temp"))
+ {
+ var remainingDirs = Directory.GetDirectories("Assets/Temp");
+ var remainingFiles = Directory.GetFiles("Assets/Temp");
+ if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
+ {
+ AssetDatabase.DeleteAsset("Assets/Temp");
+ }
+ }
+
AssetDatabase.Refresh();
}
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs
index 1ae73e0bb..92fc5e672 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/MaterialParameterToolTests.cs
@@ -55,6 +55,24 @@ public void TearDown()
{
AssetDatabase.DeleteAsset(_matPath);
}
+
+ // Clean up temp directory after each test
+ if (AssetDatabase.IsValidFolder(TempRoot))
+ {
+ AssetDatabase.DeleteAsset(TempRoot);
+ }
+
+ // Clean up parent Temp folder if it's empty
+ if (AssetDatabase.IsValidFolder("Assets/Temp"))
+ {
+ var remainingDirs = Directory.GetDirectories("Assets/Temp");
+ var remainingFiles = Directory.GetFiles("Assets/Temp");
+ if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
+ {
+ AssetDatabase.DeleteAsset("Assets/Temp");
+ }
+ }
+
AssetDatabase.Refresh();
}