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

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

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

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