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 Register
main
Marcus Sanatan 2025-10-11 03:08:16 -04:00 committed by GitHub
parent 2c53943556
commit 1d6d8c67af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 3486 additions and 130 deletions

View File

@ -108,3 +108,30 @@ jobs:
git tag -a "$TAG" -m "Version ${NEW_VERSION}" git tag -a "$TAG" -m "Version ${NEW_VERSION}"
git push origin "$TAG" 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}"

View File

@ -1,4 +1,9 @@
using System; using System;
using System.IO;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
namespace MCPForUnity.Editor.Helpers namespace MCPForUnity.Editor.Helpers
{ {
@ -25,5 +30,133 @@ namespace MCPForUnity.Editor.Helpers
return path; 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";
}
}
} }
} }

View File

@ -7,7 +7,7 @@ using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Helpers namespace MCPForUnity.Editor.Helpers
{ {
/// <summary> /// <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 /// development mode, embedded servers, and installed packages
/// </summary> /// </summary>
public static class McpPathResolver public static class McpPathResolver
@ -15,7 +15,7 @@ namespace MCPForUnity.Editor.Helpers
private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer"; private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer";
/// <summary> /// <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 /// including development mode support and fallback mechanisms
/// </summary> /// </summary>
public static string FindPackagePythonDirectory(bool debugLogsEnabled = false) public static string FindPackagePythonDirectory(bool debugLogsEnabled = false)

View File

@ -49,8 +49,7 @@ namespace MCPForUnity.Editor.Helpers
if (!string.IsNullOrEmpty(error)) if (!string.IsNullOrEmpty(error))
{ {
Debug.LogWarning($"MCP for Unity: Auto-detect on load failed: {capturedEx}"); McpLog.Info($"Server check: {error}. Download via Window > MCP For Unity if needed.", always: false);
// Alternatively: Debug.LogException(capturedEx);
} }
}; };
} }

View File

