New UI and work without MCP server embedded (#313)
* First pass at new UI * Point to new UI * Refactor: New Service-Based MCP Editor Window Architecture We separate the business logic from the UI rendering of the new editor window with new services. I didn't go full Dependency Injection, not sure if I want to add those deps to the install as yet, but service location is fairly straightforward. Some differences with the old window: - No more Auto-Setup, users will manually decide what they want to do - Removed Python detection warning, we have a setup wizard now - Added explicit path overrides for `uv` and the MCP server itself * style: add flex-shrink and overflow handling to improve UI element scaling * fix: update UI configuration and visibility when client status changes * feat: add menu item to open legacy MCP configuration window * refactor: improve editor window lifecycle handling with proper update subscription * feat: add auto-verification of bridge health when connected * fix: update Claude Code MCP server registration to use lowercase unityMCP name and correct the manual installation instructions * fix: add Claude CLI directory to PATH for node/nvm environments * Clarify how users will see MCP tools * Add a keyboard shortcut to open the window * feat: add server download UI and improve installation status messaging This is needed for the Unity Asset Store, which doesn't have the Python server embedded. * feat: add dynamic asset path detection to support both Package Manager and Asset Store installations * fix: replace unicode emojis with escaped characters in status messages * feat: add server package creation and GitHub release publishing to version bump workflow * fix: add v prefix to server package filename in release workflow * Fix download location * style: improve dropdown and settings layout responsiveness with flex-shrink and max-width * feat: add package.json version detection and refactor path utilities * refactor: simplify imports and use fully qualified names in ServerInstaller.cs * refactor: replace Unity Debug.Log calls with custom McpLog class * fix: extract server files to temp directory before moving to final location * docs: add v6 UI documentation and screenshots with service architecture overview * docs: add new UI Toolkit-based editor window with service architecture and path overrides * feat: improve package path resolution to support Package Manager and Asset Store installations * Change Claude Code's casing back to "UnityMCP" There's no need to break anything as yet * fix: update success dialog text to clarify manual bridge start requirement * refactor: move RefreshDebounce and ManageScriptRefreshHelpers classes inside namespace * feat: add Asset Store fallback path detection for package root lookup * fix: update server installation success message to be more descriptive * refactor: replace Unity Debug.Log calls with custom McpLog utility * fix: add file existence check before opening configuration file * refactor: simplify asset path handling and remove redundant helper namespace references * docs: update code block syntax highlighting in UI changes doc * docs: add code block syntax highlighting for file structure example * feat: import UnityEditor.UIElements namespace for UI components for Unity 2021 compatibility * refactor: rename Python server references to MCP server for consistency * fix: reset client status label color after error state is cleared * Replace the phrase "Python server" with "MCP server" * MInor doc clarification * docs: add path override methods for UV and Claude CLI executables * docs: update service locator registration method name from SetCustomImplementation to Registermain
parent
2c53943556
commit
1d6d8c67af
|
|
@ -108,3 +108,30 @@ jobs:
|
|||
|
||||
git tag -a "$TAG" -m "Version ${NEW_VERSION}"
|
||||
git push origin "$TAG"
|
||||
|
||||
- name: Package server for release
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd MCPForUnity/UnityMcpServer~
|
||||
zip -r ../../mcp-for-unity-server-v${NEW_VERSION}.zip .
|
||||
cd ../..
|
||||
ls -lh mcp-for-unity-server-v${NEW_VERSION}.zip
|
||||
echo "Server package created: mcp-for-unity-server-v${NEW_VERSION}.zip"
|
||||
|
||||
- name: Create GitHub release with server artifact
|
||||
env:
|
||||
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="v${NEW_VERSION}"
|
||||
|
||||
# Create release
|
||||
gh release create "$TAG" \
|
||||
--title "v${NEW_VERSION}" \
|
||||
--notes "Release v${NEW_VERSION}" \
|
||||
"mcp-for-unity-server-v${NEW_VERSION}.zip#MCP Server v${NEW_VERSION}"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
|
||||
|
||||
namespace MCPForUnity.Editor.Helpers
|
||||
{
|
||||
|
|
@ -25,5 +30,133 @@ namespace MCPForUnity.Editor.Helpers
|
|||
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the MCP for Unity package root path.
|
||||
/// Works for registry Package Manager, local Package Manager, and Asset Store installations.
|
||||
/// </summary>
|
||||
/// <returns>The package root path (virtual for PM, absolute for Asset Store), or null if not found</returns>
|
||||
public static string GetMcpPackageRootPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try Package Manager first (registry and local installs)
|
||||
var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly);
|
||||
if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.assetPath))
|
||||
{
|
||||
return packageInfo.assetPath;
|
||||
}
|
||||
|
||||
// Fallback to AssetDatabase for Asset Store installs (Assets/MCPForUnity)
|
||||
string[] guids = AssetDatabase.FindAssets($"t:Script {nameof(AssetPathUtility)}");
|
||||
|
||||
if (guids.Length == 0)
|
||||
{
|
||||
McpLog.Warn("Could not find AssetPathUtility script in AssetDatabase");
|
||||
return null;
|
||||
}
|
||||
|
||||
string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]);
|
||||
|
||||
// Script is at: {packageRoot}/Editor/Helpers/AssetPathUtility.cs
|
||||
// Extract {packageRoot}
|
||||
int editorIndex = scriptPath.IndexOf("/Editor/", StringComparison.Ordinal);
|
||||
|
||||
if (editorIndex >= 0)
|
||||
{
|
||||
return scriptPath.Substring(0, editorIndex);
|
||||
}
|
||||
|
||||
McpLog.Warn($"Could not determine package root from script path: {scriptPath}");
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"Failed to get package root path: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads and parses the package.json file for MCP for Unity.
|
||||
/// Handles both Package Manager (registry/local) and Asset Store installations.
|
||||
/// </summary>
|
||||
/// <returns>JObject containing package.json data, or null if not found or parse failed</returns>
|
||||
public static JObject GetPackageJson()
|
||||
{
|
||||
try
|
||||
{
|
||||
string packageRoot = GetMcpPackageRootPath();
|
||||
if (string.IsNullOrEmpty(packageRoot))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string packageJsonPath = Path.Combine(packageRoot, "package.json");
|
||||
|
||||
// Convert virtual asset path to file system path
|
||||
if (packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Package Manager install - must use PackageInfo.resolvedPath
|
||||
// Virtual paths like "Packages/..." don't work with File.Exists()
|
||||
// Registry packages live in Library/PackageCache/package@version/
|
||||
var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly);
|
||||
if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.resolvedPath))
|
||||
{
|
||||
packageJsonPath = Path.Combine(packageInfo.resolvedPath, "package.json");
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Warn("Could not resolve Package Manager path for package.json");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (packageRoot.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Asset Store install - convert to absolute file system path
|
||||
// Application.dataPath is the absolute path to the Assets folder
|
||||
string relativePath = packageRoot.Substring("Assets/".Length);
|
||||
packageJsonPath = Path.Combine(Application.dataPath, relativePath, "package.json");
|
||||
}
|
||||
|
||||
if (!File.Exists(packageJsonPath))
|
||||
{
|
||||
McpLog.Warn($"package.json not found at: {packageJsonPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(packageJsonPath);
|
||||
return JObject.Parse(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"Failed to read or parse package.json: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version string from the package.json file.
|
||||
/// </summary>
|
||||
/// <returns>Version string, or "unknown" if not found</returns>
|
||||
public static string GetPackageVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
var packageJson = GetPackageJson();
|
||||
if (packageJson == null)
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
string version = packageJson["version"]?.ToString();
|
||||
return string.IsNullOrEmpty(version) ? "unknown" : version;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"Failed to get package version: {ex.Message}");
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using MCPForUnity.Editor.Helpers;
|
|||
namespace MCPForUnity.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared helper for resolving Python server directory paths with support for
|
||||
/// Shared helper for resolving MCP server directory paths with support for
|
||||
/// development mode, embedded servers, and installed packages
|
||||
/// </summary>
|
||||
public static class McpPathResolver
|
||||
|
|
@ -15,7 +15,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer";
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the Python server directory path with comprehensive logic
|
||||
/// Resolves the MCP server directory path with comprehensive logic
|
||||
/// including development mode support and fallback mechanisms
|
||||
/// </summary>
|
||||
public static string FindPackagePythonDirectory(bool debugLogsEnabled = false)
|
||||
|
|
|
|||
|
|
@ -49,8 +49,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
Debug.LogWarning($"MCP for Unity: Auto-detect on load failed: {capturedEx}");
|
||||
// Alternatively: Debug.LogException(capturedEx);
|
||||
McpLog.Info($"Server check: {error}. Download via Window > MCP For Unity if needed.", always: false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ using UnityEngine;
|
|||
namespace MCPForUnity.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles automatic installation of the Python server when the package is first installed.
|
||||
/// Handles automatic installation of the MCP server when the package is first installed.
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
public static class PackageInstaller
|
||||
|
|
@ -25,18 +25,21 @@ namespace MCPForUnity.Editor.Helpers
|
|||
{
|
||||
try
|
||||
{
|
||||
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Installing Python server...");
|
||||
ServerInstaller.EnsureServerInstalled();
|
||||
|
||||
// Mark as installed
|
||||
// Mark as installed/checked
|
||||
EditorPrefs.SetBool(InstallationFlagKey, true);
|
||||
|
||||
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Python server installation completed successfully.");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
// Only log success if server was actually embedded and copied
|
||||
if (ServerInstaller.HasEmbeddedServer())
|
||||
{
|
||||
Debug.LogError($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Failed to install Python server: {ex.Message}");
|
||||
Debug.LogWarning("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: You may need to manually install the Python server. Check the MCP For Unity Window for instructions.");
|
||||
McpLog.Info("MCP server installation completed successfully.");
|
||||
}
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
EditorPrefs.SetBool(InstallationFlagKey, true); // Mark as handled
|
||||
McpLog.Info("Server installation pending. Open Window > MCP For Unity to download the server.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
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;
|
||||
|
||||
|
|
@ -34,8 +36,19 @@ namespace MCPForUnity.Editor.Helpers
|
|||
// Resolve embedded source and versions
|
||||
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
|
||||
{
|
||||
throw new Exception("Could not find embedded UnityMcpServer/src in the package.");
|
||||
// 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));
|
||||
|
||||
|
|
@ -151,7 +164,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
TryCreateMacSymlinkForAppSupport();
|
||||
return Path.Combine(localAppSupport, RootFolder);
|
||||
}
|
||||
throw new Exception("Unsupported operating system.");
|
||||
throw new Exception("Unsupported operating system");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -177,7 +190,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
if (!Directory.Exists(canonical)) return;
|
||||
|
||||
// Use 'ln -s' to create a directory symlink (macOS)
|
||||
var psi = new System.Diagnostics.ProcessStartInfo
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/ln",
|
||||
Arguments = $"-s \"{canonical}\" \"{symlink}\"",
|
||||
|
|
@ -186,7 +199,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using var p = System.Diagnostics.Process.Start(psi);
|
||||
using var p = Process.Start(psi);
|
||||
p?.WaitForExit(2000);
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
|
|
@ -303,7 +316,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
|
||||
private static IEnumerable<string> GetLegacyRootsForDetection()
|
||||
{
|
||||
var roots = new System.Collections.Generic.List<string>();
|
||||
var roots = new List<string>();
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||
// macOS/Linux legacy
|
||||
roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer"));
|
||||
|
|
@ -331,7 +344,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
if (string.IsNullOrEmpty(serverSrcPath)) return;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
|
||||
|
||||
var psi = new System.Diagnostics.ProcessStartInfo
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/usr/bin/pgrep",
|
||||
Arguments = $"-f \"uv .*--directory {serverSrcPath}\"",
|
||||
|
|
@ -340,7 +353,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using var p = System.Diagnostics.Process.Start(psi);
|
||||
using var p = Process.Start(psi);
|
||||
if (p == null) return;
|
||||
string outp = p.StandardOutput.ReadToEnd();
|
||||
p.WaitForExit(1500);
|
||||
|
|
@ -350,7 +363,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
{
|
||||
if (int.TryParse(line.Trim(), out int pid))
|
||||
{
|
||||
try { System.Diagnostics.Process.GetProcessById(pid).Kill(); } catch { }
|
||||
try { Process.GetProcessById(pid).Kill(); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -430,7 +443,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
// Find embedded source
|
||||
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
|
||||
{
|
||||
Debug.LogError("RebuildMcpServer: Could not find embedded server source.");
|
||||
McpLog.Error("RebuildMcpServer: Could not find embedded server source.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -447,11 +460,11 @@ namespace MCPForUnity.Editor.Helpers
|
|||
try
|
||||
{
|
||||
Directory.Delete(destRoot, recursive: true);
|
||||
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Deleted existing server at {destRoot}");
|
||||
McpLog.Info($"Deleted existing server at {destRoot}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Failed to delete existing server: {ex.Message}");
|
||||
McpLog.Error($"Failed to delete existing server: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -469,15 +482,15 @@ namespace MCPForUnity.Editor.Helpers
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Failed to write version file: {ex.Message}");
|
||||
McpLog.Warn($"Failed to write version file: {ex.Message}");
|
||||
}
|
||||
|
||||
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Server rebuilt successfully at {destRoot} (version {embeddedVer})");
|
||||
McpLog.Info($"Server rebuilt successfully at {destRoot} (version {embeddedVer})");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"RebuildMcpServer failed: {ex.Message}");
|
||||
McpLog.Error($"RebuildMcpServer failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -508,7 +521,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
// Fast path: resolve from PATH first
|
||||
try
|
||||
{
|
||||
var wherePsi = new System.Diagnostics.ProcessStartInfo
|
||||
var wherePsi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "where",
|
||||
Arguments = "uv.exe",
|
||||
|
|
@ -517,7 +530,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using var wp = System.Diagnostics.Process.Start(wherePsi);
|
||||
using var wp = Process.Start(wherePsi);
|
||||
string output = wp.StandardOutput.ReadToEnd().Trim();
|
||||
wp.WaitForExit(1500);
|
||||
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
|
||||
|
|
@ -613,7 +626,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
var whichPsi = new System.Diagnostics.ProcessStartInfo
|
||||
var whichPsi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/usr/bin/which",
|
||||
Arguments = "uv",
|
||||
|
|
@ -628,7 +641,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||
string prepend = string.Join(":", new[]
|
||||
{
|
||||
System.IO.Path.Combine(homeDir, ".local", "bin"),
|
||||
Path.Combine(homeDir, ".local", "bin"),
|
||||
"/opt/homebrew/bin",
|
||||
"/usr/local/bin",
|
||||
"/usr/bin",
|
||||
|
|
@ -638,7 +651,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath);
|
||||
}
|
||||
catch { }
|
||||
using var wp = System.Diagnostics.Process.Start(whichPsi);
|
||||
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))
|
||||
|
|
@ -676,7 +689,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
{
|
||||
try
|
||||
{
|
||||
var psi = new System.Diagnostics.ProcessStartInfo
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = uvPath,
|
||||
Arguments = "--version",
|
||||
|
|
@ -685,7 +698,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using var p = System.Diagnostics.Process.Start(psi);
|
||||
using var p = Process.Start(psi);
|
||||
if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; }
|
||||
if (p.ExitCode == 0)
|
||||
{
|
||||
|
|
@ -696,5 +709,133 @@ namespace MCPForUnity.Editor.Helpers
|
|||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download and install server from GitHub release (Asset Store workflow)
|
||||
/// </summary>
|
||||
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))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(destRoot, recursive: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"Could not fully delete old server: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the package has an embedded server (Git install vs Asset Store)
|
||||
/// </summary>
|
||||
public static bool HasEmbeddedServer()
|
||||
{
|
||||
return TryGetEmbeddedServerSource(out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the installed server version from the local installation
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ namespace MCPForUnity.Editor.Helpers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send telemetry data to Python server for processing
|
||||
/// This is a lightweight bridge - the actual telemetry logic is in Python
|
||||
/// Send telemetry data to MCP server for processing
|
||||
/// This is a lightweight bridge - the actual telemetry logic is in the MCP server
|
||||
/// </summary>
|
||||
public static void RecordEvent(string eventType, Dictionary<string, object> data = null)
|
||||
{
|
||||
|
|
@ -106,16 +106,16 @@ namespace MCPForUnity.Editor.Helpers
|
|||
telemetryData["data"] = data;
|
||||
}
|
||||
|
||||
// Send to Python server via existing bridge communication
|
||||
// The Python server will handle actual telemetry transmission
|
||||
SendTelemetryToPythonServer(telemetryData);
|
||||
// Send to MCP server via existing bridge communication
|
||||
// The MCP server will handle actual telemetry transmission
|
||||
SendTelemetryToMcpServer(telemetryData);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Never let telemetry errors interfere with functionality
|
||||
if (IsDebugEnabled())
|
||||
{
|
||||
Debug.LogWarning($"Telemetry error (non-blocking): {e.Message}");
|
||||
McpLog.Warn($"Telemetry error (non-blocking): {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -183,7 +183,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
RecordEvent("tool_execution_unity", data);
|
||||
}
|
||||
|
||||
private static void SendTelemetryToPythonServer(Dictionary<string, object> telemetryData)
|
||||
private static void SendTelemetryToMcpServer(Dictionary<string, object> telemetryData)
|
||||
{
|
||||
var sender = Volatile.Read(ref s_sender);
|
||||
if (sender != null)
|
||||
|
|
@ -197,7 +197,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
{
|
||||
if (IsDebugEnabled())
|
||||
{
|
||||
Debug.LogWarning($"Telemetry sender error (non-blocking): {e.Message}");
|
||||
McpLog.Warn($"Telemetry sender error (non-blocking): {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
// Fallback: log when debug is enabled
|
||||
if (IsDebugEnabled())
|
||||
{
|
||||
Debug.Log($"<b><color=#2EA3FF>MCP-TELEMETRY</color></b>: {telemetryData["event_type"]}");
|
||||
McpLog.Info($"Telemetry: {telemetryData["event_type"]}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2ab6b1cc527214416b21e07b96164f24
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace MCPForUnity.Editor.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of bridge control service
|
||||
/// </summary>
|
||||
public class BridgeControlService : IBridgeControlService
|
||||
{
|
||||
public bool IsRunning => 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")))
|
||||
{
|
||||
MCPForUnityBridge.StartAutoConnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
MCPForUnityBridge.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
MCPForUnityBridge.Stop();
|
||||
}
|
||||
|
||||
public BridgeVerificationResult Verify(int port)
|
||||
{
|
||||
var result = new BridgeVerificationResult
|
||||
{
|
||||
Success = false,
|
||||
HandshakeValid = false,
|
||||
PingSucceeded = false,
|
||||
Message = "Verification not started"
|
||||
};
|
||||
|
||||
const int ConnectTimeoutMs = 1000;
|
||||
const int FrameTimeoutMs = 30000; // Match bridge frame I/O timeout
|
||||
|
||||
try
|
||||
{
|
||||
using (var client = new TcpClient())
|
||||
{
|
||||
// Attempt connection
|
||||
var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
|
||||
if (!connectTask.Wait(ConnectTimeoutMs))
|
||||
{
|
||||
result.Message = "Connection timeout";
|
||||
return result;
|
||||
}
|
||||
|
||||
using (var stream = client.GetStream())
|
||||
{
|
||||
try { client.NoDelay = true; } catch { }
|
||||
|
||||
// 1) Read handshake line (ASCII, newline-terminated)
|
||||
string handshake = ReadLineAscii(stream, 2000);
|
||||
if (string.IsNullOrEmpty(handshake) || handshake.IndexOf("FRAMING=1", StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
result.Message = "Bridge handshake missing FRAMING=1";
|
||||
return result;
|
||||
}
|
||||
|
||||
result.HandshakeValid = true;
|
||||
|
||||
// 2) Send framed "ping"
|
||||
byte[] payload = Encoding.UTF8.GetBytes("ping");
|
||||
WriteFrame(stream, payload, FrameTimeoutMs);
|
||||
|
||||
// 3) Read framed response and check for pong
|
||||
string response = ReadFrameUtf8(stream, FrameTimeoutMs);
|
||||
if (!string.IsNullOrEmpty(response) && response.IndexOf("pong", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
result.PingSucceeded = true;
|
||||
result.Success = true;
|
||||
result.Message = "Bridge verified successfully";
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Message = $"Ping failed; response='{response}'";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Message = $"Verification error: {ex.Message}";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Minimal framing helpers (8-byte big-endian length prefix), blocking with timeouts
|
||||
private static void WriteFrame(NetworkStream stream, byte[] payload, int timeoutMs)
|
||||
{
|
||||
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);
|
||||
header[1] = (byte)(len >> 48);
|
||||
header[2] = (byte)(len >> 40);
|
||||
header[3] = (byte)(len >> 32);
|
||||
header[4] = (byte)(len >> 24);
|
||||
header[5] = (byte)(len >> 16);
|
||||
header[6] = (byte)(len >> 8);
|
||||
header[7] = (byte)(len);
|
||||
|
||||
stream.WriteTimeout = timeoutMs;
|
||||
stream.Write(header, 0, header.Length);
|
||||
stream.Write(payload, 0, payload.Length);
|
||||
}
|
||||
|
||||
private static string ReadFrameUtf8(NetworkStream stream, int timeoutMs)
|
||||
{
|
||||
byte[] header = ReadExact(stream, 8, timeoutMs);
|
||||
ulong len = ((ulong)header[0] << 56)
|
||||
| ((ulong)header[1] << 48)
|
||||
| ((ulong)header[2] << 40)
|
||||
| ((ulong)header[3] << 32)
|
||||
| ((ulong)header[4] << 24)
|
||||
| ((ulong)header[5] << 16)
|
||||
| ((ulong)header[6] << 8)
|
||||
| header[7];
|
||||
if (len == 0UL) throw new IOException("Zero-length frames are not allowed");
|
||||
if (len > int.MaxValue) throw new IOException("Frame too large");
|
||||
byte[] payload = ReadExact(stream, (int)len, timeoutMs);
|
||||
return Encoding.UTF8.GetString(payload);
|
||||
}
|
||||
|
||||
private static byte[] ReadExact(NetworkStream stream, int count, int timeoutMs)
|
||||
{
|
||||
byte[] buffer = new byte[count];
|
||||
int offset = 0;
|
||||
stream.ReadTimeout = timeoutMs;
|
||||
while (offset < count)
|
||||
{
|
||||
int read = stream.Read(buffer, offset, count - offset);
|
||||
if (read <= 0) throw new IOException("Connection closed before reading expected bytes");
|
||||
offset += read;
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static string ReadLineAscii(NetworkStream stream, int timeoutMs, int maxLen = 512)
|
||||
{
|
||||
stream.ReadTimeout = timeoutMs;
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
byte[] one = new byte[1];
|
||||
while (ms.Length < maxLen)
|
||||
{
|
||||
int n = stream.Read(one, 0, 1);
|
||||
if (n <= 0) break;
|
||||
if (one[0] == (byte)'\n') break;
|
||||
ms.WriteByte(one[0]);
|
||||
}
|
||||
return Encoding.ASCII.GetString(ms.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ed4f9f69d84a945248dafc0f0b5a62dd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,502 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using MCPForUnity.Editor.Data;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Models;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of client configuration service
|
||||
/// </summary>
|
||||
public class ClientConfigurationService : IClientConfigurationService
|
||||
{
|
||||
private readonly Data.McpClients mcpClients = new();
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
string result = client.mcpType == McpTypes.Codex
|
||||
? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client)
|
||||
: McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
|
||||
|
||||
if (result == "Configured successfully")
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: {client.name} configured successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Configuration completed with message: {result}");
|
||||
}
|
||||
|
||||
CheckClientStatus(client);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Failed to configure {client.name}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public ClientConfigurationSummary ConfigureAllDetectedClients()
|
||||
{
|
||||
var summary = new ClientConfigurationSummary();
|
||||
var pathService = MCPServiceLocator.Paths;
|
||||
|
||||
foreach (var client in mcpClients.clients)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Skip if already configured
|
||||
CheckClientStatus(client, attemptAutoRewrite: false);
|
||||
if (client.status == McpStatus.Configured)
|
||||
{
|
||||
summary.SkippedCount++;
|
||||
summary.Messages.Add($"✓ {client.name}: Already configured");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if required tools are available
|
||||
if (client.mcpType == McpTypes.ClaudeCode)
|
||||
{
|
||||
if (!pathService.IsClaudeCliDetected())
|
||||
{
|
||||
summary.SkippedCount++;
|
||||
summary.Messages.Add($"➜ {client.name}: Claude CLI not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
RegisterClaudeCode();
|
||||
summary.SuccessCount++;
|
||||
summary.Messages.Add($"✓ {client.name}: Registered successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Other clients require UV
|
||||
if (!pathService.IsUvDetected())
|
||||
{
|
||||
summary.SkippedCount++;
|
||||
summary.Messages.Add($"➜ {client.name}: UV not found");
|
||||
continue;
|
||||
}
|
||||
|
||||
ConfigureClient(client);
|
||||
summary.SuccessCount++;
|
||||
summary.Messages.Add($"✓ {client.name}: Configured successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
summary.FailureCount++;
|
||||
summary.Messages.Add($"⚠ {client.name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true)
|
||||
{
|
||||
var previousStatus = client.status;
|
||||
|
||||
try
|
||||
{
|
||||
// Special handling for Claude Code
|
||||
if (client.mcpType == McpTypes.ClaudeCode)
|
||||
{
|
||||
CheckClaudeCodeConfiguration(client);
|
||||
return client.status != previousStatus;
|
||||
}
|
||||
|
||||
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
return client.status != previousStatus;
|
||||
}
|
||||
|
||||
string configJson = File.ReadAllText(configPath);
|
||||
string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath();
|
||||
|
||||
// Check configuration based on client type
|
||||
string[] args = null;
|
||||
bool configExists = false;
|
||||
|
||||
switch (client.mcpType)
|
||||
{
|
||||
case McpTypes.VSCode:
|
||||
dynamic vsConfig = JsonConvert.DeserializeObject(configJson);
|
||||
if (vsConfig?.servers?.unityMCP != null)
|
||||
{
|
||||
args = vsConfig.servers.unityMCP.args.ToObject<string[]>();
|
||||
configExists = true;
|
||||
}
|
||||
else if (vsConfig?.mcp?.servers?.unityMCP != null)
|
||||
{
|
||||
args = vsConfig.mcp.servers.unityMCP.args.ToObject<string[]>();
|
||||
configExists = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case McpTypes.Codex:
|
||||
if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs))
|
||||
{
|
||||
args = codexArgs;
|
||||
configExists = true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
McpConfig standardConfig = JsonConvert.DeserializeObject<McpConfig>(configJson);
|
||||
if (standardConfig?.mcpServers?.unityMCP != null)
|
||||
{
|
||||
args = standardConfig.mcpServers.unityMCP.args;
|
||||
configExists = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (configExists)
|
||||
{
|
||||
string configuredDir = McpConfigFileHelper.ExtractDirectoryArg(args);
|
||||
bool matches = !string.IsNullOrEmpty(configuredDir) &&
|
||||
McpConfigFileHelper.PathsEqual(configuredDir, pythonDir);
|
||||
|
||||
if (matches)
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
}
|
||||
else if (attemptAutoRewrite)
|
||||
{
|
||||
// Attempt auto-rewrite if path mismatch detected
|
||||
try
|
||||
{
|
||||
string rewriteResult = client.mcpType == McpTypes.Codex
|
||||
? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client)
|
||||
: McpConfigurationHelper.WriteMcpConfiguration(pythonDir, 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);
|
||||
}
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.IncorrectPath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
client.SetStatus(McpStatus.IncorrectPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.IncorrectPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.MissingConfig);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
client.SetStatus(McpStatus.Error, ex.Message);
|
||||
}
|
||||
|
||||
return client.status != previousStatus;
|
||||
}
|
||||
|
||||
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";
|
||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
|
||||
string pathPrepend = null;
|
||||
if (Application.platform == RuntimePlatform.OSXEditor)
|
||||
{
|
||||
pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
else if (Application.platform == RuntimePlatform.LinuxEditor)
|
||||
{
|
||||
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
|
||||
// Add the directory containing Claude CLI to PATH (for node/nvm scenarios)
|
||||
try
|
||||
{
|
||||
string claudeDir = Path.GetDirectoryName(claudePath);
|
||||
if (!string.IsNullOrEmpty(claudeDir))
|
||||
{
|
||||
pathPrepend = string.IsNullOrEmpty(pathPrepend)
|
||||
? claudeDir
|
||||
: $"{claudeDir}:{pathPrepend}";
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
|
||||
{
|
||||
string combined = ($"{stdout}\n{stderr}") ?? string.Empty;
|
||||
if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: MCP for Unity already registered with Claude Code.");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Successfully registered with Claude Code.");
|
||||
|
||||
// Update status
|
||||
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||
if (claudeClient != null)
|
||||
{
|
||||
CheckClaudeCodeConfiguration(claudeClient);
|
||||
}
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
string pathPrepend = Application.platform == RuntimePlatform.OSXEditor
|
||||
? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
: null;
|
||||
|
||||
// Check if UnityMCP server exists (fixed - only check for "UnityMCP")
|
||||
bool serverExists = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
|
||||
if (!serverExists)
|
||||
{
|
||||
// Nothing to unregister
|
||||
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||
if (claudeClient != null)
|
||||
{
|
||||
claudeClient.SetStatus(McpStatus.NotConfigured);
|
||||
}
|
||||
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: 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("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: MCP server successfully unregistered from Claude Code.");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to unregister: {stderr}");
|
||||
}
|
||||
|
||||
// Update status
|
||||
var client = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
||||
if (client != null)
|
||||
{
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
CheckClaudeCodeConfiguration(client);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetConfigPath(McpClient client)
|
||||
{
|
||||
// Claude Code is managed via CLI, not config files
|
||||
if (client.mcpType == McpTypes.ClaudeCode)
|
||||
{
|
||||
return "Not applicable (managed via Claude CLI)";
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return client.windowsConfigPath;
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
return client.macConfigPath;
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
return client.linuxConfigPath;
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
public string GenerateConfigJson(McpClient client)
|
||||
{
|
||||
string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath();
|
||||
string uvPath = MCPServiceLocator.Paths.GetUvPath();
|
||||
|
||||
// Claude Code uses CLI commands, not JSON config
|
||||
if (client.mcpType == McpTypes.ClaudeCode)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath))
|
||||
{
|
||||
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";
|
||||
|
||||
return "# Register the MCP server with Claude Code:\n" +
|
||||
$"{registerCommand}\n\n" +
|
||||
"# Unregister the MCP server:\n" +
|
||||
"claude mcp remove UnityMCP\n\n" +
|
||||
"# List registered servers:\n" +
|
||||
"claude mcp list # Only works when claude is run in the project's directory";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath))
|
||||
return "{ \"error\": \"Configuration not available - check paths in Advanced Settings\" }";
|
||||
|
||||
try
|
||||
{
|
||||
if (client.mcpType == McpTypes.Codex)
|
||||
{
|
||||
return CodexConfigHelper.BuildCodexServerBlock(uvPath,
|
||||
McpConfigFileHelper.ResolveServerDirectory(pythonDir, null));
|
||||
}
|
||||
else
|
||||
{
|
||||
return ConfigJsonBuilder.BuildManualConfigJson(uvPath, pythonDir, client);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"{{ \"error\": \"{ex.Message}\" }}";
|
||||
}
|
||||
}
|
||||
|
||||
public string GetInstallationSteps(McpClient client)
|
||||
{
|
||||
string baseSteps = client.mcpType switch
|
||||
{
|
||||
McpTypes.ClaudeDesktop =>
|
||||
"1. Open Claude Desktop\n" +
|
||||
"2. Go to Settings > Developer > Edit Config\n" +
|
||||
" OR open the config file at the path above\n" +
|
||||
"3. Paste the configuration JSON\n" +
|
||||
"4. Save and restart Claude Desktop",
|
||||
|
||||
McpTypes.Cursor =>
|
||||
"1. Open Cursor\n" +
|
||||
"2. Go to File > Preferences > Cursor Settings > MCP > Add new global MCP server\n" +
|
||||
" OR open the config file at the path above\n" +
|
||||
"3. Paste the configuration JSON\n" +
|
||||
"4. Save and restart Cursor",
|
||||
|
||||
McpTypes.Windsurf =>
|
||||
"1. Open Windsurf\n" +
|
||||
"2. Go to File > Preferences > Windsurf Settings > MCP > Manage MCPs > View raw config\n" +
|
||||
" OR open the config file at the path above\n" +
|
||||
"3. Paste the configuration JSON\n" +
|
||||
"4. Save and restart Windsurf",
|
||||
|
||||
McpTypes.VSCode =>
|
||||
"1. Ensure VSCode and GitHub Copilot extension are installed\n" +
|
||||
"2. Open or create mcp.json at the path above\n" +
|
||||
"3. Paste the configuration JSON\n" +
|
||||
"4. Save and restart VSCode",
|
||||
|
||||
McpTypes.Kiro =>
|
||||
"1. Open Kiro\n" +
|
||||
"2. Go to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config\n" +
|
||||
" OR open the config file at the path above\n" +
|
||||
"3. Paste the configuration JSON\n" +
|
||||
"4. Save and restart Kiro",
|
||||
|
||||
McpTypes.Codex =>
|
||||
"1. Run 'codex config edit' in a terminal\n" +
|
||||
" OR open the config file at the path above\n" +
|
||||
"2. Paste the configuration TOML\n" +
|
||||
"3. Save and restart Codex",
|
||||
|
||||
McpTypes.ClaudeCode =>
|
||||
"1. Ensure Claude CLI is installed\n" +
|
||||
"2. Use the Register button to register automatically\n" +
|
||||
" OR manually run: claude mcp add UnityMCP\n" +
|
||||
"3. Restart Claude Code",
|
||||
|
||||
_ => "Configuration steps not available for this client."
|
||||
};
|
||||
|
||||
return baseSteps;
|
||||
}
|
||||
|
||||
private void CheckClaudeCodeConfiguration(McpClient client)
|
||||
{
|
||||
try
|
||||
{
|
||||
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
return;
|
||||
}
|
||||
|
||||
string configJson = File.ReadAllText(configPath);
|
||||
dynamic claudeConfig = JsonConvert.DeserializeObject(configJson);
|
||||
|
||||
if (claudeConfig?.mcpServers != null)
|
||||
{
|
||||
var servers = claudeConfig.mcpServers;
|
||||
// Only check for UnityMCP (fixed - removed candidate hacks)
|
||||
if (servers.UnityMCP != null)
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
client.SetStatus(McpStatus.Error, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 76cad34d10fd24aaa95c4583c1f88fdf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
namespace MCPForUnity.Editor.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for controlling the Unity MCP Bridge connection
|
||||
/// </summary>
|
||||
public interface IBridgeControlService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether the bridge is currently running
|
||||
/// </summary>
|
||||
bool IsRunning { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current port the bridge is listening on
|
||||
/// </summary>
|
||||
int CurrentPort { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the bridge is in auto-connect mode
|
||||
/// </summary>
|
||||
bool IsAutoConnectMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts the Unity MCP Bridge
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>
|
||||
/// Stops the Unity MCP Bridge
|
||||
/// </summary>
|
||||
void Stop();
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the bridge connection by sending a ping and waiting for a pong response
|
||||
/// </summary>
|
||||
/// <param name="port">The port to verify</param>
|
||||
/// <returns>Verification result with detailed status</returns>
|
||||
BridgeVerificationResult Verify(int port);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a bridge verification attempt
|
||||
/// </summary>
|
||||
public class BridgeVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the verification was successful
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable message about the verification result
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the handshake was valid (FRAMING=1 protocol)
|
||||
/// </summary>
|
||||
public bool HandshakeValid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the ping/pong exchange succeeded
|
||||
/// </summary>
|
||||
public bool PingSucceeded { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6b5d9f677f6f54fc59e6fe921b260c61
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
using MCPForUnity.Editor.Models;
|
||||
|
||||
namespace MCPForUnity.Editor.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for configuring MCP clients
|
||||
/// </summary>
|
||||
public interface IClientConfigurationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures a specific MCP client
|
||||
/// </summary>
|
||||
/// <param name="client">The client to configure</param>
|
||||
void ConfigureClient(McpClient client);
|
||||
|
||||
/// <summary>
|
||||
/// Configures all detected/installed MCP clients (skips clients where CLI/tools not found)
|
||||
/// </summary>
|
||||
/// <returns>Summary of configuration results</returns>
|
||||
ClientConfigurationSummary ConfigureAllDetectedClients();
|
||||
|
||||
/// <summary>
|
||||
/// Checks the configuration status of a client
|
||||
/// </summary>
|
||||
/// <param name="client">The client to check</param>
|
||||
/// <param name="attemptAutoRewrite">If true, attempts to auto-fix mismatched paths</param>
|
||||
/// <returns>True if status changed, false otherwise</returns>
|
||||
bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true);
|
||||
|
||||
/// <summary>
|
||||
/// Registers Unity MCP with Claude Code CLI
|
||||
/// </summary>
|
||||
void RegisterClaudeCode();
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters Unity MCP from Claude Code CLI
|
||||
/// </summary>
|
||||
void UnregisterClaudeCode();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration file path for a client
|
||||
/// </summary>
|
||||
/// <param name="client">The client</param>
|
||||
/// <returns>Platform-specific config path</returns>
|
||||
string GetConfigPath(McpClient client);
|
||||
|
||||
/// <summary>
|
||||
/// Generates the configuration JSON for a client
|
||||
/// </summary>
|
||||
/// <param name="client">The client</param>
|
||||
/// <returns>JSON configuration string</returns>
|
||||
string GenerateConfigJson(McpClient client);
|
||||
|
||||
/// <summary>
|
||||
/// Gets human-readable installation steps for a client
|
||||
/// </summary>
|
||||
/// <param name="client">The client</param>
|
||||
/// <returns>Installation instructions</returns>
|
||||
string GetInstallationSteps(McpClient client);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary of configuration results for multiple clients
|
||||
/// </summary>
|
||||
public class ClientConfigurationSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of clients successfully configured
|
||||
/// </summary>
|
||||
public int SuccessCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of clients that failed to configure
|
||||
/// </summary>
|
||||
public int FailureCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of clients skipped (already configured or tool not found)
|
||||
/// </summary>
|
||||
public int SkippedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Detailed messages for each client
|
||||
/// </summary>
|
||||
public System.Collections.Generic.List<string> Messages { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human-readable summary message
|
||||
/// </summary>
|
||||
public string GetSummaryMessage()
|
||||
{
|
||||
return $"✓ {SuccessCount} configured, ⚠ {FailureCount} failed, ➜ {SkippedCount} skipped";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: aae139cfae7ac4044ac52e2658005ea1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
namespace MCPForUnity.Editor.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for resolving paths to required tools and supporting user overrides
|
||||
/// </summary>
|
||||
public interface IPathResolverService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the MCP server path (respects override if set)
|
||||
/// </summary>
|
||||
/// <returns>Path to the MCP server directory containing server.py, or null if not found</returns>
|
||||
string GetMcpServerPath();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UV package manager path (respects override if set)
|
||||
/// </summary>
|
||||
/// <returns>Path to the uv executable, or null if not found</returns>
|
||||
string GetUvPath();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Claude CLI path (respects override if set)
|
||||
/// </summary>
|
||||
/// <returns>Path to the claude executable, or null if not found</returns>
|
||||
string GetClaudeCliPath();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if Python is detected on the system
|
||||
/// </summary>
|
||||
/// <returns>True if Python is found</returns>
|
||||
bool IsPythonDetected();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if UV is detected on the system
|
||||
/// </summary>
|
||||
/// <returns>True if UV is found</returns>
|
||||
bool IsUvDetected();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if Claude CLI is detected on the system
|
||||
/// </summary>
|
||||
/// <returns>True if Claude CLI is found</returns>
|
||||
bool IsClaudeCliDetected();
|
||||
|
||||
/// <summary>
|
||||
/// Sets an override for the MCP server path
|
||||
/// </summary>
|
||||
/// <param name="path">Path to override with</param>
|
||||
void SetMcpServerOverride(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Sets an override for the UV path
|
||||
/// </summary>
|
||||
/// <param name="path">Path to override with</param>
|
||||
void SetUvPathOverride(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Sets an override for the Claude CLI path
|
||||
/// </summary>
|
||||
/// <param name="path">Path to override with</param>
|
||||
void SetClaudeCliPathOverride(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the MCP server path override
|
||||
/// </summary>
|
||||
void ClearMcpServerOverride();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the UV path override
|
||||
/// </summary>
|
||||
void ClearUvPathOverride();
|
||||
|
||||
/// <summary>
|
||||
/// Clears the Claude CLI path override
|
||||
/// </summary>
|
||||
void ClearClaudeCliPathOverride();
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a MCP server path override is active
|
||||
/// </summary>
|
||||
bool HasMcpServerOverride { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a UV path override is active
|
||||
/// </summary>
|
||||
bool HasUvPathOverride { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a Claude CLI path override is active
|
||||
/// </summary>
|
||||
bool HasClaudeCliPathOverride { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1e8d388be507345aeb0eaf27fbd3c022
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
namespace MCPForUnity.Editor.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service locator for accessing MCP services without dependency injection
|
||||
/// </summary>
|
||||
public static class MCPServiceLocator
|
||||
{
|
||||
private static IBridgeControlService _bridgeService;
|
||||
private static IClientConfigurationService _clientService;
|
||||
private static IPathResolverService _pathService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bridge control service
|
||||
/// </summary>
|
||||
public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client configuration service
|
||||
/// </summary>
|
||||
public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path resolver service
|
||||
/// </summary>
|
||||
public static IPathResolverService Paths => _pathService ??= new PathResolverService();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom implementation for a service (useful for testing)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The service interface type</typeparam>
|
||||
/// <param name="implementation">The implementation to register</param>
|
||||
public static void Register<T>(T implementation) where T : class
|
||||
{
|
||||
if (implementation is IBridgeControlService b)
|
||||
_bridgeService = b;
|
||||
else if (implementation is IClientConfigurationService c)
|
||||
_clientService = c;
|
||||
else if (implementation is IPathResolverService p)
|
||||
_pathService = p;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all services to their default implementations (useful for testing)
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
{
|
||||
_bridgeService = null;
|
||||
_clientService = null;
|
||||
_pathService = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 276d6a9f9a1714ead91573945de78992
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Editor.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of path resolver service with override support
|
||||
/// </summary>
|
||||
public class PathResolverService : IPathResolverService
|
||||
{
|
||||
private const string PythonDirOverrideKey = "MCPForUnity.PythonDirOverride";
|
||||
private const string UvPathOverrideKey = "MCPForUnity.UvPath";
|
||||
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 HasClaudeCliPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(ClaudeCliPathOverrideKey, null));
|
||||
|
||||
public string GetMcpServerPath()
|
||||
{
|
||||
// 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
|
||||
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;
|
||||
}
|
||||
|
||||
// Fall back to automatic detection
|
||||
return ExecPath.ResolveClaude();
|
||||
}
|
||||
|
||||
public bool IsPythonDetected()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Windows-specific Python detection
|
||||
if (Application.platform == RuntimePlatform.WindowsEditor)
|
||||
{
|
||||
// Common Windows Python installation paths
|
||||
string[] windowsCandidates =
|
||||
{
|
||||
@"C:\Python313\python.exe",
|
||||
@"C:\Python312\python.exe",
|
||||
@"C:\Python311\python.exe",
|
||||
@"C:\Python310\python.exe",
|
||||
@"C:\Python39\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.LocalApplicationData), @"Programs\Python\Python39\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"),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python39\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
|
||||
{
|
||||
FileName = "where",
|
||||
Arguments = "python",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using (var p = Process.Start(psi))
|
||||
{
|
||||
string outp = p.StandardOutput.ReadToEnd().Trim();
|
||||
p.WaitForExit(2000);
|
||||
if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
|
||||
{
|
||||
string[] lines = outp.Split('\n');
|
||||
foreach (string line in lines)
|
||||
{
|
||||
string trimmed = line.Trim();
|
||||
if (File.Exists(trimmed)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// macOS/Linux detection
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
||||
string[] candidates =
|
||||
{
|
||||
"/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.13/bin/python3",
|
||||
"/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
|
||||
};
|
||||
foreach (string c in candidates)
|
||||
{
|
||||
if (File.Exists(c)) return true;
|
||||
}
|
||||
|
||||
// Try 'which python3'
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/usr/bin/which",
|
||||
Arguments = "python3",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using (var p = Process.Start(psi))
|
||||
{
|
||||
string outp = p.StandardOutput.ReadToEnd().Trim();
|
||||
p.WaitForExit(2000);
|
||||
if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp) && File.Exists(outp)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsUvDetected()
|
||||
{
|
||||
return !string.IsNullOrEmpty(GetUvPath());
|
||||
}
|
||||
|
||||
public bool IsClaudeCliDetected()
|
||||
{
|
||||
return !string.IsNullOrEmpty(GetClaudeCliPath());
|
||||
}
|
||||
|
||||
public void SetMcpServerOverride(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
ClearMcpServerOverride();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(Path.Combine(path, "server.py")))
|
||||
{
|
||||
throw new ArgumentException("The selected folder does not contain server.py");
|
||||
}
|
||||
|
||||
EditorPrefs.SetString(PythonDirOverrideKey, path);
|
||||
}
|
||||
|
||||
public void SetUvPathOverride(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
ClearUvPathOverride();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new ArgumentException("The selected UV executable does not exist");
|
||||
}
|
||||
|
||||
EditorPrefs.SetString(UvPathOverrideKey, path);
|
||||
}
|
||||
|
||||
public void SetClaudeCliPathOverride(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
ClearClaudeCliPathOverride();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new ArgumentException("The selected Claude CLI executable does not exist");
|
||||
}
|
||||
|
||||
EditorPrefs.SetString(ClaudeCliPathOverrideKey, path);
|
||||
// Also update the ExecPath helper for backwards compatibility
|
||||
ExecPath.SetClaudeCliPath(path);
|
||||
}
|
||||
|
||||
public void ClearMcpServerOverride()
|
||||
{
|
||||
EditorPrefs.DeleteKey(PythonDirOverrideKey);
|
||||
}
|
||||
|
||||
public void ClearUvPathOverride()
|
||||
{
|
||||
EditorPrefs.DeleteKey(UvPathOverrideKey);
|
||||
}
|
||||
|
||||
public void ClearClaudeCliPathOverride()
|
||||
{
|
||||
EditorPrefs.DeleteKey(ClaudeCliPathOverrideKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 00a6188fd15a847fa8cc7cb7a4ce3dce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -141,8 +141,17 @@ namespace MCPForUnity.Editor.Setup
|
|||
/// <summary>
|
||||
/// Open MCP Client Configuration window
|
||||
/// </summary>
|
||||
[MenuItem("Window/MCP For Unity/Open MCP Window", priority = 4)]
|
||||
[MenuItem("Window/MCP For Unity/Open MCP Window %#m", priority = 4)]
|
||||
public static void OpenClientConfiguration()
|
||||
{
|
||||
Windows.MCPForUnityEditorWindowNew.ShowWindow();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open legacy MCP Client Configuration window
|
||||
/// </summary>
|
||||
[MenuItem("Window/MCP For Unity/Open Legacy MCP Window", priority = 5)]
|
||||
public static void OpenLegacyClientConfiguration()
|
||||
{
|
||||
Windows.MCPForUnityEditorWindow.ShowWindow();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2551,7 +2551,6 @@ namespace MCPForUnity.Editor.Tools
|
|||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// Debounced refresh/compile scheduler to coalesce bursts of edits
|
||||
static class RefreshDebounce
|
||||
|
|
@ -2659,3 +2658,4 @@ static class ManageScriptRefreshHelpers
|
|||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,834 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements; // For Unity 2021 compatibility
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using MCPForUnity.Editor.Data;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Models;
|
||||
using MCPForUnity.Editor.Services;
|
||||
|
||||
namespace MCPForUnity.Editor.Windows
|
||||
{
|
||||
public class MCPForUnityEditorWindowNew : EditorWindow
|
||||
{
|
||||
// Protocol enum for future HTTP support
|
||||
private enum ConnectionProtocol
|
||||
{
|
||||
Stdio,
|
||||
// HTTPStreaming // Future
|
||||
}
|
||||
|
||||
// Settings UI Elements
|
||||
private Toggle debugLogsToggle;
|
||||
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;
|
||||
|
||||
// Connection UI Elements
|
||||
private EnumField protocolDropdown;
|
||||
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;
|
||||
private Button configureAllButton;
|
||||
private VisualElement clientStatusIndicator;
|
||||
private Label clientStatusLabel;
|
||||
private Button configureButton;
|
||||
private VisualElement claudeCliPathRow;
|
||||
private TextField claudeCliPath;
|
||||
private Button browseClaudeButton;
|
||||
private Foldout manualConfigFoldout;
|
||||
private TextField configPathField;
|
||||
private Button copyPathButton;
|
||||
private Button openFileButton;
|
||||
private TextField configJsonField;
|
||||
private Button copyJsonButton;
|
||||
private Label installationStepsLabel;
|
||||
|
||||
// Data
|
||||
private readonly McpClients mcpClients = new();
|
||||
private int selectedClientIndex = 0;
|
||||
private ValidationLevel currentValidationLevel = ValidationLevel.Standard;
|
||||
|
||||
// Validation levels matching the existing enum
|
||||
private enum ValidationLevel
|
||||
{
|
||||
Basic,
|
||||
Standard,
|
||||
Comprehensive,
|
||||
Strict
|
||||
}
|
||||
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<MCPForUnityEditorWindowNew>("MCP For Unity");
|
||||
window.minSize = new Vector2(500, 600);
|
||||
}
|
||||
public void CreateGUI()
|
||||
{
|
||||
// Determine base path (Package Manager vs Asset Store install)
|
||||
string basePath = AssetPathUtility.GetMcpPackageRootPath();
|
||||
|
||||
// Load UXML
|
||||
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
|
||||
$"{basePath}/Editor/Windows/MCPForUnityEditorWindowNew.uxml"
|
||||
);
|
||||
|
||||
if (visualTree == null)
|
||||
{
|
||||
McpLog.Error($"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindowNew.uxml");
|
||||
return;
|
||||
}
|
||||
|
||||
visualTree.CloneTree(rootVisualElement);
|
||||
|
||||
// Load USS
|
||||
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(
|
||||
$"{basePath}/Editor/Windows/MCPForUnityEditorWindowNew.uss"
|
||||
);
|
||||
|
||||
if (styleSheet != null)
|
||||
{
|
||||
rootVisualElement.styleSheets.Add(styleSheet);
|
||||
}
|
||||
|
||||
// Cache UI elements
|
||||
CacheUIElements();
|
||||
|
||||
// Initialize UI
|
||||
InitializeUI();
|
||||
|
||||
// Register callbacks
|
||||
RegisterCallbacks();
|
||||
|
||||
// Initial update
|
||||
UpdateConnectionStatus();
|
||||
UpdateServerStatusBanner();
|
||||
UpdateClientStatus();
|
||||
UpdatePathOverrides();
|
||||
// Technically not required to connect, but if we don't do this, the UI will be blank
|
||||
UpdateManualConfiguration();
|
||||
UpdateClaudeCliPathVisibility();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EditorApplication.update += OnEditorUpdate;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EditorApplication.update -= OnEditorUpdate;
|
||||
}
|
||||
|
||||
private void OnFocus()
|
||||
{
|
||||
// Only refresh data if UI is built
|
||||
if (rootVisualElement == null || rootVisualElement.childCount == 0)
|
||||
return;
|
||||
|
||||
RefreshAllData();
|
||||
}
|
||||
|
||||
private void OnEditorUpdate()
|
||||
{
|
||||
// Only update UI if it's built
|
||||
if (rootVisualElement == null || rootVisualElement.childCount == 0)
|
||||
return;
|
||||
|
||||
UpdateConnectionStatus();
|
||||
}
|
||||
|
||||
private void RefreshAllData()
|
||||
{
|
||||
// Update connection status
|
||||
UpdateConnectionStatus();
|
||||
|
||||
// Auto-verify bridge health if connected
|
||||
if (MCPServiceLocator.Bridge.IsRunning)
|
||||
{
|
||||
VerifyBridgeConnection();
|
||||
}
|
||||
|
||||
// Update path overrides
|
||||
UpdatePathOverrides();
|
||||
|
||||
// Refresh selected client (may have been configured externally)
|
||||
if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count)
|
||||
{
|
||||
var client = mcpClients.clients[selectedClientIndex];
|
||||
MCPServiceLocator.Client.CheckClientStatus(client);
|
||||
UpdateClientStatus();
|
||||
UpdateManualConfiguration();
|
||||
UpdateClaudeCliPathVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheUIElements()
|
||||
{
|
||||
// Settings
|
||||
debugLogsToggle = rootVisualElement.Q<Toggle>("debug-logs-toggle");
|
||||
validationLevelField = rootVisualElement.Q<EnumField>("validation-level");
|
||||
validationDescription = rootVisualElement.Q<Label>("validation-description");
|
||||
advancedSettingsFoldout = rootVisualElement.Q<Foldout>("advanced-settings-foldout");
|
||||
mcpServerPathOverride = rootVisualElement.Q<TextField>("python-path-override");
|
||||
uvPathOverride = rootVisualElement.Q<TextField>("uv-path-override");
|
||||
browsePythonButton = rootVisualElement.Q<Button>("browse-python-button");
|
||||
clearPythonButton = rootVisualElement.Q<Button>("clear-python-button");
|
||||
browseUvButton = rootVisualElement.Q<Button>("browse-uv-button");
|
||||
clearUvButton = rootVisualElement.Q<Button>("clear-uv-button");
|
||||
mcpServerPathStatus = rootVisualElement.Q<VisualElement>("mcp-server-path-status");
|
||||
uvPathStatus = rootVisualElement.Q<VisualElement>("uv-path-status");
|
||||
|
||||
// Connection
|
||||
protocolDropdown = rootVisualElement.Q<EnumField>("protocol-dropdown");
|
||||
unityPortField = rootVisualElement.Q<TextField>("unity-port");
|
||||
serverPortField = rootVisualElement.Q<TextField>("server-port");
|
||||
statusIndicator = rootVisualElement.Q<VisualElement>("status-indicator");
|
||||
connectionStatusLabel = rootVisualElement.Q<Label>("connection-status");
|
||||
connectionToggleButton = rootVisualElement.Q<Button>("connection-toggle");
|
||||
healthIndicator = rootVisualElement.Q<VisualElement>("health-indicator");
|
||||
healthStatusLabel = rootVisualElement.Q<Label>("health-status");
|
||||
testConnectionButton = rootVisualElement.Q<Button>("test-connection-button");
|
||||
serverStatusBanner = rootVisualElement.Q<VisualElement>("server-status-banner");
|
||||
serverStatusMessage = rootVisualElement.Q<Label>("server-status-message");
|
||||
downloadServerButton = rootVisualElement.Q<Button>("download-server-button");
|
||||
rebuildServerButton = rootVisualElement.Q<Button>("rebuild-server-button");
|
||||
|
||||
// Client
|
||||
clientDropdown = rootVisualElement.Q<DropdownField>("client-dropdown");
|
||||
configureAllButton = rootVisualElement.Q<Button>("configure-all-button");
|
||||
clientStatusIndicator = rootVisualElement.Q<VisualElement>("client-status-indicator");
|
||||
clientStatusLabel = rootVisualElement.Q<Label>("client-status");
|
||||
configureButton = rootVisualElement.Q<Button>("configure-button");
|
||||
claudeCliPathRow = rootVisualElement.Q<VisualElement>("claude-cli-path-row");
|
||||
claudeCliPath = rootVisualElement.Q<TextField>("claude-cli-path");
|
||||
browseClaudeButton = rootVisualElement.Q<Button>("browse-claude-button");
|
||||
manualConfigFoldout = rootVisualElement.Q<Foldout>("manual-config-foldout");
|
||||
configPathField = rootVisualElement.Q<TextField>("config-path");
|
||||
copyPathButton = rootVisualElement.Q<Button>("copy-path-button");
|
||||
openFileButton = rootVisualElement.Q<Button>("open-file-button");
|
||||
configJsonField = rootVisualElement.Q<TextField>("config-json");
|
||||
copyJsonButton = rootVisualElement.Q<Button>("copy-json-button");
|
||||
installationStepsLabel = rootVisualElement.Q<Label>("installation-steps");
|
||||
}
|
||||
|
||||
private void InitializeUI()
|
||||
{
|
||||
// Settings Section
|
||||
debugLogsToggle.value = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
|
||||
|
||||
validationLevelField.Init(ValidationLevel.Standard);
|
||||
int savedLevel = EditorPrefs.GetInt("MCPForUnity.ValidationLevel", 1);
|
||||
currentValidationLevel = (ValidationLevel)Mathf.Clamp(savedLevel, 0, 3);
|
||||
validationLevelField.value = currentValidationLevel;
|
||||
UpdateValidationDescription();
|
||||
|
||||
// Advanced settings starts collapsed
|
||||
advancedSettingsFoldout.value = false;
|
||||
|
||||
// Connection Section
|
||||
protocolDropdown.Init(ConnectionProtocol.Stdio);
|
||||
protocolDropdown.SetEnabled(false); // Disabled for now, only stdio supported
|
||||
|
||||
unityPortField.value = MCPServiceLocator.Bridge.CurrentPort.ToString();
|
||||
serverPortField.value = "6500";
|
||||
|
||||
// Client Configuration
|
||||
var clientNames = mcpClients.clients.Select(c => c.name).ToList();
|
||||
clientDropdown.choices = clientNames;
|
||||
if (clientNames.Count > 0)
|
||||
{
|
||||
clientDropdown.index = 0;
|
||||
}
|
||||
|
||||
// Manual config starts collapsed
|
||||
manualConfigFoldout.value = false;
|
||||
|
||||
// Claude CLI path row hidden by default
|
||||
claudeCliPathRow.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
private void RegisterCallbacks()
|
||||
{
|
||||
// Settings callbacks
|
||||
debugLogsToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool("MCPForUnity.DebugLogs", evt.newValue);
|
||||
});
|
||||
|
||||
validationLevelField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
currentValidationLevel = (ValidationLevel)evt.newValue;
|
||||
EditorPrefs.SetInt("MCPForUnity.ValidationLevel", (int)currentValidationLevel);
|
||||
UpdateValidationDescription();
|
||||
});
|
||||
|
||||
// Advanced settings callbacks
|
||||
browsePythonButton.clicked += OnBrowsePythonClicked;
|
||||
clearPythonButton.clicked += OnClearPythonClicked;
|
||||
browseUvButton.clicked += OnBrowseUvClicked;
|
||||
clearUvButton.clicked += OnClearUvClicked;
|
||||
|
||||
// Connection callbacks
|
||||
connectionToggleButton.clicked += OnConnectionToggleClicked;
|
||||
testConnectionButton.clicked += OnTestConnectionClicked;
|
||||
downloadServerButton.clicked += OnDownloadServerClicked;
|
||||
rebuildServerButton.clicked += OnRebuildServerClicked;
|
||||
|
||||
// Client callbacks
|
||||
clientDropdown.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
selectedClientIndex = clientDropdown.index;
|
||||
UpdateClientStatus();
|
||||
UpdateManualConfiguration();
|
||||
UpdateClaudeCliPathVisibility();
|
||||
});
|
||||
|
||||
configureAllButton.clicked += OnConfigureAllClientsClicked;
|
||||
configureButton.clicked += OnConfigureClicked;
|
||||
browseClaudeButton.clicked += OnBrowseClaudeClicked;
|
||||
copyPathButton.clicked += OnCopyPathClicked;
|
||||
openFileButton.clicked += OnOpenFileClicked;
|
||||
copyJsonButton.clicked += OnCopyJsonClicked;
|
||||
}
|
||||
|
||||
private void UpdateValidationDescription()
|
||||
{
|
||||
validationDescription.text = GetValidationLevelDescription((int)currentValidationLevel);
|
||||
}
|
||||
|
||||
private string GetValidationLevelDescription(int index)
|
||||
{
|
||||
return index switch
|
||||
{
|
||||
0 => "Only basic syntax checks (braces, quotes, comments)",
|
||||
1 => "Syntax checks + Unity best practices and warnings",
|
||||
2 => "All checks + semantic analysis and performance warnings",
|
||||
3 => "Full semantic validation with namespace/type resolution (requires Roslyn)",
|
||||
_ => "Standard validation"
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateConnectionStatus()
|
||||
{
|
||||
var bridgeService = MCPServiceLocator.Bridge;
|
||||
bool isRunning = bridgeService.IsRunning;
|
||||
|
||||
if (isRunning)
|
||||
{
|
||||
connectionStatusLabel.text = "Connected";
|
||||
statusIndicator.RemoveFromClassList("disconnected");
|
||||
statusIndicator.AddToClassList("connected");
|
||||
connectionToggleButton.text = "Stop";
|
||||
}
|
||||
else
|
||||
{
|
||||
connectionStatusLabel.text = "Disconnected";
|
||||
statusIndicator.RemoveFromClassList("connected");
|
||||
statusIndicator.AddToClassList("disconnected");
|
||||
connectionToggleButton.text = "Start";
|
||||
|
||||
// Reset health status when disconnected
|
||||
healthStatusLabel.text = "Unknown";
|
||||
healthIndicator.RemoveFromClassList("healthy");
|
||||
healthIndicator.RemoveFromClassList("warning");
|
||||
healthIndicator.AddToClassList("unknown");
|
||||
}
|
||||
|
||||
// Update ports
|
||||
unityPortField.value = bridgeService.CurrentPort.ToString();
|
||||
}
|
||||
|
||||
private void UpdateClientStatus()
|
||||
{
|
||||
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
|
||||
return;
|
||||
|
||||
var client = mcpClients.clients[selectedClientIndex];
|
||||
MCPServiceLocator.Client.CheckClientStatus(client);
|
||||
|
||||
clientStatusLabel.text = client.GetStatusDisplayString();
|
||||
|
||||
// Reset inline color style (clear error state from OnConfigureClicked)
|
||||
clientStatusLabel.style.color = StyleKeyword.Null;
|
||||
|
||||
// Update status indicator color
|
||||
clientStatusIndicator.RemoveFromClassList("configured");
|
||||
clientStatusIndicator.RemoveFromClassList("not-configured");
|
||||
clientStatusIndicator.RemoveFromClassList("warning");
|
||||
|
||||
switch (client.status)
|
||||
{
|
||||
case McpStatus.Configured:
|
||||
case McpStatus.Running:
|
||||
case McpStatus.Connected:
|
||||
clientStatusIndicator.AddToClassList("configured");
|
||||
break;
|
||||
case McpStatus.IncorrectPath:
|
||||
case McpStatus.CommunicationError:
|
||||
case McpStatus.NoResponse:
|
||||
clientStatusIndicator.AddToClassList("warning");
|
||||
break;
|
||||
default:
|
||||
clientStatusIndicator.AddToClassList("not-configured");
|
||||
break;
|
||||
}
|
||||
|
||||
// Update configure button text for Claude Code
|
||||
if (client.mcpType == McpTypes.ClaudeCode)
|
||||
{
|
||||
bool isConfigured = client.status == McpStatus.Configured;
|
||||
configureButton.text = isConfigured ? "Unregister" : "Register";
|
||||
}
|
||||
else
|
||||
{
|
||||
configureButton.text = "Configure";
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateManualConfiguration()
|
||||
{
|
||||
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
|
||||
return;
|
||||
|
||||
var client = mcpClients.clients[selectedClientIndex];
|
||||
|
||||
// Get config path
|
||||
string configPath = MCPServiceLocator.Client.GetConfigPath(client);
|
||||
configPathField.value = configPath;
|
||||
|
||||
// Get config JSON
|
||||
string configJson = MCPServiceLocator.Client.GenerateConfigJson(client);
|
||||
configJsonField.value = configJson;
|
||||
|
||||
// Get installation steps
|
||||
string steps = MCPServiceLocator.Client.GetInstallationSteps(client);
|
||||
installationStepsLabel.text = steps;
|
||||
}
|
||||
|
||||
private void UpdateClaudeCliPathVisibility()
|
||||
{
|
||||
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
|
||||
return;
|
||||
|
||||
var client = mcpClients.clients[selectedClientIndex];
|
||||
|
||||
// Show Claude CLI path only for Claude Code client
|
||||
if (client.mcpType == McpTypes.ClaudeCode)
|
||||
{
|
||||
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
||||
if (string.IsNullOrEmpty(claudePath))
|
||||
{
|
||||
// Show path selector if not found
|
||||
claudeCliPathRow.style.display = DisplayStyle.Flex;
|
||||
claudeCliPath.value = "Not found - click Browse to select";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show detected path
|
||||
claudeCliPathRow.style.display = DisplayStyle.Flex;
|
||||
claudeCliPath.value = claudePath;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
claudeCliPathRow.style.display = DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePathOverrides()
|
||||
{
|
||||
var pathService = MCPServiceLocator.Paths;
|
||||
|
||||
// MCP Server Path
|
||||
string mcpServerPath = pathService.GetMcpServerPath();
|
||||
if (pathService.HasMcpServerOverride)
|
||||
{
|
||||
mcpServerPathOverride.value = mcpServerPath ?? "(override set but invalid)";
|
||||
}
|
||||
else
|
||||
{
|
||||
mcpServerPathOverride.value = mcpServerPath ?? "(auto-detected)";
|
||||
}
|
||||
|
||||
// Update status indicator
|
||||
mcpServerPathStatus.RemoveFromClassList("valid");
|
||||
mcpServerPathStatus.RemoveFromClassList("invalid");
|
||||
if (!string.IsNullOrEmpty(mcpServerPath) && File.Exists(Path.Combine(mcpServerPath, "server.py")))
|
||||
{
|
||||
mcpServerPathStatus.AddToClassList("valid");
|
||||
}
|
||||
else
|
||||
{
|
||||
mcpServerPathStatus.AddToClassList("invalid");
|
||||
}
|
||||
|
||||
// UV Path
|
||||
string uvPath = pathService.GetUvPath();
|
||||
if (pathService.HasUvPathOverride)
|
||||
{
|
||||
uvPathOverride.value = uvPath ?? "(override set but invalid)";
|
||||
}
|
||||
else
|
||||
{
|
||||
uvPathOverride.value = uvPath ?? "(auto-detected)";
|
||||
}
|
||||
|
||||
// Update status indicator
|
||||
uvPathStatus.RemoveFromClassList("valid");
|
||||
uvPathStatus.RemoveFromClassList("invalid");
|
||||
if (!string.IsNullOrEmpty(uvPath) && File.Exists(uvPath))
|
||||
{
|
||||
uvPathStatus.AddToClassList("valid");
|
||||
}
|
||||
else
|
||||
{
|
||||
uvPathStatus.AddToClassList("invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// Button callbacks
|
||||
private void OnConnectionToggleClicked()
|
||||
{
|
||||
var bridgeService = MCPServiceLocator.Bridge;
|
||||
|
||||
if (bridgeService.IsRunning)
|
||||
{
|
||||
bridgeService.Stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
bridgeService.Start();
|
||||
|
||||
// Verify connection after starting (Option C: verify on connect)
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
if (bridgeService.IsRunning)
|
||||
{
|
||||
VerifyBridgeConnection();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
UpdateConnectionStatus();
|
||||
}
|
||||
|
||||
private void OnTestConnectionClicked()
|
||||
{
|
||||
VerifyBridgeConnection();
|
||||
}
|
||||
|
||||
private void VerifyBridgeConnection()
|
||||
{
|
||||
var bridgeService = MCPServiceLocator.Bridge;
|
||||
|
||||
if (!bridgeService.IsRunning)
|
||||
{
|
||||
healthStatusLabel.text = "Disconnected";
|
||||
healthIndicator.RemoveFromClassList("healthy");
|
||||
healthIndicator.RemoveFromClassList("warning");
|
||||
healthIndicator.AddToClassList("unknown");
|
||||
McpLog.Warn("Cannot verify connection: Bridge is not running");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = bridgeService.Verify(bridgeService.CurrentPort);
|
||||
|
||||
healthIndicator.RemoveFromClassList("healthy");
|
||||
healthIndicator.RemoveFromClassList("warning");
|
||||
healthIndicator.RemoveFromClassList("unknown");
|
||||
|
||||
if (result.Success && result.PingSucceeded)
|
||||
{
|
||||
healthStatusLabel.text = "Healthy";
|
||||
healthIndicator.AddToClassList("healthy");
|
||||
McpLog.Info("Bridge verification successful");
|
||||
}
|
||||
else if (result.HandshakeValid)
|
||||
{
|
||||
healthStatusLabel.text = "Ping Failed";
|
||||
healthIndicator.AddToClassList("warning");
|
||||
McpLog.Warn($"Bridge verification warning: {result.Message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
healthStatusLabel.text = "Unhealthy";
|
||||
healthIndicator.AddToClassList("warning");
|
||||
McpLog.Error($"Bridge verification failed: {result.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDownloadServerClicked()
|
||||
{
|
||||
if (ServerInstaller.DownloadAndInstallServer())
|
||||
{
|
||||
UpdateServerStatusBanner();
|
||||
UpdatePathOverrides();
|
||||
EditorUtility.DisplayDialog(
|
||||
"Download Complete",
|
||||
"Server installed successfully! Start your connection and configure your MCP clients to begin.",
|
||||
"OK"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRebuildServerClicked()
|
||||
{
|
||||
try
|
||||
{
|
||||
bool success = ServerInstaller.RebuildMcpServer();
|
||||
if (success)
|
||||
{
|
||||
EditorUtility.DisplayDialog("MCP For Unity", "Server rebuilt successfully.", "OK");
|
||||
UpdateServerStatusBanner();
|
||||
UpdatePathOverrides();
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorUtility.DisplayDialog("MCP For Unity", "Rebuild failed. Please check Console for details.", "OK");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"Failed to rebuild server: {ex.Message}");
|
||||
EditorUtility.DisplayDialog("MCP For Unity", $"Rebuild failed: {ex.Message}", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateServerStatusBanner()
|
||||
{
|
||||
bool hasEmbedded = ServerInstaller.HasEmbeddedServer();
|
||||
string installedVer = ServerInstaller.GetInstalledServerVersion();
|
||||
string packageVer = AssetPathUtility.GetPackageVersion();
|
||||
|
||||
// Show/hide download vs rebuild buttons
|
||||
if (hasEmbedded)
|
||||
{
|
||||
downloadServerButton.style.display = DisplayStyle.None;
|
||||
rebuildServerButton.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
else
|
||||
{
|
||||
downloadServerButton.style.display = DisplayStyle.Flex;
|
||||
rebuildServerButton.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
// Update banner
|
||||
if (!hasEmbedded && string.IsNullOrEmpty(installedVer))
|
||||
{
|
||||
serverStatusMessage.text = "\u26A0 Server not installed. Click 'Download & Install Server' to get started.";
|
||||
serverStatusBanner.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
else if (!hasEmbedded && !string.IsNullOrEmpty(installedVer) && installedVer != packageVer)
|
||||
{
|
||||
serverStatusMessage.text = $"\u26A0 Server update available (v{installedVer} \u2192 v{packageVer}). Update recommended.";
|
||||
serverStatusBanner.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
else
|
||||
{
|
||||
serverStatusBanner.style.display = DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfigureAllClientsClicked()
|
||||
{
|
||||
try
|
||||
{
|
||||
var summary = MCPServiceLocator.Client.ConfigureAllDetectedClients();
|
||||
|
||||
// Build detailed message
|
||||
string message = summary.GetSummaryMessage() + "\n\n";
|
||||
foreach (var msg in summary.Messages)
|
||||
{
|
||||
message += msg + "\n";
|
||||
}
|
||||
|
||||
EditorUtility.DisplayDialog("Configure All Clients", message, "OK");
|
||||
|
||||
// Refresh current client status
|
||||
if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count)
|
||||
{
|
||||
UpdateClientStatus();
|
||||
UpdateManualConfiguration();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Configuration Failed", ex.Message, "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfigureClicked()
|
||||
{
|
||||
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
|
||||
return;
|
||||
|
||||
var client = mcpClients.clients[selectedClientIndex];
|
||||
|
||||
try
|
||||
{
|
||||
if (client.mcpType == McpTypes.ClaudeCode)
|
||||
{
|
||||
bool isConfigured = client.status == McpStatus.Configured;
|
||||
if (isConfigured)
|
||||
{
|
||||
MCPServiceLocator.Client.UnregisterClaudeCode();
|
||||
}
|
||||
else
|
||||
{
|
||||
MCPServiceLocator.Client.RegisterClaudeCode();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MCPServiceLocator.Client.ConfigureClient(client);
|
||||
}
|
||||
|
||||
UpdateClientStatus();
|
||||
UpdateManualConfiguration();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
clientStatusLabel.text = "Error";
|
||||
clientStatusLabel.style.color = Color.red;
|
||||
McpLog.Error($"Configuration failed: {ex.Message}");
|
||||
EditorUtility.DisplayDialog("Configuration Failed", ex.Message, "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBrowsePythonClicked()
|
||||
{
|
||||
string picked = EditorUtility.OpenFolderPanel("Select MCP Server Directory", Application.dataPath, "");
|
||||
if (!string.IsNullOrEmpty(picked))
|
||||
{
|
||||
try
|
||||
{
|
||||
MCPServiceLocator.Paths.SetMcpServerOverride(picked);
|
||||
UpdatePathOverrides();
|
||||
McpLog.Info($"MCP server path override set to: {picked}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Invalid Path", ex.Message, "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClearPythonClicked()
|
||||
{
|
||||
MCPServiceLocator.Paths.ClearMcpServerOverride();
|
||||
UpdatePathOverrides();
|
||||
McpLog.Info("MCP server path override cleared");
|
||||
}
|
||||
|
||||
private void OnBrowseUvClicked()
|
||||
{
|
||||
string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||
? "/opt/homebrew/bin"
|
||||
: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
string picked = EditorUtility.OpenFilePanel("Select UV Executable", suggested, "");
|
||||
if (!string.IsNullOrEmpty(picked))
|
||||
{
|
||||
try
|
||||
{
|
||||
MCPServiceLocator.Paths.SetUvPathOverride(picked);
|
||||
UpdatePathOverrides();
|
||||
McpLog.Info($"UV path override set to: {picked}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Invalid Path", ex.Message, "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClearUvClicked()
|
||||
{
|
||||
MCPServiceLocator.Paths.ClearUvPathOverride();
|
||||
UpdatePathOverrides();
|
||||
McpLog.Info("UV path override cleared");
|
||||
}
|
||||
|
||||
private void OnBrowseClaudeClicked()
|
||||
{
|
||||
string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||
? "/opt/homebrew/bin"
|
||||
: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
string picked = EditorUtility.OpenFilePanel("Select Claude CLI", suggested, "");
|
||||
if (!string.IsNullOrEmpty(picked))
|
||||
{
|
||||
try
|
||||
{
|
||||
MCPServiceLocator.Paths.SetClaudeCliPathOverride(picked);
|
||||
UpdateClaudeCliPathVisibility();
|
||||
UpdateClientStatus();
|
||||
McpLog.Info($"Claude CLI path override set to: {picked}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Invalid Path", ex.Message, "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCopyPathClicked()
|
||||
{
|
||||
EditorGUIUtility.systemCopyBuffer = configPathField.value;
|
||||
McpLog.Info("Config path copied to clipboard");
|
||||
}
|
||||
|
||||
private void OnOpenFileClicked()
|
||||
{
|
||||
string path = configPathField.value;
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Open File", "The configuration file path does not exist.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = path,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"Failed to open file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCopyJsonClicked()
|
||||
{
|
||||
EditorGUIUtility.systemCopyBuffer = configJsonField.value;
|
||||
McpLog.Info("Configuration copied to clipboard");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9f0c6e1d3e4d5e6f7a8b9c0d1e2f3a4d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
/* Root Container */
|
||||
#root-container {
|
||||
padding: 16px;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Title */
|
||||
.title {
|
||||
font-size: 20px;
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Section Styling */
|
||||
.section {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 6px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Setting Rows */
|
||||
.setting-row {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
min-height: 24px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.setting-column {
|
||||
flex-direction: column;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
min-width: 140px;
|
||||
-unity-text-align: middle-left;
|
||||
}
|
||||
|
||||
.setting-value {
|
||||
-unity-text-align: middle-right;
|
||||
color: rgba(150, 150, 150, 1);
|
||||
}
|
||||
|
||||
.setting-toggle {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.setting-dropdown {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-top: 4px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.setting-dropdown-inline {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 150px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Validation Description */
|
||||
.validation-description {
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
background-color: rgba(100, 150, 200, 0.15);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Port Fields */
|
||||
.port-field {
|
||||
width: 100px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.port-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
-unity-text-align: middle-center;
|
||||
}
|
||||
|
||||
/* Status Container */
|
||||
.status-container {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
margin-right: 8px;
|
||||
background-color: rgba(150, 150, 150, 1);
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background-color: rgba(0, 200, 100, 1);
|
||||
}
|
||||
|
||||
.status-dot.disconnected {
|
||||
background-color: rgba(200, 50, 50, 1);
|
||||
}
|
||||
|
||||
.status-dot.configured {
|
||||
background-color: rgba(0, 200, 100, 1);
|
||||
}
|
||||
|
||||
.status-dot.not-configured {
|
||||
background-color: rgba(200, 50, 50, 1);
|
||||
}
|
||||
|
||||
.status-dot.warning {
|
||||
background-color: rgba(255, 200, 0, 1);
|
||||
}
|
||||
|
||||
.status-dot.unknown {
|
||||
background-color: rgba(150, 150, 150, 1);
|
||||
}
|
||||
|
||||
.status-dot.healthy {
|
||||
background-color: rgba(0, 200, 100, 1);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
-unity-text-align: middle-left;
|
||||
}
|
||||
|
||||
.status-indicator-small {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
background-color: rgba(150, 150, 150, 1);
|
||||
}
|
||||
|
||||
.status-indicator-small.valid {
|
||||
background-color: rgba(0, 200, 100, 1);
|
||||
}
|
||||
|
||||
.status-indicator-small.invalid {
|
||||
background-color: rgba(200, 50, 50, 1);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.action-button {
|
||||
min-width: 80px;
|
||||
height: 28px;
|
||||
margin-left: 8px;
|
||||
background-color: rgba(50, 150, 250, 0.8);
|
||||
border-radius: 4px;
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background-color: rgba(50, 150, 250, 1);
|
||||
}
|
||||
|
||||
.action-button:active {
|
||||
background-color: rgba(30, 120, 200, 1);
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
margin-top: 8px;
|
||||
background-color: rgba(100, 100, 100, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
background-color: rgba(100, 100, 100, 0.5);
|
||||
}
|
||||
|
||||
.secondary-button:active {
|
||||
background-color: rgba(80, 80, 80, 0.5);
|
||||
}
|
||||
|
||||
/* Manual Configuration */
|
||||
.manual-config-foldout {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.manual-config-foldout > .unity-foldout__toggle {
|
||||
-unity-font-style: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.manual-config-content {
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-label {
|
||||
font-size: 11px;
|
||||
-unity-font-style: bold;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.path-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-path-field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-right: 4px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.config-path-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
min-width: 50px;
|
||||
height: 24px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.config-json-row {
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-json-field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 150px;
|
||||
margin-right: 4px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.config-json-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
font-size: 10px;
|
||||
-unity-font-style: normal;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon-button-vertical {
|
||||
min-width: 50px;
|
||||
height: 30px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.installation-steps {
|
||||
padding: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
white-space: normal;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Advanced Settings */
|
||||
.advanced-settings-foldout {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.advanced-settings-foldout > .unity-foldout__toggle {
|
||||
-unity-font-style: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.advanced-settings-content {
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.advanced-label {
|
||||
font-size: 11px;
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(150, 150, 150, 1);
|
||||
}
|
||||
|
||||
.override-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.override-label {
|
||||
font-size: 11px;
|
||||
-unity-font-style: bold;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.path-override-controls {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.override-field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-right: 4px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.override-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.setting-label-small {
|
||||
font-size: 11px;
|
||||
min-width: 120px;
|
||||
-unity-text-align: middle-left;
|
||||
}
|
||||
|
||||
.path-display-field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-right: 4px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.path-display-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
/* Light Theme Overrides */
|
||||
.unity-theme-light .title {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.unity-theme-light .section {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.unity-theme-light .validation-description {
|
||||
background-color: rgba(100, 150, 200, 0.1);
|
||||
}
|
||||
|
||||
.unity-theme-light .port-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.unity-theme-light .config-path-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.unity-theme-light .config-json-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.unity-theme-light .manual-config-content {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.unity-theme-light .installation-steps {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.unity-theme-light .advanced-settings-content {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.unity-theme-light .override-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.unity-theme-light .path-display-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8f9b5e0c2d3c4e5f6a7b8c9d0e1f2a3c
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
||||
<ui:ScrollView name="root-container" style="padding: 16px;">
|
||||
<!-- Title -->
|
||||
<ui:Label text="MCP For Unity" name="title" class="title" />
|
||||
|
||||
<!-- Settings Section -->
|
||||
<ui:VisualElement name="settings-section" class="section">
|
||||
<ui:Label text="Settings" class="section-title" />
|
||||
<ui:VisualElement class="section-content">
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Version:" class="setting-label" />
|
||||
<ui:Label text="5.0.0" name="version-label" class="setting-value" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Show Debug Logs:" class="setting-label" />
|
||||
<ui:Toggle name="debug-logs-toggle" class="setting-toggle" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-column">
|
||||
<ui:Label text="Script Validation Level:" class="setting-label" />
|
||||
<uie:EnumField name="validation-level" class="setting-dropdown" />
|
||||
<ui:Label name="validation-description" class="validation-description" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Advanced Settings (Collapsible) -->
|
||||
<ui:Foldout name="advanced-settings-foldout" text="Advanced Settings" class="advanced-settings-foldout">
|
||||
<ui:VisualElement class="advanced-settings-content">
|
||||
<ui:Label text="Path Overrides (leave empty for auto-detection):" class="advanced-label" />
|
||||
|
||||
<!-- MCP Server Path Override -->
|
||||
<ui:VisualElement class="override-row">
|
||||
<ui:Label text="MCP Server Path:" class="override-label" />
|
||||
<ui:VisualElement class="status-indicator-small" name="mcp-server-path-status" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="path-override-controls">
|
||||
<ui:TextField name="python-path-override" readonly="true" class="override-field" />
|
||||
<ui:Button name="browse-python-button" text="Browse" class="icon-button" />
|
||||
<ui:Button name="clear-python-button" text="Clear" class="icon-button" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- UV Path Override -->
|
||||
<ui:VisualElement class="override-row">
|
||||
<ui:Label text="UV Path:" class="override-label" />
|
||||
<ui:VisualElement class="status-indicator-small" name="uv-path-status" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="path-override-controls">
|
||||
<ui:TextField name="uv-path-override" readonly="true" class="override-field" />
|
||||
<ui:Button name="browse-uv-button" text="Browse" class="icon-button" />
|
||||
<ui:Button name="clear-uv-button" text="Clear" class="icon-button" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:Foldout>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Connection Section -->
|
||||
<ui:VisualElement name="connection-section" class="section">
|
||||
<ui:Label text="Connection" class="section-title" />
|
||||
<ui:VisualElement class="section-content">
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Protocol:" class="setting-label" />
|
||||
<uie:EnumField name="protocol-dropdown" class="setting-dropdown-inline" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Unity Port:" class="setting-label" />
|
||||
<ui:TextField name="unity-port" readonly="true" class="port-field" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Server Port:" class="setting-label" />
|
||||
<ui:TextField name="server-port" readonly="true" class="port-field" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:VisualElement class="status-container">
|
||||
<ui:VisualElement name="status-indicator" class="status-dot" />
|
||||
<ui:Label name="connection-status" text="Disconnected" class="status-text" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="connection-toggle" text="Start" class="action-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Health:" class="setting-label" />
|
||||
<ui:VisualElement class="status-container">
|
||||
<ui:VisualElement name="health-indicator" class="status-dot" />
|
||||
<ui:Label name="health-status" text="Unknown" class="status-text" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="test-connection-button" text="Test" class="action-button" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Server Status Banner -->
|
||||
<ui:VisualElement name="server-status-banner" class="banner" style="display: none; background-color: rgba(255, 193, 7, 0.2); padding: 8px; margin: 8px 0; border-radius: 4px; border-left-width: 3px; border-left-color: rgb(255, 193, 7);">
|
||||
<ui:Label name="server-status-message" text="" style="-unity-text-align: middle-left; white-space: normal;" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Server Management Buttons -->
|
||||
<ui:VisualElement class="button-row" style="flex-direction: row; margin-top: 8px;">
|
||||
<ui:Button name="download-server-button" text="Download & Install Server" class="secondary-button" style="display: none; flex: 1;" />
|
||||
<ui:Button name="rebuild-server-button" text="Rebuild Server" class="secondary-button" style="flex: 1;" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Client Configuration Section -->
|
||||
<ui:VisualElement name="client-section" class="section">
|
||||
<ui:Label text="Client Configuration" class="section-title" />
|
||||
<ui:VisualElement class="section-content">
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Client:" class="setting-label" />
|
||||
<ui:DropdownField name="client-dropdown" class="setting-dropdown-inline" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="configure-all-button" text="Configure All Detected Clients" class="secondary-button" />
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:VisualElement class="status-container">
|
||||
<ui:VisualElement name="client-status-indicator" class="status-dot" />
|
||||
<ui:Label name="client-status" text="Not Configured" class="status-text" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="configure-button" text="Configure" class="action-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row" name="claude-cli-path-row" style="display: none;">
|
||||
<ui:Label text="Claude CLI Path:" class="setting-label-small" />
|
||||
<ui:TextField name="claude-cli-path" readonly="true" class="path-display-field" />
|
||||
<ui:Button name="browse-claude-button" text="Browse" class="icon-button" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<!-- Manual Configuration (Collapsible) -->
|
||||
<ui:Foldout name="manual-config-foldout" text="Manual Configuration" class="manual-config-foldout">
|
||||
<ui:VisualElement class="manual-config-content">
|
||||
<ui:Label text="Config Path:" class="config-label" />
|
||||
<ui:VisualElement class="path-row">
|
||||
<ui:TextField name="config-path" readonly="true" class="config-path-field" />
|
||||
<ui:Button name="copy-path-button" text="Copy" class="icon-button" />
|
||||
<ui:Button name="open-file-button" text="Open" class="icon-button" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:Label text="Configuration:" class="config-label" />
|
||||
<ui:VisualElement class="config-json-row">
|
||||
<ui:TextField name="config-json" readonly="true" multiline="true" class="config-json-field" />
|
||||
<ui:Button name="copy-json-button" text="Copy" class="icon-button-vertical" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:Label text="Installation Steps:" class="config-label" />
|
||||
<ui:Label name="installation-steps" class="installation-steps" />
|
||||
</ui:VisualElement>
|
||||
</ui:Foldout>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:ScrollView>
|
||||
</ui:UXML>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7f8a4e9c1d2b3e4f5a6b7c8d9e0f1a2b
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 640 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 636 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 610 KiB |
|
|
@ -0,0 +1,303 @@
|
|||
# MCP for Unity v6 - New Editor Window
|
||||
|
||||
> **UI Toolkit-based window with service-oriented architecture**
|
||||
|
||||

|
||||
*Dark theme*
|
||||
|
||||

|
||||
*Light theme*
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The new MCP Editor Window is a complete rebuild using **UI Toolkit (UXML/USS)** with a **service-oriented architecture**. The design philosophy emphasizes **explicit over implicit** behavior, making the system more predictable, testable, and maintainable.
|
||||
|
||||
**Quick Access:** `Cmd/Ctrl+Shift+M` or `Window > MCP For Unity > Open MCP Window`
|
||||
|
||||
**Key Improvements:**
|
||||
- 🎨 Modern UI that doesn't hide info as the window size changes
|
||||
- 🏗️ Service layer separates business logic from UI
|
||||
- 🔧 Explicit path overrides for troubleshooting
|
||||
- 📦 Asset Store support with server download capability
|
||||
- ⚡ Keyboard shortcut for quick access
|
||||
|
||||
---
|
||||
|
||||
## Key Differences at a Glance
|
||||
|
||||
| Feature | Old Window | New Window | Notes |
|
||||
|---------|-----------|------------|-------|
|
||||
| **Architecture** | Monolithic | Service-based | Better testability & reusability |
|
||||
| **UI Framework** | IMGUI | UI Toolkit (UXML/USS) | Modern, responsive, themeable |
|
||||
| **Auto-Setup** | ✅ Automatic | ❌ Manual | Users have explicit control |
|
||||
| **Path Overrides** | ⚠️ Python only | ✅ Python + UV + Claude CLI | Advanced troubleshooting |
|
||||
| **Bridge Health** | ⚠️ Hidden | ✅ Visible with test button | Separate from connection status |
|
||||
| **Configure All** | ❌ None | ✅ Batch with summary | Configure all clients at once |
|
||||
| **Manual Config** | ✅ Popup windows | ✅ Inline foldout | Less window clutter |
|
||||
| **Server Download** | ❌ None | ✅ Asset Store support | Download server from GitHub |
|
||||
| **Keyboard Shortcut** | ❌ None | ✅ Cmd/Ctrl+Shift+M | Quick access |
|
||||
|
||||
## What's New
|
||||
|
||||
### UI Enhancements
|
||||
- **Advanced Settings Foldout** - Collapsible section for path overrides (MCP server, UV, Claude CLI)
|
||||
- **Visual Path Validation** - Green/red indicators show whether override paths are valid
|
||||
- **Bridge Health Indicator** - Separate from connection status, shows handshake and ping/pong results
|
||||
- **Manual Connection Test Button** - Verify bridge health on demand without reconnecting
|
||||
- **Inline Manual Configuration** - Copy config path and JSON without opening separate windows
|
||||
|
||||
### Functional Improvements
|
||||
- **Configure All Detected Clients** - One-click batch configuration with summary dialog
|
||||
- **Keyboard Shortcut** - `Cmd/Ctrl+Shift+M` opens the window quickly
|
||||
|
||||
### Asset Store Support
|
||||
- **Server Download Button** - Asset Store users can download the server from GitHub releases
|
||||
- **Dynamic UI** - Shows appropriate button based on installation type
|
||||
|
||||

|
||||
*Asset Store version showing the "Download & Install Server" button*
|
||||
|
||||
---
|
||||
|
||||
## Features Not Supported (By Design)
|
||||
|
||||
The new window intentionally removes implicit behaviors and complex edge-case handling to provide a cleaner, more predictable UX.
|
||||
|
||||
### ❌ Auto-Setup on First Run
|
||||
- **Old:** Automatically configured clients on first window open
|
||||
- **Why Removed:** Users should explicitly choose which clients to configure
|
||||
- **Alternative:** Use "Configure All Detected Clients" button
|
||||
|
||||
### ❌ Python Detection Warning
|
||||
- **Old:** Warning banner if Python not detected on system
|
||||
- **Why Removed:** Setup Wizard handles dependency checks, we also can't flood a bunch of error and warning logs when submitting to the Asset Store
|
||||
- **Alternative:** Run Setup Wizard via `Window > MCP For Unity > Setup Wizard`
|
||||
|
||||
### ❌ Separate Manual Setup Windows
|
||||
- **Old:** `VSCodeManualSetupWindow`, `ManualConfigEditorWindow` popup dialogs
|
||||
- **Why Removed:** Looks neater, less visual clutter
|
||||
- **Alternative:** Inline "Manual Configuration" foldout with copy buttons
|
||||
|
||||
### ❌ Server Installation Status Panel
|
||||
- **Old:** Dedicated panel showing server install status with color indicators
|
||||
- **Why Removed:** Simplified to focus on active configuration and the connection status, we now have a setup wizard for this
|
||||
- **Alternative:** Server path override in Advanced Settings + Rebuild button
|
||||
|
||||
---
|
||||
|
||||
## Service Locator Architecture
|
||||
|
||||
The new window uses a **service locator pattern** to access business logic without tight coupling. This provides flexibility for testing and future dependency injection migration.
|
||||
|
||||
### MCPServiceLocator
|
||||
|
||||
**Purpose:** Central access point for MCP services
|
||||
|
||||
**Usage:**
|
||||
```csharp
|
||||
// Access bridge service
|
||||
MCPServiceLocator.Bridge.Start();
|
||||
|
||||
// Access client configuration service
|
||||
MCPServiceLocator.Client.ConfigureAllDetectedClients();
|
||||
|
||||
// Access path resolver service
|
||||
string mcpServerPath = MCPServiceLocator.Paths.GetMcpServerPath();
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- No constructor dependencies (easy to use anywhere)
|
||||
- Lazy initialization (services created only when needed)
|
||||
- Testable (supports custom implementations via `Register()`)
|
||||
|
||||
---
|
||||
|
||||
### IBridgeControlService
|
||||
|
||||
**Purpose:** Manages MCP for Unity Bridge lifecycle and health verification
|
||||
|
||||
**Key Methods:**
|
||||
- `Start()` / `Stop()` - Bridge lifecycle management
|
||||
- `Verify(port)` - Health check with handshake + ping/pong validation
|
||||
- `IsRunning` - Current bridge status
|
||||
- `CurrentPort` - Active port number
|
||||
|
||||
**Implementation:** `BridgeControlService`
|
||||
|
||||
**Usage Example:**
|
||||
```csharp
|
||||
var bridge = MCPServiceLocator.Bridge;
|
||||
bridge.Start();
|
||||
|
||||
var result = bridge.Verify(bridge.CurrentPort);
|
||||
if (result.Success && result.PingSucceeded)
|
||||
{
|
||||
Debug.Log("Bridge is healthy");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### IClientConfigurationService
|
||||
|
||||
**Purpose:** Handles MCP client configuration and registration
|
||||
|
||||
**Key Methods:**
|
||||
- `ConfigureClient(client)` - Configure a single client
|
||||
- `ConfigureAllDetectedClients()` - Batch configure with summary
|
||||
- `CheckClientStatus(client)` - Verify client status + auto-rewrite paths
|
||||
- `RegisterClaudeCode()` / `UnregisterClaudeCode()` - Claude Code management
|
||||
- `GenerateConfigJson(client)` - Get JSON for manual configuration
|
||||
|
||||
**Implementation:** `ClientConfigurationService`
|
||||
|
||||
**Usage Example:**
|
||||
```csharp
|
||||
var clientService = MCPServiceLocator.Client;
|
||||
var summary = clientService.ConfigureAllDetectedClients();
|
||||
Debug.Log($"Configured: {summary.SuccessCount}, Failed: {summary.FailureCount}");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### IPathResolverService
|
||||
|
||||
**Purpose:** Resolves paths to required tools with override support
|
||||
|
||||
**Key Methods:**
|
||||
- `GetMcpServerPath()` - MCP server directory
|
||||
- `GetUvPath()` - UV executable path
|
||||
- `GetClaudeCliPath()` - Claude CLI path
|
||||
- `SetMcpServerOverride(path)` / `ClearMcpServerOverride()` - Manage MCP server overrides
|
||||
- `SetUvPathOverride(path)` / `ClearUvPathOverride()` - Manage UV overrides
|
||||
- `SetClaudeCliPathOverride(path)` / `ClearClaudeCliPathOverride()` - Manage Claude CLI overrides
|
||||
- `IsPythonDetected()` / `IsUvDetected()` - Detection checks
|
||||
|
||||
**Implementation:** `PathResolverService`
|
||||
|
||||
**Usage Example:**
|
||||
```csharp
|
||||
var paths = MCPServiceLocator.Paths;
|
||||
|
||||
// Check if UV is detected
|
||||
if (!paths.IsUvDetected())
|
||||
{
|
||||
Debug.LogWarning("UV not found");
|
||||
}
|
||||
|
||||
// Set an override
|
||||
paths.SetUvPathOverride("/custom/path/to/uv");
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Files Created
|
||||
|
||||
**Services:**
|
||||
```text
|
||||
MCPForUnity/Editor/Services/
|
||||
├── IBridgeControlService.cs # Bridge lifecycle interface
|
||||
├── BridgeControlService.cs # Bridge lifecycle implementation
|
||||
├── IClientConfigurationService.cs # Client config interface
|
||||
├── ClientConfigurationService.cs # Client config implementation
|
||||
├── IPathResolverService.cs # Path resolution interface
|
||||
├── PathResolverService.cs # Path resolution implementation
|
||||
└── MCPServiceLocator.cs # Service locator pattern
|
||||
```
|
||||
|
||||
**Helpers:**
|
||||
```text
|
||||
MCPForUnity/Editor/Helpers/
|
||||
└── AssetPathUtility.cs # Package path detection & package.json parsing
|
||||
```
|
||||
|
||||
**UI:**
|
||||
```text
|
||||
MCPForUnity/Editor/Windows/
|
||||
├── MCPForUnityEditorWindowNew.cs # Main window (~850 lines)
|
||||
├── MCPForUnityEditorWindowNew.uxml # UI Toolkit layout
|
||||
└── MCPForUnityEditorWindowNew.uss # UI Toolkit styles
|
||||
```
|
||||
|
||||
**CI/CD:**
|
||||
```text
|
||||
.github/workflows/
|
||||
└── bump-version.yml # Server upload to releases
|
||||
```
|
||||
|
||||
### Key Files Modified
|
||||
|
||||
- `ServerInstaller.cs` - Added download/install logic for Asset Store
|
||||
- `SetupWizard.cs` - Integration with new service locator
|
||||
- `PackageDetector.cs` - Uses `AssetPathUtility` for version detection
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### For Users
|
||||
|
||||
**Immediate Changes (v6.x):**
|
||||
- Both old and new windows are available
|
||||
- New window accessible via `Cmd/Ctrl+Shift+M` or menu
|
||||
- Settings and overrides are shared between windows (same EditorPrefs keys)
|
||||
- Services can be used by both windows
|
||||
|
||||
**Upcoming Changes (v8.x):**
|
||||
- ⚠️ **Old window will be removed in v8.0**
|
||||
- All users will automatically use the new window
|
||||
- EditorPrefs keys remain the same (no migration needed)
|
||||
- Custom scripts using old window APIs will need updates
|
||||
|
||||
### For Developers
|
||||
|
||||
**Using the Services:**
|
||||
```csharp
|
||||
// Accessing services from any editor script
|
||||
var bridge = MCPServiceLocator.Bridge;
|
||||
var client = MCPServiceLocator.Client;
|
||||
var paths = MCPServiceLocator.Paths;
|
||||
|
||||
// Services are lazily initialized on first access
|
||||
// No need to check for null
|
||||
```
|
||||
|
||||
**Testing with Custom Implementations:**
|
||||
```csharp
|
||||
// In test setup
|
||||
var mockBridge = new MockBridgeService();
|
||||
MCPServiceLocator.Register(mockBridge);
|
||||
|
||||
// Services are now testable without Unity dependencies
|
||||
```
|
||||
|
||||
**Reusing Service Logic:**
|
||||
The service layer is designed to be reused by other parts of the codebase. For example:
|
||||
- Build scripts can use `IClientConfigurationService` to auto-configure clients
|
||||
- CI/CD can use `IBridgeControlService` to verify bridge health
|
||||
- Tools can use `IPathResolverService` for consistent path resolution
|
||||
|
||||
**Notes:**
|
||||
- A lot of Helpers will gradually be moved to the service layer
|
||||
- Why not Dependency Injection? This change had a lot of changes, so we didn't want to add too much complexity to the codebase in one go
|
||||
|
||||
---
|
||||
|
||||
## Pull Request Reference
|
||||
|
||||
**PR #313:** [feat: New UI with service architecture](https://github.com/CoplayDev/unity-mcp/pull/313)
|
||||
|
||||
**Key Commits:**
|
||||
- Service layer implementation
|
||||
- UI Toolkit window rebuild
|
||||
- Asset Store server download support
|
||||
- CI/CD server upload automation
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-10-10
|
||||
**Unity Versions:** Unity 2021.3+ through Unity 6.x
|
||||
**Architecture:** Service Locator + UI Toolkit
|
||||
**Status:** Active (Old window deprecated in v8.0)
|
||||
Loading…
Reference in New Issue