@ -4,7 +4,7 @@ using UnityEngine;
namespace MCPForUnity.Editor.Helpers namespace MCPForUnity.Editor.Helpers
{ {
/// <summary> /// <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> /// </summary>
[InitializeOnLoad] [InitializeOnLoad]
public static class PackageInstaller public static class PackageInstaller
@ -25,18 +25,21 @@ namespace MCPForUnity.Editor.Helpers
{ {
try try
{ {
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Installing Python server...");
ServerInstaller.EnsureServerInstalled(); ServerInstaller.EnsureServerInstalled();
// Mark as installed // Mark as installed/checked
EditorPrefs.SetBool(InstallationFlagKey, true); EditorPrefs.SetBool(InstallationFlagKey, true);
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Python server installation completed successfully."); // Only log success if server was actually embedded and copied
} if (ServerInstaller.HasEmbeddedServer())
catch (System.Exception ex)
{ {
Debug.LogError($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Failed to install Python server: {ex.Message}"); McpLog.Info("MCP server installation completed successfully.");
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."); }
}
catch (System.Exception)
{
EditorPrefs.SetBool(InstallationFlagKey, true); // Mark as handled
McpLog.Info("Server installation pending. Open Window > MCP For Unity to download the server.");
} }
} }
} }

View File

@ -1,9 +1,11 @@
using System; using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
@ -34,8 +36,19 @@ namespace MCPForUnity.Editor.Helpers
// Resolve embedded source and versions // Resolve embedded source and versions
if (!TryGetEmbeddedServerSource(out string embeddedSrc)) 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 embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName)); string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName));
@ -151,7 +164,7 @@ namespace MCPForUnity.Editor.Helpers
TryCreateMacSymlinkForAppSupport(); TryCreateMacSymlinkForAppSupport();
return Path.Combine(localAppSupport, RootFolder); return Path.Combine(localAppSupport, RootFolder);
} }
throw new Exception("Unsupported operating system."); throw new Exception("Unsupported operating system");
} }
/// <summary> /// <summary>
@ -177,7 +190,7 @@ namespace MCPForUnity.Editor.Helpers
if (!Directory.Exists(canonical)) return; if (!Directory.Exists(canonical)) return;
// Use 'ln -s' to create a directory symlink (macOS) // Use 'ln -s' to create a directory symlink (macOS)
var psi = new System.Diagnostics.ProcessStartInfo var psi = new ProcessStartInfo
{ {
FileName = "/bin/ln", FileName = "/bin/ln",
Arguments = $"-s \"{canonical}\" \"{symlink}\"", Arguments = $"-s \"{canonical}\" \"{symlink}\"",
@ -186,7 +199,7 @@ namespace MCPForUnity.Editor.Helpers
RedirectStandardError = true, RedirectStandardError = true,
CreateNoWindow = true CreateNoWindow = true
}; };
using var p = System.Diagnostics.Process.Start(psi); using var p = Process.Start(psi);
p?.WaitForExit(2000); p?.WaitForExit(2000);
} }
catch { /* best-effort */ } catch { /* best-effort */ }
@ -303,7 +316,7 @@ namespace MCPForUnity.Editor.Helpers
private static IEnumerable<string> GetLegacyRootsForDetection() 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; string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
// macOS/Linux legacy // macOS/Linux legacy
roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer")); roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer"));
@ -331,7 +344,7 @@ namespace MCPForUnity.Editor.Helpers
if (string.IsNullOrEmpty(serverSrcPath)) return; if (string.IsNullOrEmpty(serverSrcPath)) return;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
var psi = new System.Diagnostics.ProcessStartInfo var psi = new ProcessStartInfo
{ {
FileName = "/usr/bin/pgrep", FileName = "/usr/bin/pgrep",
Arguments = $"-f \"uv .*--directory {serverSrcPath}\"", Arguments = $"-f \"uv .*--directory {serverSrcPath}\"",
@ -340,7 +353,7 @@ namespace MCPForUnity.Editor.Helpers
RedirectStandardError = true, RedirectStandardError = true,
CreateNoWindow = true CreateNoWindow = true
}; };
using var p = System.Diagnostics.Process.Start(psi); using var p = Process.Start(psi);
if (p == null) return; if (p == null) return;
string outp = p.StandardOutput.ReadToEnd(); string outp = p.StandardOutput.ReadToEnd();
p.WaitForExit(1500); p.WaitForExit(1500);
@ -350,7 +363,7 @@ namespace MCPForUnity.Editor.Helpers
{ {
if (int.TryParse(line.Trim(), out int pid)) 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 // Find embedded source
if (!TryGetEmbeddedServerSource(out string embeddedSrc)) 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; return false;
} }
@ -447,11 +460,11 @@ namespace MCPForUnity.Editor.Helpers
try try
{ {
Directory.Delete(destRoot, recursive: true); 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) catch (Exception ex)
{ {
Debug.LogError($"Failed to delete existing server: {ex.Message}"); McpLog.Error($"Failed to delete existing server: {ex.Message}");
return false; return false;
} }
} }
@ -469,15 +482,15 @@ namespace MCPForUnity.Editor.Helpers
} }
catch (Exception ex) 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; return true;
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogError($"RebuildMcpServer failed: {ex.Message}"); McpLog.Error($"RebuildMcpServer failed: {ex.Message}");
return false; return false;
} }
} }
@ -508,7 +521,7 @@ namespace MCPForUnity.Editor.Helpers
// Fast path: resolve from PATH first // Fast path: resolve from PATH first
try try
{ {
var wherePsi = new System.Diagnostics.ProcessStartInfo var wherePsi = new ProcessStartInfo
{ {
FileName = "where", FileName = "where",
Arguments = "uv.exe", Arguments = "uv.exe",
@ -517,7 +530,7 @@ namespace MCPForUnity.Editor.Helpers
RedirectStandardError = true, RedirectStandardError = true,
CreateNoWindow = true CreateNoWindow = true
}; };
using var wp = System.Diagnostics.Process.Start(wherePsi); using var wp = Process.Start(wherePsi);
string output = wp.StandardOutput.ReadToEnd().Trim(); string output = wp.StandardOutput.ReadToEnd().Trim();
wp.WaitForExit(1500); wp.WaitForExit(1500);
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output)) if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
@ -613,7 +626,7 @@ namespace MCPForUnity.Editor.Helpers
{ {
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
var whichPsi = new System.Diagnostics.ProcessStartInfo var whichPsi = new ProcessStartInfo
{ {
FileName = "/usr/bin/which", FileName = "/usr/bin/which",
Arguments = "uv", Arguments = "uv",
@ -628,7 +641,7 @@ namespace MCPForUnity.Editor.Helpers
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string prepend = string.Join(":", new[] string prepend = string.Join(":", new[]
{ {
System.IO.Path.Combine(homeDir, ".local", "bin"), Path.Combine(homeDir, ".local", "bin"),
"/opt/homebrew/bin", "/opt/homebrew/bin",
"/usr/local/bin", "/usr/local/bin",
"/usr/bin", "/usr/bin",
@ -638,7 +651,7 @@ namespace MCPForUnity.Editor.Helpers
whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath); whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath);
} }
catch { } catch { }
using var wp = System.Diagnostics.Process.Start(whichPsi); using var wp = Process.Start(whichPsi);
string output = wp.StandardOutput.ReadToEnd().Trim(); string output = wp.StandardOutput.ReadToEnd().Trim();
wp.WaitForExit(3000); wp.WaitForExit(3000);
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
@ -676,7 +689,7 @@ namespace MCPForUnity.Editor.Helpers
{ {
try try
{ {
var psi = new System.Diagnostics.ProcessStartInfo var psi = new ProcessStartInfo
{ {
FileName = uvPath, FileName = uvPath,
Arguments = "--version", Arguments = "--version",
@ -685,7 +698,7 @@ namespace MCPForUnity.Editor.Helpers
RedirectStandardError = true, RedirectStandardError = true,
CreateNoWindow = 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.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; }
if (p.ExitCode == 0) if (p.ExitCode == 0)
{ {
@ -696,5 +709,133 @@ namespace MCPForUnity.Editor.Helpers
catch { } catch { }
return false; 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;
}
} }
} }

View File

@ -81,8 +81,8 @@ namespace MCPForUnity.Editor.Helpers
} }
/// <summary> /// <summary>
/// Send telemetry data to Python server for processing /// Send telemetry data to MCP server for processing
/// This is a lightweight bridge - the actual telemetry logic is in Python /// This is a lightweight bridge - the actual telemetry logic is in the MCP server
/// </summary> /// </summary>
public static void RecordEvent(string eventType, Dictionary<string, object> data = null) public static void RecordEvent(string eventType, Dictionary<string, object> data = null)
{ {
@ -106,16 +106,16 @@ namespace MCPForUnity.Editor.Helpers
telemetryData["data"] = data; telemetryData["data"] = data;
} }
// Send to Python server via existing bridge communication // Send to MCP server via existing bridge communication
// The Python server will handle actual telemetry transmission // The MCP server will handle actual telemetry transmission
SendTelemetryToPythonServer(telemetryData); SendTelemetryToMcpServer(telemetryData);
} }
catch (Exception e) catch (Exception e)
{ {
// Never let telemetry errors interfere with functionality // Never let telemetry errors interfere with functionality
if (IsDebugEnabled()) 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); 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); var sender = Volatile.Read(ref s_sender);
if (sender != null) if (sender != null)
@ -197,7 +197,7 @@ namespace MCPForUnity.Editor.Helpers
{ {
if (IsDebugEnabled()) 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 // Fallback: log when debug is enabled
if (IsDebugEnabled()) if (IsDebugEnabled())
{ {
Debug.Log($"<b><color=#2EA3FF>MCP-TELEMETRY</color></b>: {telemetryData["event_type"]}"); McpLog.Info($"Telemetry: {telemetryData["event_type"]}");
} }
} }

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2ab6b1cc527214416b21e07b96164f24
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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());
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ed4f9f69d84a945248dafc0f0b5a62dd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 76cad34d10fd24aaa95c4583c1f88fdf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b5d9f677f6f54fc59e6fe921b260c61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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";
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aae139cfae7ac4044ac52e2658005ea1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1e8d388be507345aeb0eaf27fbd3c022
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 276d6a9f9a1714ead91573945de78992
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 00a6188fd15a847fa8cc7cb7a4ce3dce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -141,8 +141,17 @@ namespace MCPForUnity.Editor.Setup
/// <summary> /// <summary>
/// Open MCP Client Configuration window /// Open MCP Client Configuration window
/// </summary> /// </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() 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(); Windows.MCPForUnityEditorWindow.ShowWindow();
} }

View File

@ -2551,11 +2551,10 @@ namespace MCPForUnity.Editor.Tools
// } // }
// } // }
} }
}
// Debounced refresh/compile scheduler to coalesce bursts of edits // Debounced refresh/compile scheduler to coalesce bursts of edits
static class RefreshDebounce static class RefreshDebounce
{ {
private static int _pending; private static int _pending;
private static readonly object _lock = new object(); private static readonly object _lock = new object();
private static readonly HashSet<string> _paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static readonly HashSet<string> _paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@ -2625,10 +2624,10 @@ static class RefreshDebounce
// AssetDatabase.Refresh(); // AssetDatabase.Refresh();
} }
} }
} }
static class ManageScriptRefreshHelpers static class ManageScriptRefreshHelpers
{ {
public static string SanitizeAssetsPath(string p) public static string SanitizeAssetsPath(string p)
{ {
if (string.IsNullOrEmpty(p)) return p; if (string.IsNullOrEmpty(p)) return p;
@ -2658,4 +2657,5 @@ static class ManageScriptRefreshHelpers
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
#endif #endif
} }
}
} }

View File

@ -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");
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9f0c6e1d3e4d5e6f7a8b9c0d1e2f3a4d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 8f9b5e0c2d3c4e5f6a7b8c9d0e1f2a3c
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}

View File

@ -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 &amp; 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>

View File

@ -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

303
docs/v6_NEW_UI_CHANGES.md Normal file
View File

@ -0,0 +1,303 @@
# MCP for Unity v6 - New Editor Window
> **UI Toolkit-based window with service-oriented architecture**
![New MCP Editor Window Dark](./screenshots/v6_new_ui_dark.png)
*Dark theme*
![New MCP Editor Window Light](./screenshots/v6_new_ui_light.png)
*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](./screenshots/v6_new_ui_asset_store_version.png)
*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)