Simplify MCP client configs (#401)

* First pass at MCP client refactor

* Restore original text instructions

Well most of them, I modified a few

* Move configurators to their own folder

It's less clusterd

* Remvoe override for Windsurf because we no longer need to use it

* Add Antigravity configs

Works like Windsurf, but it sucks ass

* Add some docs for properties

* Add comprehensive MCP client configurators documentation

* Add missing imports (#7)

* Handle Linux paths when unregistering CLI commands

* Construct a JSON error in a much more secure fashion
main
Marcus Sanatan 2025-11-27 18:18:44 -04:00 committed by GitHub
parent 7b25f7ce3e
commit f94cb2460a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1533 additions and 957 deletions

View File

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

View File

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

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class AntigravityConfigurator : JsonFileMcpConfigurator
{
public AntigravityConfigurator() : base(new McpClient
{
name = "Antigravity",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"),
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Antigravity",
"Click the more_horiz menu in the Agent pane > MCP Servers",
"Select 'Install' for Unity MCP or use the Configure button above",
"Restart Antigravity if necessary"
};
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 711b86bbc1f661e4fb2c822e14970e16
guid: 331b33961513042e3945d0a1d06615b5
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class ClaudeCodeConfigurator : ClaudeCliMcpConfigurator
{
public ClaudeCodeConfigurator() : base(new McpClient
{
name = "Claude Code",
windowsConfigPath = string.Empty,
macConfigPath = string.Empty,
linuxConfigPath = string.Empty,
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Claude CLI is installed",
"Use the Register button to register automatically\nOR manually run: claude mcp add UnityMCP",
"Restart Claude Code"
};
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1
guid: d0d22681fc594475db1c189f2d9abdf7
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class ClaudeDesktopConfigurator : JsonFileMcpConfigurator
{
public ClaudeDesktopConfigurator() : base(new McpClient
{
name = "Claude Desktop",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Claude", "claude_desktop_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Claude", "claude_desktop_config.json"),
SupportsHttpTransport = false
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Claude Desktop",
"Go to Settings > Developer > Edit Config\nOR open the config path",
"Paste the configuration JSON",
"Save and restart Claude Desktop"
};
public override void Configure()
{
bool useHttp = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
if (useHttp)
{
throw new InvalidOperationException("Claude Desktop does not support HTTP transport. Switch to stdio in settings before configuring.");
}
base.Configure();
}
public override string GetManualSnippet()
{
bool useHttp = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
if (useHttp)
{
return "# Claude Desktop does not support HTTP transport.\n" +
"# Open Advanced Settings and disable HTTP transport to use stdio, then regenerate.";
}
return base.GetManualSnippet();
}
}
}

View File

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

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CodexConfigurator : CodexMcpConfigurator
{
public CodexConfigurator() : base(new McpClient
{
name = "Codex",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml")
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Run 'codex config edit' in a terminal\nOR open the config file at the path above",
"Paste the configuration TOML",
"Save and restart Codex"
};
}
}

View File

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

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CursorConfigurator : JsonFileMcpConfigurator
{
public CursorConfigurator() : base(new McpClient
{
name = "Cursor",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json")
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Cursor",
"Go to File > Preferences > Cursor Settings > MCP > Add new global MCP server\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Cursor"
};
}
}

View File

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

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class KiroConfigurator : JsonFileMcpConfigurator
{
public KiroConfigurator() : base(new McpClient
{
name = "Kiro",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
EnsureEnvObject = true,
DefaultUnityFields = { { "disabled", false } }
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Kiro",
"Go to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Kiro"
};
}
}

View File

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

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class TraeConfigurator : JsonFileMcpConfigurator
{
public TraeConfigurator() : base(new McpClient
{
name = "Trae",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Trae", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Trae", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Trae", "mcp.json"),
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Trae and go to Settings > MCP",
"Select Add Server > Add Manually",
"Paste the JSON or point to the mcp.json file\n"+
"Windows: %AppData%\\Trae\\mcp.json\n" +
"macOS: ~/Library/Application Support/Trae/mcp.json\n" +
"Linux: ~/.config/Trae/mcp.json\n",
"Save and restart Trae"
};
}
}

View File

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

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class VSCodeConfigurator : JsonFileMcpConfigurator
{
public VSCodeConfigurator() : base(new McpClient
{
name = "VSCode GitHub Copilot",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "mcp.json"),
IsVsCodeLayout = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot extension",
"Open or create mcp.json at the path above",
"Paste the configuration JSON",
"Save and restart VSCode"
};
}
}

View File

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

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class WindsurfConfigurator : JsonFileMcpConfigurator
{
public WindsurfConfigurator() : base(new McpClient
{
name = "Windsurf",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Windsurf",
"Go to File > Preferences > Windsurf Settings > MCP > Manage MCPs > View raw config\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Windsurf"
};
}
}

View File

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

View File

@ -0,0 +1,41 @@
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients
{
/// <summary>
/// Contract for MCP client configurators. Each client is responsible for
/// status detection, auto-configure, and manual snippet/steps.
/// </summary>
public interface IMcpClientConfigurator
{
/// <summary>Stable identifier (e.g., "cursor").</summary>
string Id { get; }
/// <summary>Display name shown in the UI.</summary>
string DisplayName { get; }
/// <summary>Current status cached by the configurator.</summary>
McpStatus Status { get; }
/// <summary>True if this client supports auto-configure.</summary>
bool SupportsAutoConfigure { get; }
/// <summary>Label to show on the configure button for the current state.</summary>
string GetConfigureActionLabel();
/// <summary>Returns the platform-specific config path (or message for CLI-managed clients).</summary>
string GetConfigPath();
/// <summary>Checks and updates status; returns current status.</summary>
McpStatus CheckStatus(bool attemptAutoRewrite = true);
/// <summary>Runs auto-configuration (register/write file/CLI etc.).</summary>
void Configure();
/// <summary>Returns the manual configuration snippet (JSON/TOML/commands).</summary>
string GetManualSnippet();
/// <summary>Returns ordered human-readable installation steps.</summary>
System.Collections.Generic.IList<string> GetInstallationSteps();
}
}

View File

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

View File

@ -0,0 +1,555 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Clients
{
/// <summary>Shared base class for MCP configurators.</summary>
public abstract class McpClientConfiguratorBase : IMcpClientConfigurator
{
protected readonly McpClient client;
protected McpClientConfiguratorBase(McpClient client)
{
this.client = client;
}
internal McpClient Client => client;
public string Id => client.name.Replace(" ", "").ToLowerInvariant();
public virtual string DisplayName => client.name;
public McpStatus Status => client.status;
public virtual bool SupportsAutoConfigure => true;
public virtual string GetConfigureActionLabel() => "Configure";
public abstract string GetConfigPath();
public abstract McpStatus CheckStatus(bool attemptAutoRewrite = true);
public abstract void Configure();
public abstract string GetManualSnippet();
public abstract IList<string> GetInstallationSteps();
protected string GetUvxPathOrError()
{
string uvx = MCPServiceLocator.Paths.GetUvxPath();
if (string.IsNullOrEmpty(uvx))
{
throw new InvalidOperationException("uv not found. Install uv/uvx or set the override in Advanced Settings.");
}
return uvx;
}
protected string CurrentOsPath()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return client.windowsConfigPath;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return client.macConfigPath;
return client.linuxConfigPath;
}
protected bool UrlsEqual(string a, string b)
{
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
{
return false;
}
if (Uri.TryCreate(a.Trim(), UriKind.Absolute, out var uriA) &&
Uri.TryCreate(b.Trim(), UriKind.Absolute, out var uriB))
{
return Uri.Compare(
uriA,
uriB,
UriComponents.HttpRequestUrl,
UriFormat.SafeUnescaped,
StringComparison.OrdinalIgnoreCase) == 0;
}
string Normalize(string value) => value.Trim().TrimEnd('/');
return string.Equals(Normalize(a), Normalize(b), StringComparison.OrdinalIgnoreCase);
}
}
/// <summary>JSON-file based configurator (Cursor, Windsurf, VS Code, etc.).</summary>
public abstract class JsonFileMcpConfigurator : McpClientConfiguratorBase
{
public JsonFileMcpConfigurator(McpClient client) : base(client) { }
public override string GetConfigPath() => CurrentOsPath();
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
{
string path = GetConfigPath();
if (!File.Exists(path))
{
client.SetStatus(McpStatus.NotConfigured);
return client.status;
}
string configJson = File.ReadAllText(path);
string[] args = null;
string configuredUrl = null;
bool configExists = false;
if (client.IsVsCodeLayout)
{
var vsConfig = JsonConvert.DeserializeObject<JToken>(configJson) as JObject;
if (vsConfig != null)
{
var unityToken =
vsConfig["servers"]?["unityMCP"]
?? vsConfig["mcp"]?["servers"]?["unityMCP"];
if (unityToken is JObject unityObj)
{
configExists = true;
var argsToken = unityObj["args"];
if (argsToken is JArray)
{
args = argsToken.ToObject<string[]>();
}
var urlToken = unityObj["url"] ?? unityObj["serverUrl"];
if (urlToken != null && urlToken.Type != JTokenType.Null)
{
configuredUrl = urlToken.ToString();
}
}
}
}
else
{
McpConfig standardConfig = JsonConvert.DeserializeObject<McpConfig>(configJson);
if (standardConfig?.mcpServers?.unityMCP != null)
{
args = standardConfig.mcpServers.unityMCP.args;
configExists = true;
}
}
if (!configExists)
{
client.SetStatus(McpStatus.MissingConfig);
return client.status;
}
bool matches = false;
if (args != null && args.Length > 0)
{
string expectedUvxUrl = AssetPathUtility.GetMcpServerGitUrl();
string configuredUvxUrl = McpConfigurationHelper.ExtractUvxUrl(args);
matches = !string.IsNullOrEmpty(configuredUvxUrl) &&
McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl);
}
else if (!string.IsNullOrEmpty(configuredUrl))
{
string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl();
matches = UrlsEqual(configuredUrl, expectedUrl);
}
if (matches)
{
client.SetStatus(McpStatus.Configured);
return client.status;
}
if (attemptAutoRewrite)
{
var result = McpConfigurationHelper.WriteMcpConfiguration(path, client);
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
}
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
}
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
}
return client.status;
}
public override void Configure()
{
string path = GetConfigPath();
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
string result = McpConfigurationHelper.WriteMcpConfiguration(path, client);
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
}
else
{
throw new InvalidOperationException(result);
}
}
public override string GetManualSnippet()
{
try
{
string uvx = GetUvxPathOrError();
return ConfigJsonBuilder.BuildManualConfigJson(uvx, client);
}
catch (Exception ex)
{
var errorObj = new { error = ex.Message };
return JsonConvert.SerializeObject(errorObj);
}
}
public override IList<string> GetInstallationSteps() => new List<string> { "Configuration steps not available for this client." };
}
/// <summary>Codex (TOML) configurator.</summary>
public abstract class CodexMcpConfigurator : McpClientConfiguratorBase
{
public CodexMcpConfigurator(McpClient client) : base(client) { }
public override string GetConfigPath() => CurrentOsPath();
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
{
string path = GetConfigPath();
if (!File.Exists(path))
{
client.SetStatus(McpStatus.NotConfigured);
return client.status;
}
string toml = File.ReadAllText(path);
if (CodexConfigHelper.TryParseCodexServer(toml, out _, out var args, out var url))
{
bool matches = false;
if (!string.IsNullOrEmpty(url))
{
matches = UrlsEqual(url, HttpEndpointUtility.GetMcpRpcUrl());
}
else if (args != null && args.Length > 0)
{
string expected = AssetPathUtility.GetMcpServerGitUrl();
string configured = McpConfigurationHelper.ExtractUvxUrl(args);
matches = !string.IsNullOrEmpty(configured) &&
McpConfigurationHelper.PathsEqual(configured, expected);
}
if (matches)
{
client.SetStatus(McpStatus.Configured);
return client.status;
}
}
if (attemptAutoRewrite)
{
string result = McpConfigurationHelper.ConfigureCodexClient(path, client);
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
}
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
}
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
}
return client.status;
}
public override void Configure()
{
string path = GetConfigPath();
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
string result = McpConfigurationHelper.ConfigureCodexClient(path, client);
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
}
else
{
throw new InvalidOperationException(result);
}
}
public override string GetManualSnippet()
{
try
{
string uvx = GetUvxPathOrError();
return CodexConfigHelper.BuildCodexServerBlock(uvx);
}
catch (Exception ex)
{
return $"# error: {ex.Message}";
}
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Run 'codex config edit' or open the config path",
"Paste the TOML",
"Save and restart Codex"
};
}
/// <summary>CLI-based configurator (Claude Code).</summary>
public abstract class ClaudeCliMcpConfigurator : McpClientConfiguratorBase
{
public ClaudeCliMcpConfigurator(McpClient client) : base(client) { }
public override bool SupportsAutoConfigure => true;
public override string GetConfigureActionLabel() => client.status == McpStatus.Configured ? "Unregister" : "Register";
public override string GetConfigPath() => "Managed via Claude CLI";
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
{
var pathService = MCPServiceLocator.Paths;
string claudePath = pathService.GetClaudeCliPath();
if (string.IsNullOrEmpty(claudePath))
{
client.SetStatus(McpStatus.NotConfigured, "Claude CLI not found");
return client.status;
}
string args = "mcp list";
string projectDir = Path.GetDirectoryName(Application.dataPath);
string pathPrepend = null;
if (Application.platform == RuntimePlatform.OSXEditor)
{
pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
}
else if (Application.platform == RuntimePlatform.LinuxEditor)
{
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
}
try
{
string claudeDir = Path.GetDirectoryName(claudePath);
if (!string.IsNullOrEmpty(claudeDir))
{
pathPrepend = string.IsNullOrEmpty(pathPrepend)
? claudeDir
: $"{claudeDir}:{pathPrepend}";
}
}
catch { }
if (ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out _, 10000, pathPrepend))
{
if (!string.IsNullOrEmpty(stdout) && stdout.IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0)
{
client.SetStatus(McpStatus.Configured);
return client.status;
}
}
client.SetStatus(McpStatus.NotConfigured);
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
}
return client.status;
}
public override void Configure()
{
if (client.status == McpStatus.Configured)
{
Unregister();
}
else
{
Register();
}
}
private void Register()
{
var pathService = MCPServiceLocator.Paths;
string claudePath = pathService.GetClaudeCliPath();
if (string.IsNullOrEmpty(claudePath))
{
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
}
bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
string args;
if (useHttpTransport)
{
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
args = $"mcp add --transport http UnityMCP {httpUrl}";
}
else
{
var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" {packageName}";
}
string projectDir = Path.GetDirectoryName(Application.dataPath);
string pathPrepend = null;
if (Application.platform == RuntimePlatform.OSXEditor)
{
pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
}
else if (Application.platform == RuntimePlatform.LinuxEditor)
{
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
}
try
{
string claudeDir = Path.GetDirectoryName(claudePath);
if (!string.IsNullOrEmpty(claudeDir))
{
pathPrepend = string.IsNullOrEmpty(pathPrepend)
? claudeDir
: $"{claudeDir}:{pathPrepend}";
}
}
catch { }
bool already = false;
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)
{
already = true;
}
else
{
throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}");
}
}
if (!already)
{
McpLog.Info("Successfully registered with Claude Code.");
}
CheckStatus();
}
private void Unregister()
{
var pathService = MCPServiceLocator.Paths;
string claudePath = pathService.GetClaudeCliPath();
if (string.IsNullOrEmpty(claudePath))
{
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
}
string projectDir = Path.GetDirectoryName(Application.dataPath);
string pathPrepend = null;
if (Application.platform == RuntimePlatform.OSXEditor)
{
pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
}
else if (Application.platform == RuntimePlatform.LinuxEditor)
{
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
}
bool serverExists = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
if (!serverExists)
{
client.SetStatus(McpStatus.NotConfigured);
McpLog.Info("No MCP for Unity server found - already unregistered.");
return;
}
if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
{
McpLog.Info("MCP server successfully unregistered from Claude Code.");
}
else
{
throw new InvalidOperationException($"Failed to unregister: {stderr}");
}
client.SetStatus(McpStatus.NotConfigured);
CheckStatus();
}
public override string GetManualSnippet()
{
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
if (useHttpTransport)
{
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
return "# Register the MCP server with Claude Code:\n" +
$"claude mcp add --transport http UnityMCP {httpUrl}\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(uvxPath))
{
return "# Error: Configuration not available - check paths in Advanced Settings";
}
string gitUrl = AssetPathUtility.GetMcpServerGitUrl();
return "# Register the MCP server with Claude Code:\n" +
$"claude mcp add --transport stdio UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" mcp-for-unity\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";
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Claude CLI is installed",
"Use Register to add UnityMCP (or run claude mcp add UnityMCP)",
"Restart Claude Code"
};
}
}

View File

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

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Clients
{
/// <summary>
/// Central registry that auto-discovers configurators via TypeCache.
/// </summary>
public static class McpClientRegistry
{
private static List<IMcpClientConfigurator> cached;
public static IReadOnlyList<IMcpClientConfigurator> All
{
get
{
if (cached == null)
{
cached = BuildRegistry();
}
return cached;
}
}
private static List<IMcpClientConfigurator> BuildRegistry()
{
var configurators = new List<IMcpClientConfigurator>();
foreach (var type in TypeCache.GetTypesDerivedFrom<IMcpClientConfigurator>())
{
if (type.IsAbstract || !type.IsClass || !type.IsPublic)
continue;
// Require a public parameterless constructor
if (type.GetConstructor(Type.EmptyTypes) == null)
continue;
try
{
if (Activator.CreateInstance(type) is IMcpClientConfigurator instance)
{
configurators.Add(instance);
}
}
catch (Exception ex)
{
Debug.LogWarning($"UnityMCP: Failed to instantiate configurator {type.Name}: {ex.Message}");
}
}
// Alphabetical order by display name
configurators = configurators.OrderBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase).ToList();
return configurators;
}
}
}

View File

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

View File

@ -1,225 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Data
{
public class McpClients
{
public List<McpClient> clients = new()
{
// 1) Cursor
new()
{
name = "Cursor",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".cursor",
"mcp.json"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".cursor",
"mcp.json"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".cursor",
"mcp.json"
),
mcpType = McpTypes.Cursor,
configStatus = "Not Configured",
},
// 2) Claude Code
new()
{
name = "Claude Code",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".claude.json"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".claude.json"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".claude.json"
),
mcpType = McpTypes.ClaudeCode,
configStatus = "Not Configured",
},
// 3) Windsurf
new()
{
name = "Windsurf",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".codeium",
"windsurf",
"mcp_config.json"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".codeium",
"windsurf",
"mcp_config.json"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".codeium",
"windsurf",
"mcp_config.json"
),
mcpType = McpTypes.Windsurf,
configStatus = "Not Configured",
},
// 4) Claude Desktop
new()
{
name = "Claude Desktop",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Claude",
"claude_desktop_config.json"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library",
"Application Support",
"Claude",
"claude_desktop_config.json"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config",
"Claude",
"claude_desktop_config.json"
),
mcpType = McpTypes.ClaudeDesktop,
configStatus = "Not Configured",
},
// 5) VSCode GitHub Copilot
new()
{
name = "VSCode GitHub Copilot",
// Windows path is canonical under %AppData%\Code\User
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Code",
"User",
"mcp.json"
),
// macOS: ~/Library/Application Support/Code/User/mcp.json
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library",
"Application Support",
"Code",
"User",
"mcp.json"
),
// Linux: ~/.config/Code/User/mcp.json
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config",
"Code",
"User",
"mcp.json"
),
mcpType = McpTypes.VSCode,
configStatus = "Not Configured",
},
// Trae IDE
new()
{
name = "Trae",
// Windows: %AppData%\Trae\mcp.json
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Trae",
"mcp.json"
),
// macOS: ~/Library/Application Support/Trae/mcp.json
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library",
"Application Support",
"Trae",
"mcp.json"
),
// Linux: ~/.config/Trae/mcp.json
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config",
"Trae",
"mcp.json"
),
mcpType = McpTypes.Trae,
configStatus = "Not Configured",
},
// 3) Kiro
new()
{
name = "Kiro",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".kiro",
"settings",
"mcp.json"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".kiro",
"settings",
"mcp.json"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".kiro",
"settings",
"mcp.json"
),
mcpType = McpTypes.Kiro,
configStatus = "Not Configured",
},
// 4) Codex CLI
new()
{
name = "Codex CLI",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".codex",
"config.toml"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".codex",
"config.toml"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".codex",
"config.toml"
),
mcpType = McpTypes.Codex,
configStatus = "Not Configured",
},
};
// Initialize status enums after construction
public McpClients()
{
foreach (var client in clients)
{
if (client.configStatus == "Not Configured")
{
client.status = McpStatus.NotConfigured;
}
}
}
}
}

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Models;
@ -13,16 +14,8 @@ namespace MCPForUnity.Editor.Helpers
public static string BuildManualConfigJson(string uvPath, McpClient client)
{
var root = new JObject();
bool isVSCode = client?.mcpType == McpTypes.VSCode;
JObject container;
if (isVSCode)
{
container = EnsureObject(root, "servers");
}
else
{
container = EnsureObject(root, "mcpServers");
}
bool isVSCode = client?.IsVsCodeLayout == true;
JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
var unity = new JObject();
PopulateUnityNode(unity, uvPath, client, isVSCode);
@ -35,7 +28,7 @@ namespace MCPForUnity.Editor.Helpers
public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, McpClient client)
{
if (root == null) root = new JObject();
bool isVSCode = client?.mcpType == McpTypes.VSCode;
bool isVSCode = client?.IsVsCodeLayout == true;
JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
JObject unity = container["unityMCP"] as JObject ?? new JObject();
PopulateUnityNode(unity, uvPath, client, isVSCode);
@ -54,21 +47,20 @@ namespace MCPForUnity.Editor.Helpers
private static void PopulateUnityNode(JObject unity, string uvPath, McpClient client, bool isVSCode)
{
// Get transport preference (default to HTTP)
bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
bool isWindsurf = client?.mcpType == McpTypes.Windsurf;
bool useHttpTransport = client?.SupportsHttpTransport != false && EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
string httpProperty = string.IsNullOrEmpty(client?.HttpUrlProperty) ? "url" : client.HttpUrlProperty;
var urlPropsToRemove = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "url", "serverUrl" };
urlPropsToRemove.Remove(httpProperty);
if (useHttpTransport)
{
// HTTP mode: Use URL, no command
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
string httpProperty = isWindsurf ? "serverUrl" : "url";
unity[httpProperty] = httpUrl;
// Remove legacy property for Windsurf (or vice versa)
string staleProperty = isWindsurf ? "url" : "serverUrl";
if (unity[staleProperty] != null)
foreach (var prop in urlPropsToRemove)
{
unity.Remove(staleProperty);
if (unity[prop] != null) unity.Remove(prop);
}
// Remove command/args if they exist from previous config
@ -102,6 +94,10 @@ namespace MCPForUnity.Editor.Helpers
// Remove url/serverUrl if they exist from previous config
if (unity["url"] != null) unity.Remove("url");
if (unity["serverUrl"] != null) unity.Remove("serverUrl");
foreach (var prop in urlPropsToRemove)
{
if (unity[prop] != null) unity.Remove(prop);
}
if (isVSCode)
{
@ -115,8 +111,8 @@ namespace MCPForUnity.Editor.Helpers
unity.Remove("type");
}
bool requiresEnv = client?.mcpType == McpTypes.Kiro;
bool requiresDisabled = client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro);
bool requiresEnv = client?.EnsureEnvObject == true;
bool stripEnv = client?.StripEnvWhenNotRequired == true;
if (requiresEnv)
{
@ -125,14 +121,20 @@ namespace MCPForUnity.Editor.Helpers
unity["env"] = new JObject();
}
}
else if (isWindsurf && unity["env"] != null)
else if (stripEnv && unity["env"] != null)
{
unity.Remove("env");
}
if (requiresDisabled && unity["disabled"] == null)
if (client?.DefaultUnityFields != null)
{
unity["disabled"] = false;
foreach (var kvp in client.DefaultUnityFields)
{
if (unity[kvp.Key] == null)
{
unity[kvp.Key] = kvp.Value != null ? JToken.FromObject(kvp.Value) : JValue.CreateNull();
}
}
}
}

View File

@ -79,7 +79,7 @@ namespace MCPForUnity.Editor.Helpers
// Determine existing entry references (command/args)
string existingCommand = null;
string[] existingArgs = null;
bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode);
bool isVSCode = (mcpClient?.IsVsCodeLayout == true);
try
{
if (isVSCode)

View File

@ -1,6 +1,6 @@
using System;
using System.IO;
using MCPForUnity.Editor.Data;
using MCPForUnity.Editor.Clients;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
@ -8,6 +8,7 @@ using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Constants;
using System.Linq;
namespace MCPForUnity.Editor.Migrations
{
@ -48,21 +49,24 @@ namespace MCPForUnity.Editor.Migrations
bool hadFailures = false;
bool touchedAny = false;
var clients = new McpClients().clients;
foreach (var client in clients)
var configurators = McpClientRegistry.All.OfType<McpClientConfiguratorBase>().ToList();
foreach (var configurator in configurators)
{
try
{
if (!ConfigUsesStdIo(client))
if (!ConfigUsesStdIo(configurator.Client))
continue;
MCPServiceLocator.Client.ConfigureClient(client);
if (!configurator.SupportsAutoConfigure)
continue;
MCPServiceLocator.Client.ConfigureClient(configurator);
touchedAny = true;
}
catch (Exception ex)
{
hadFailures = true;
McpLog.Warn($"Failed to refresh stdio config for {client.name}: {ex.Message}");
McpLog.Warn($"Failed to refresh stdio config for {configurator.DisplayName}: {ex.Message}");
}
}
@ -90,13 +94,7 @@ namespace MCPForUnity.Editor.Migrations
private static bool ConfigUsesStdIo(McpClient client)
{
switch (client.mcpType)
{
case McpTypes.Codex:
return CodexConfigUsesStdIo(client);
default:
return JsonConfigUsesStdIo(client);
}
return JsonConfigUsesStdIo(client);
}
private static bool JsonConfigUsesStdIo(McpClient client)
@ -112,7 +110,7 @@ namespace MCPForUnity.Editor.Migrations
var root = JObject.Parse(File.ReadAllText(configPath));
JToken unityNode = null;
if (client.mcpType == McpTypes.VSCode)
if (client.IsVsCodeLayout)
{
unityNode = root.SelectToken("servers.unityMCP")
?? root.SelectToken("mcp.servers.unityMCP");
@ -132,24 +130,5 @@ namespace MCPForUnity.Editor.Migrations
}
}
private static bool CodexConfigUsesStdIo(McpClient client)
{
try
{
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
if (string.IsNullOrEmpty(configPath) || !File.Exists(configPath))
{
return false;
}
string toml = File.ReadAllText(configPath);
return CodexConfigHelper.TryParseCodexServer(toml, out var command, out _)
&& !string.IsNullOrEmpty(command);
}
catch
{
return false;
}
}
}
}

View File

@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace MCPForUnity.Editor.Models
{
public class McpClient
@ -6,10 +8,17 @@ namespace MCPForUnity.Editor.Models
public string windowsConfigPath;
public string macConfigPath;
public string linuxConfigPath;
public McpTypes mcpType;
public string configStatus;
public McpStatus status = McpStatus.NotConfigured;
// Capability flags/config for JSON-based configurators
public bool IsVsCodeLayout; // Whether the config file follows VS Code layout (env object at root)
public bool SupportsHttpTransport = true; // Whether the MCP server supports HTTP transport
public bool EnsureEnvObject; // Whether to ensure the env object is present in the config
public bool StripEnvWhenNotRequired; // Whether to strip the env object when not required
public string HttpUrlProperty = "url"; // The property name for the HTTP URL in the config
public Dictionary<string, object> DefaultUnityFields = new();
// Helper method to convert the enum to a display string
public string GetStatusDisplayString()
{

View File

@ -1,14 +0,0 @@
namespace MCPForUnity.Editor.Models
{
public enum McpTypes
{
ClaudeCode,
ClaudeDesktop,
Codex,
Cursor,
Kiro,
VSCode,
Windsurf,
Trae,
}
}

View File

@ -1,15 +1,8 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Data;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Clients;
using MCPForUnity.Editor.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Services
{
@ -18,565 +11,49 @@ namespace MCPForUnity.Editor.Services
/// </summary>
public class ClientConfigurationService : IClientConfigurationService
{
private readonly Data.McpClients mcpClients = new();
private readonly List<IMcpClientConfigurator> configurators;
public void ConfigureClient(McpClient client)
public ClientConfigurationService()
{
var pathService = MCPServiceLocator.Paths;
string uvxPath = pathService.GetUvxPath();
configurators = McpClientRegistry.All.ToList();
}
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
public IReadOnlyList<IMcpClientConfigurator> GetAllClients() => configurators;
string result = client.mcpType == McpTypes.Codex
? McpConfigurationHelper.ConfigureCodexClient(configPath, client)
: McpConfigurationHelper.WriteMcpConfiguration(configPath, client);
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
}
else
{
client.SetStatus(McpStatus.NotConfigured);
throw new InvalidOperationException($"Configuration failed: {result}");
}
public void ConfigureClient(IMcpClientConfigurator configurator)
{
configurator.Configure();
}
public ClientConfigurationSummary ConfigureAllDetectedClients()
{
var summary = new ClientConfigurationSummary();
var pathService = MCPServiceLocator.Paths;
foreach (var client in mcpClients.clients)
foreach (var configurator in configurators)
{
try
{
// Always re-run configuration so core fields stay current
CheckClientStatus(client, attemptAutoRewrite: false);
// 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;
}
// Force a fresh registration so transport settings stay current
UnregisterClaudeCode();
RegisterClaudeCode();
summary.SuccessCount++;
summary.Messages.Add($"✓ {client.name}: Re-registered successfully");
}
else
{
ConfigureClient(client);
summary.SuccessCount++;
summary.Messages.Add($"✓ {client.name}: Configured successfully");
}
configurator.CheckStatus(attemptAutoRewrite: false);
configurator.Configure();
summary.SuccessCount++;
summary.Messages.Add($"✓ {configurator.DisplayName}: Configured successfully");
}
catch (Exception ex)
{
summary.FailureCount++;
summary.Messages.Add($"⚠ {client.name}: {ex.Message}");
summary.Messages.Add($"⚠ {configurator.DisplayName}: {ex.Message}");
}
}
return summary;
}
public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true)
public bool CheckClientStatus(IMcpClientConfigurator configurator, 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);
// Check configuration based on client type
string[] args = null;
string configuredUrl = null;
bool configExists = false;
switch (client.mcpType)
{
case McpTypes.VSCode:
var vsConfig = JsonConvert.DeserializeObject<JToken>(configJson) as JObject;
if (vsConfig != null)
{
var unityToken =
vsConfig["servers"]?["unityMCP"]
?? vsConfig["mcp"]?["servers"]?["unityMCP"];
if (unityToken is JObject unityObj)
{
configExists = true;
var argsToken = unityObj["args"];
if (argsToken is JArray)
{
args = argsToken.ToObject<string[]>();
}
var urlToken = unityObj["url"] ?? unityObj["serverUrl"];
if (urlToken != null && urlToken.Type != JTokenType.Null)
{
configuredUrl = urlToken.ToString();
}
}
}
break;
case McpTypes.Codex:
if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs, out var codexUrl))
{
args = codexArgs;
configuredUrl = codexUrl;
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)
{
bool matches = false;
if (args != null && args.Length > 0)
{
string expectedUvxUrl = AssetPathUtility.GetMcpServerGitUrl();
string configuredUvxUrl = McpConfigurationHelper.ExtractUvxUrl(args);
matches = !string.IsNullOrEmpty(configuredUvxUrl) &&
McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl);
}
else if (!string.IsNullOrEmpty(configuredUrl))
{
string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl();
matches = UrlsEqual(configuredUrl, expectedUrl);
}
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(configPath, client)
: McpConfigurationHelper.WriteMcpConfiguration(configPath, client);
if (rewriteResult == "Configured successfully")
{
bool debugLogsEnabled = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
if (debugLogsEnabled)
{
string targetDescriptor = args != null && args.Length > 0
? AssetPathUtility.GetMcpServerGitUrl()
: HttpEndpointUtility.GetMcpRpcUrl();
McpLog.Info($"Auto-updated MCP config for '{client.name}' to new version: {targetDescriptor}", 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;
var previous = configurator.Status;
var current = configurator.CheckStatus(attemptAutoRewrite);
return current != previous;
}
public void RegisterClaudeCode()
{
var pathService = MCPServiceLocator.Paths;
string claudePath = pathService.GetClaudeCliPath();
if (string.IsNullOrEmpty(claudePath))
{
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
}
// Check transport preference
bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
string args;
if (useHttpTransport)
{
// HTTP mode: Use --transport http with URL
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
args = $"mcp add --transport http UnityMCP {httpUrl}";
}
else
{
// Stdio mode: Use command with uvx
var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" {packageName}";
}
string projectDir = Path.GetDirectoryName(Application.dataPath);
string pathPrepend = null;
if (Application.platform == RuntimePlatform.OSXEditor)
{
pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
}
else if (Application.platform == RuntimePlatform.LinuxEditor)
{
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
}
// 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)
{
McpLog.Info("MCP for Unity already registered with Claude Code.");
}
else
{
throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}");
}
return;
}
McpLog.Info("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);
}
McpLog.Info("No MCP for Unity server found - already unregistered.");
return;
}
// Remove the server
if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
{
McpLog.Info("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 uvxPath = MCPServiceLocator.Paths.GetUvxPath();
// Claude Code uses CLI commands, not JSON config
if (client.mcpType == McpTypes.ClaudeCode)
{
// Check transport preference
bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
string registerCommand;
if (useHttpTransport)
{
// HTTP mode
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
registerCommand = $"claude mcp add --transport http UnityMCP {httpUrl}";
}
else
{
// Stdio mode
if (string.IsNullOrEmpty(uvxPath))
{
return "# Error: Configuration not available - check paths in Advanced Settings";
}
string gitUrl = AssetPathUtility.GetMcpServerGitUrl();
registerCommand = $"claude mcp add --transport stdio UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" mcp-for-unity";
}
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(uvxPath))
return "{ \"error\": \"Configuration not available - check paths in Advanced Settings\" }";
try
{
if (client.mcpType == McpTypes.Codex)
{
return CodexConfigHelper.BuildCodexServerBlock(uvxPath);
}
else
{
return ConfigJsonBuilder.BuildManualConfigJson(uvxPath, 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",
McpTypes.Trae =>
"1. Open Trae and go to Settings > MCP\n" +
"2. Select Add Server > Add Manually\n" +
"3. Paste the JSON or point to the mcp.json file\n" +
" Windows: %AppData%\\Trae\\mcp.json\n" +
" macOS: ~/Library/Application Support/Trae/mcp.json\n" +
" Linux: ~/.config/Trae/mcp.json\n" +
"4. For local servers, Node.js (npx) or uvx must be installed\n" +
"5. Save and restart Trae",
_ => "Configuration steps not available for this client."
};
return baseSteps;
}
private void CheckClaudeCodeConfiguration(McpClient client)
{
try
{
var pathService = MCPServiceLocator.Paths;
string claudePath = pathService.GetClaudeCliPath();
if (string.IsNullOrEmpty(claudePath))
{
client.SetStatus(McpStatus.NotConfigured, "Claude CLI not found");
return;
}
// Use 'claude mcp list' to check if UnityMCP is registered
string args = "mcp list";
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
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, 10000, pathPrepend))
{
// Check if UnityMCP is in the output
if (!string.IsNullOrEmpty(stdout) && stdout.IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0)
{
client.SetStatus(McpStatus.Configured);
return;
}
}
client.SetStatus(McpStatus.NotConfigured);
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
}
}
private static bool UrlsEqual(string a, string b)
{
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
{
return false;
}
if (Uri.TryCreate(a.Trim(), UriKind.Absolute, out var uriA) &&
Uri.TryCreate(b.Trim(), UriKind.Absolute, out var uriB))
{
return Uri.Compare(
uriA,
uriB,
UriComponents.HttpRequestUrl,
UriFormat.SafeUnescaped,
StringComparison.OrdinalIgnoreCase) == 0;
}
string Normalize(string value) => value.Trim().TrimEnd('/');
return string.Equals(Normalize(a), Normalize(b), StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@ -1,3 +1,5 @@
using System.Collections.Generic;
using MCPForUnity.Editor.Clients;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Services
@ -11,7 +13,7 @@ namespace MCPForUnity.Editor.Services
/// Configures a specific MCP client
/// </summary>
/// <param name="client">The client to configure</param>
void ConfigureClient(McpClient client);
void ConfigureClient(IMcpClientConfigurator configurator);
/// <summary>
/// Configures all detected/installed MCP clients (skips clients where CLI/tools not found)
@ -25,38 +27,10 @@ namespace MCPForUnity.Editor.Services
/// <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);
bool CheckClientStatus(IMcpClientConfigurator configurator, bool attemptAutoRewrite = true);
/// <summary>
/// Registers MCP for Unity with Claude Code CLI
/// </summary>
void RegisterClaudeCode();
/// <summary>
/// Unregisters MCP for Unity 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>Gets the registry of discovered configurators.</summary>
IReadOnlyList<IMcpClientConfigurator> GetAllClients();
}
/// <summary>

View File

@ -2,7 +2,6 @@ using System;
using System.IO;
using System.Linq;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Data;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
@ -6,7 +7,7 @@ using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using MCPForUnity.Editor.Data;
using MCPForUnity.Editor.Clients;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
@ -36,15 +37,15 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
private Label installationStepsLabel;
// Data
private readonly McpClients mcpClients;
private readonly List<IMcpClientConfigurator> configurators;
private int selectedClientIndex = 0;
public VisualElement Root { get; private set; }
public McpClientConfigSection(VisualElement root, McpClients clients)
public McpClientConfigSection(VisualElement root)
{
Root = root;
mcpClients = clients;
configurators = MCPServiceLocator.Client.GetAllClients().ToList();
CacheUIElements();
InitializeUI();
RegisterCallbacks();
@ -70,7 +71,7 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
private void InitializeUI()
{
var clientNames = mcpClients.clients.Select(c => c.name).ToList();
var clientNames = configurators.Select(c => c.DisplayName).ToList();
clientDropdown.choices = clientNames;
if (clientNames.Count > 0)
{
@ -100,20 +101,20 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
public void UpdateClientStatus()
{
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
return;
var client = mcpClients.clients[selectedClientIndex];
var client = configurators[selectedClientIndex];
MCPServiceLocator.Client.CheckClientStatus(client);
clientStatusLabel.text = client.GetStatusDisplayString();
clientStatusLabel.text = GetStatusDisplayString(client.Status);
clientStatusLabel.style.color = StyleKeyword.Null;
clientStatusIndicator.RemoveFromClassList("configured");
clientStatusIndicator.RemoveFromClassList("not-configured");
clientStatusIndicator.RemoveFromClassList("warning");
switch (client.status)
switch (client.Status)
{
case McpStatus.Configured:
case McpStatus.Running:
@ -130,42 +131,60 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
break;
}
if (client.mcpType == McpTypes.ClaudeCode)
configureButton.text = client.GetConfigureActionLabel();
}
private string GetStatusDisplayString(McpStatus status)
{
return status switch
{
bool isConfigured = client.status == McpStatus.Configured;
configureButton.text = isConfigured ? "Unregister" : "Register";
}
else
{
configureButton.text = "Configure";
}
McpStatus.NotConfigured => "Not Configured",
McpStatus.Configured => "Configured",
McpStatus.Running => "Running",
McpStatus.Connected => "Connected",
McpStatus.IncorrectPath => "Incorrect Path",
McpStatus.CommunicationError => "Communication Error",
McpStatus.NoResponse => "No Response",
McpStatus.UnsupportedOS => "Unsupported OS",
McpStatus.MissingConfig => "Missing MCPForUnity Config",
McpStatus.Error => "Error",
_ => "Unknown",
};
}
public void UpdateManualConfiguration()
{
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
return;
var client = mcpClients.clients[selectedClientIndex];
var client = configurators[selectedClientIndex];
string configPath = MCPServiceLocator.Client.GetConfigPath(client);
string configPath = client.GetConfigPath();
configPathField.value = configPath;
string configJson = MCPServiceLocator.Client.GenerateConfigJson(client);
string configJson = client.GetManualSnippet();
configJsonField.value = configJson;
string steps = MCPServiceLocator.Client.GetInstallationSteps(client);
installationStepsLabel.text = steps;
var steps = client.GetInstallationSteps();
if (steps != null && steps.Count > 0)
{
var numbered = steps.Select((s, i) => $"{i + 1}. {s}");
installationStepsLabel.text = string.Join("\n", numbered);
}
else
{
installationStepsLabel.text = "Configuration steps not available for this client.";
}
}
private void UpdateClaudeCliPathVisibility()
{
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
return;
var client = mcpClients.clients[selectedClientIndex];
var client = configurators[selectedClientIndex];
if (client.mcpType == McpTypes.ClaudeCode)
if (client is ClaudeCliMcpConfigurator)
{
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
if (string.IsNullOrEmpty(claudePath))
@ -199,7 +218,7 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
EditorUtility.DisplayDialog("Configure All Clients", message, "OK");
if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count)
if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count)
{
UpdateClientStatus();
UpdateManualConfiguration();
@ -213,30 +232,14 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
private void OnConfigureClicked()
{
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
return;
var client = mcpClients.clients[selectedClientIndex];
var client = configurators[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);
}
MCPServiceLocator.Client.ConfigureClient(client);
UpdateClientStatus();
UpdateManualConfiguration();
}
@ -308,9 +311,9 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
public void RefreshSelectedClient()
{
if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count)
if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count)
{
var client = mcpClients.clients[selectedClientIndex];
var client = configurators[selectedClientIndex];
MCPServiceLocator.Client.CheckClientStatus(client);
UpdateClientStatus();
UpdateManualConfiguration();

View File

@ -4,9 +4,8 @@ using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using MCPForUnity.Editor.Data;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Windows.Components.Settings;
using MCPForUnity.Editor.Windows.Components.Connection;
using MCPForUnity.Editor.Windows.Components.ClientConfig;
@ -20,9 +19,7 @@ namespace MCPForUnity.Editor.Windows
private McpConnectionSection connectionSection;
private McpClientConfigSection clientConfigSection;
// Data
private readonly McpClients mcpClients = new();
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
public static void ShowWindow()
{
@ -105,8 +102,8 @@ namespace MCPForUnity.Editor.Windows
{
var clientConfigRoot = clientConfigTree.Instantiate();
sectionsContainer.Add(clientConfigRoot);
clientConfigSection = new McpClientConfigSection(clientConfigRoot, mcpClients);
}
clientConfigSection = new McpClientConfigSection(clientConfigRoot);
}
// Initial updates
RefreshAllData();

View File

@ -14,9 +14,11 @@ What changed and why:
import glob
import json
import logging
import os
from datetime import datetime
from pathlib import Path
import socket
import struct
from models.models import UnityInstanceInfo

View File

@ -76,7 +76,13 @@ namespace MCPForUnityTests.Editor.Helpers
var configPath = Path.Combine(_tempRoot, "windsurf.json");
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf };
var client = new McpClient
{
name = "Windsurf",
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
};
InvokeWriteToConfig(configPath, client);
var root = JObject.Parse(File.ReadAllText(configPath));
@ -84,7 +90,7 @@ namespace MCPForUnityTests.Editor.Helpers
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
Assert.IsNull(unity["env"], "Windsurf configs should not include an env block");
Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Windsurf when missing");
AssertTransportConfiguration(unity, McpTypes.Windsurf);
AssertTransportConfiguration(unity, client);
}
[Test]
@ -93,7 +99,12 @@ namespace MCPForUnityTests.Editor.Helpers
var configPath = Path.Combine(_tempRoot, "kiro.json");
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
var client = new McpClient { name = "Kiro", mcpType = McpTypes.Kiro };
var client = new McpClient
{
name = "Kiro",
EnsureEnvObject = true,
DefaultUnityFields = { { "disabled", false } }
};
InvokeWriteToConfig(configPath, client);
var root = JObject.Parse(File.ReadAllText(configPath));
@ -102,7 +113,7 @@ namespace MCPForUnityTests.Editor.Helpers
Assert.NotNull(unity["env"], "env should be present for all clients");
Assert.IsTrue(unity["env"]!.Type == JTokenType.Object, "env should be an object");
Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Kiro when missing");
AssertTransportConfiguration(unity, McpTypes.Kiro);
AssertTransportConfiguration(unity, client);
}
[Test]
@ -111,7 +122,7 @@ namespace MCPForUnityTests.Editor.Helpers
var configPath = Path.Combine(_tempRoot, "cursor.json");
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
var client = new McpClient { name = "Cursor", mcpType = McpTypes.Cursor };
var client = new McpClient { name = "Cursor" };
InvokeWriteToConfig(configPath, client);
var root = JObject.Parse(File.ReadAllText(configPath));
@ -119,7 +130,7 @@ namespace MCPForUnityTests.Editor.Helpers
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
Assert.IsNull(unity["env"], "env should not be added for non-Windsurf/Kiro clients");
Assert.IsNull(unity["disabled"], "disabled should not be added for non-Windsurf/Kiro clients");
AssertTransportConfiguration(unity, McpTypes.Cursor);
AssertTransportConfiguration(unity, client);
}
[Test]
@ -128,7 +139,7 @@ namespace MCPForUnityTests.Editor.Helpers
var configPath = Path.Combine(_tempRoot, "vscode.json");
WriteInitialConfig(configPath, isVSCode: true, command: _fakeUvPath, directory: "/old/path");
var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode };
var client = new McpClient { name = "VSCode", IsVsCodeLayout = true };
InvokeWriteToConfig(configPath, client);
var root = JObject.Parse(File.ReadAllText(configPath));
@ -136,7 +147,7 @@ namespace MCPForUnityTests.Editor.Helpers
Assert.NotNull(unity, "Expected servers.unityMCP node");
Assert.IsNull(unity["env"], "env should not be added for VSCode client");
Assert.IsNull(unity["disabled"], "disabled should not be added for VSCode client");
AssertTransportConfiguration(unity, McpTypes.VSCode);
AssertTransportConfiguration(unity, client);
}
[Test]
@ -145,12 +156,7 @@ namespace MCPForUnityTests.Editor.Helpers
var configPath = Path.Combine(_tempRoot, "trae.json");
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
if (!Enum.TryParse<McpTypes>("Trae", out var traeValue))
{
Assert.Ignore("McpTypes.Trae not available in this package version; skipping test.");
}
var client = new McpClient { name = "Trae", mcpType = traeValue };
var client = new McpClient { name = "Trae" };
InvokeWriteToConfig(configPath, client);
var root = JObject.Parse(File.ReadAllText(configPath));
@ -158,7 +164,7 @@ namespace MCPForUnityTests.Editor.Helpers
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
Assert.IsNull(unity["env"], "env should not be added for Trae client");
Assert.IsNull(unity["disabled"], "disabled should not be added for Trae client");
AssertTransportConfiguration(unity, traeValue);
AssertTransportConfiguration(unity, client);
}
[Test]
@ -182,7 +188,12 @@ namespace MCPForUnityTests.Editor.Helpers
};
File.WriteAllText(configPath, json.ToString());
var client = new McpClient { name = "Kiro", mcpType = McpTypes.Kiro };
var client = new McpClient
{
name = "Kiro",
EnsureEnvObject = true,
DefaultUnityFields = { { "disabled", false } }
};
InvokeWriteToConfig(configPath, client);
var root = JObject.Parse(File.ReadAllText(configPath));
@ -190,7 +201,7 @@ namespace MCPForUnityTests.Editor.Helpers
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
Assert.AreEqual("bar", (string)unity["env"]!["FOO"], "Existing env should be preserved");
Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved");
AssertTransportConfiguration(unity, McpTypes.Kiro);
AssertTransportConfiguration(unity, client);
}
[Test]
@ -213,7 +224,13 @@ namespace MCPForUnityTests.Editor.Helpers
};
File.WriteAllText(configPath, json.ToString());
var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf };
var client = new McpClient
{
name = "Windsurf",
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
};
InvokeWriteToConfig(configPath, client);
var root = JObject.Parse(File.ReadAllText(configPath));
@ -221,7 +238,7 @@ namespace MCPForUnityTests.Editor.Helpers
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
Assert.IsNull(unity["env"], "Windsurf config should strip any existing env block");
Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved");
AssertTransportConfiguration(unity, McpTypes.Windsurf);
AssertTransportConfiguration(unity, client);
}
[Test]
@ -232,13 +249,19 @@ namespace MCPForUnityTests.Editor.Helpers
WithTransportPreference(false, () =>
{
var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf };
var client = new McpClient
{
name = "Windsurf",
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
};
InvokeWriteToConfig(configPath, client);
var root = JObject.Parse(File.ReadAllText(configPath));
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
AssertTransportConfiguration(unity, McpTypes.Windsurf);
AssertTransportConfiguration(unity, client);
});
}
@ -250,13 +273,13 @@ namespace MCPForUnityTests.Editor.Helpers
WithTransportPreference(false, () =>
{
var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode };
var client = new McpClient { name = "VSCode", IsVsCodeLayout = true };
InvokeWriteToConfig(configPath, client);
var root = JObject.Parse(File.ReadAllText(configPath));
var unity = (JObject)root.SelectToken("servers.unityMCP");
Assert.NotNull(unity, "Expected servers.unityMCP node");
AssertTransportConfiguration(unity, McpTypes.VSCode);
AssertTransportConfiguration(unity, client);
});
}
@ -324,11 +347,11 @@ namespace MCPForUnityTests.Editor.Helpers
Assert.AreEqual("Configured successfully", result, "WriteMcpConfiguration should return success");
}
private static void AssertTransportConfiguration(JObject unity, McpTypes clientType)
private static void AssertTransportConfiguration(JObject unity, McpClient client)
{
bool useHttp = EditorPrefs.GetBool(UseHttpTransportPrefKey, true);
bool isVSCode = clientType == McpTypes.VSCode;
bool isWindsurf = clientType == McpTypes.Windsurf;
bool isVSCode = client.IsVsCodeLayout;
bool isWindsurf = string.Equals(client.HttpUrlProperty, "serverUrl", StringComparison.OrdinalIgnoreCase);
if (useHttp)
{

View File

@ -0,0 +1,290 @@
# MCP Client Configurators
This guide explains how MCP client configurators work in this repo and how to add a new one.
It covers:
- **Typical JSON-file clients** (Cursor, VSCode GitHub Copilot, Windsurf, Kiro, Trae, Antigravity, etc.).
- **Special clients** like **Claude CLI** and **Codex** that require custom logic.
- **How to add a new configurator class** so it shows up automatically in the MCP for Unity window.
## Quick example: JSON-file configurator
For most clients you just need a small class like this:
```csharp
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class MyClientConfigurator : JsonFileMcpConfigurator
{
public MyClientConfigurator() : base(new McpClient
{
name = "My Client",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".myclient", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".myclient", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".myclient", "mcp.json"),
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open My Client and go to MCP settings",
"Open or create the mcp.json file at the path above",
"Click Configure in MCP for Unity (or paste the manual JSON snippet)",
"Restart My Client"
};
}
}
```
---
## How the configurator system works
At a high level:
- **`IMcpClientConfigurator`** (`MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs`)
- Contract for all MCP client configurators.
- Handles status detection, auto-configure, manual snippet, and installation steps.
- **Base classes** (`MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs`)
- **`McpClientConfiguratorBase`**
- Common properties and helpers.
- **`JsonFileMcpConfigurator`**
- For JSON-based config files (most clients).
- Implements `CheckStatus`, `Configure`, and `GetManualSnippet` using `ConfigJsonBuilder`.
- **`CodexMcpConfigurator`**
- For Codex-style TOML config files.
- **`ClaudeCliMcpConfigurator`**
- For CLI-driven clients like Claude Code (register/unregister via CLI, not JSON files).
- **`McpClient` model** (`MCPForUnity/Editor/Models/McpClient.cs`)
- Holds the per-client configuration:
- `name`
- `windowsConfigPath`, `macConfigPath`, `linuxConfigPath`
- Status and several **JSON-config flags** (used by `JsonFileMcpConfigurator`):
- `IsVsCodeLayout` VS Code-style layout (`servers` root, `type` field, etc.).
- `SupportsHttpTransport` whether the client supports HTTP transport.
- `EnsureEnvObject` ensure an `env` object exists.
- `StripEnvWhenNotRequired` remove `env` when not needed.
- `HttpUrlProperty` which property holds the HTTP URL (e.g. `"url"` vs `"serverUrl"`).
- `DefaultUnityFields` key/value pairs like `{ "disabled": false }` applied when missing.
- **Auto-discovery** (`McpClientRegistry`)
- `McpClientRegistry.All` uses `TypeCache.GetTypesDerivedFrom<IMcpClientConfigurator>()` to find configurators.
- A configurator appears automatically if:
- It is a **public, non-abstract class**.
- It has a **public parameterless constructor**.
- No extra registration list is required.
---
## Typical JSON-file clients
Most MCP clients use a JSON config file that defines one or more MCP servers. Examples:
- **Cursor** `JsonFileMcpConfigurator` (global `~/.cursor/mcp.json`).
- **VSCode GitHub Copilot** `JsonFileMcpConfigurator` with `IsVsCodeLayout = true`.
- **Windsurf** `JsonFileMcpConfigurator` with Windsurf-specific flags (`HttpUrlProperty = "serverUrl"`, `DefaultUnityFields["disabled"] = false`, etc.).
- **Kiro**, **Trae**, **Antigravity (Gemini)** JSON configs with project-specific paths and flags.
All of these follow the same pattern:
1. **Subclass `JsonFileMcpConfigurator`.**
2. **Provide a `McpClient` instance** in the constructor with:
- A user-friendly `name`.
- OS-specific config paths.
- Any JSON behavior flags as needed.
3. **Override `GetInstallationSteps`** to describe how users open or edit the config.
4. Rely on **base implementations** for:
- `CheckStatus` reads and validates the JSON config; can auto-rewrite to match Unity MCP.
- `Configure` writes/rewrites the config file.
- `GetManualSnippet` builds a JSON snippet using `ConfigJsonBuilder`.
### JSON behavior controlled by `McpClient`
`JsonFileMcpConfigurator` relies on the fields on `McpClient`:
- **HTTP vs stdio**
- `SupportsHttpTransport` + `EditorPrefs.UseHttpTransport` decide whether to configure
- `url` / `serverUrl` (HTTP), or
- `command` + `args` (stdio with `uvx`).
- **URL property name**
- `HttpUrlProperty` (default `"url"`) selects which JSON property to use for HTTP urls.
- Example: Windsurf and Antigravity use `"serverUrl"`.
- **VS Code layout**
- `IsVsCodeLayout = true` switches config structure to a VS Code compatible layout.
- **Env object and default fields**
- `EnsureEnvObject` / `StripEnvWhenNotRequired` control an `env` block.
- `DefaultUnityFields` adds client-specific fields if they are missing (e.g. `disabled: false`).
All of this logic is centralized in **`ConfigJsonBuilder`**, so most JSON-based clients **do not need to override** `GetManualSnippet`.
---
## Special clients
Some clients cannot be handled by the generic JSON configurator alone.
### Codex (TOML-based)
- Uses **`CodexMcpConfigurator`**.
- Reads and writes a **TOML** config (usually `~/.codex/config.toml`).
- Uses `CodexConfigHelper` to:
- Parse the existing TOML.
- Check for a matching Unity MCP server configuration.
- Write/patch the Codex server block.
- The `CodexConfigurator` class:
- Only needs to supply a `McpClient` with TOML config paths.
- Inherits the Codex-specific status and configure behavior from `CodexMcpConfigurator`.
### Claude Code (CLI-based)
- Uses **`ClaudeCliMcpConfigurator`**.
- Configuration is stored **internally by the Claude CLI**, not in a JSON file.
- `CheckStatus` and `Configure` are implemented in the base class using `claude mcp ...` commands:
- `CheckStatus` calls `claude mcp list` to detect if `UnityMCP` is registered.
- `Configure` toggles register/unregister via `claude mcp add/remove UnityMCP`.
- The `ClaudeCodeConfigurator` class:
- Only needs a `McpClient` with a `name`.
- Overrides `GetInstallationSteps` with CLI-specific instructions.
### Claude Desktop (JSON with restrictions)
- Uses **`JsonFileMcpConfigurator`**, but only supports **stdio transport**.
- `ClaudeDesktopConfigurator`:
- Sets `SupportsHttpTransport = false` in `McpClient`.
- Overrides `Configure` / `GetManualSnippet` to:
- Guard against HTTP mode.
- Provide clear error text if HTTP is enabled.
---
## Adding a new MCP client (typical JSON case)
This is the most common scenario: your MCP client uses a JSON file to configure servers.
### 1. Choose the base class
- Use **`JsonFileMcpConfigurator`** if your client reads a JSON config file.
- Consider **`CodexMcpConfigurator`** only if you are integrating a TOML-based client like Codex.
- Consider **`ClaudeCliMcpConfigurator`** only if your client exposes a CLI command to manage MCP servers.
### 2. Create the configurator class
Create a new file under:
```text
MCPForUnity/Editor/Clients/Configurators
```
Name it something like:
```text
MyClientConfigurator.cs
```
Inside, follow the existing pattern (e.g. `CursorConfigurator`, `WindsurfConfigurator`, `KiroConfigurator`):
- **Namespace** must be:
- `MCPForUnity.Editor.Clients.Configurators`
- **Class**:
- `public class MyClientConfigurator : JsonFileMcpConfigurator`
- **Constructor**:
- Public, **parameterless**, and call `base(new McpClient { ... })`.
- Set at least:
- `name = "My Client"`
- `windowsConfigPath = ...`
- `macConfigPath = ...`
- `linuxConfigPath = ...`
- Optionally set flags:
- `IsVsCodeLayout = true` for VS Code-style config.
- `HttpUrlProperty = "serverUrl"` if your client expects `serverUrl`.
- `EnsureEnvObject` / `StripEnvWhenNotRequired` based on env handling.
- `DefaultUnityFields = { { "disabled", false }, ... }` for client-specific defaults.
Because the constructor is parameterless and public, **`McpClientRegistry` will auto-discover this configurator** with no extra registration.
### 3. Add installation steps
Override `GetInstallationSteps` to tell users how to configure the client:
- Where to find or create the JSON config file.
- Which menu path opens the MCP settings.
- Whether they should rely on the **Configure** button or copy-paste the manual JSON.
Look at `CursorConfigurator`, `VSCodeConfigurator`, `KiroConfigurator`, `TraeConfigurator`, or `AntigravityConfigurator` for phrasing.
### 4. Rely on the base JSON logic
Unless your client has very unusual behavior, you typically **do not need to override**:
- `CheckStatus`
- `Configure`
- `GetManualSnippet`
The base `JsonFileMcpConfigurator`:
- Detects missing or mismatched config.
- Optionally rewrites config to match Unity MCP.
- Builds a JSON snippet with **correct HTTP vs stdio settings**, using `ConfigJsonBuilder`.
Only override these methods if your client has constraints that cannot be expressed via `McpClient` flags.
### 5. Verify in Unity
After adding your configurator class:
1. Open Unity and the **MCP for Unity** window.
2. Your client should appear in the list, sorted by display name (`McpClient.name`).
3. Use **Check Status** to verify:
- Missing config files show as `Not Configured`.
- Existing files with matching server settings show as `Configured`.
4. Click **Configure** to auto-write the config file.
5. Restart your MCP client and confirm it connects to Unity.
---
## Adding a custom (non-JSON) client
If your MCP client doesnt store configuration as a JSON file, you likely need a custom base class.
### Codex-style TOML client
- Subclass **`CodexMcpConfigurator`**.
- Provide TOML paths via `McpClient` (similar to `CodexConfigurator`).
- Override `GetInstallationSteps` to describe how to open/edit the TOML.
The Codex-specific status and configure logic is already implemented in the base class.
### CLI-managed client (Claude-style)
- Subclass **`ClaudeCliMcpConfigurator`**.
- Provide a `McpClient` with a `name`.
- Override `GetInstallationSteps` with the CLI flow.
The base class:
- Locates the CLI binary using `MCPServiceLocator.Paths`.
- Uses `ExecPath.TryRun` to call `mcp list`, `mcp add`, and `mcp remove`.
- Implements `Configure` as a toggle between register and unregister.
Use this only if the client exposes an official CLI for managing MCP servers.
---
## Summary
- **For most MCP clients**, you only need to:
- Create a `JsonFileMcpConfigurator` subclass in `Editor/Clients/Configurators`.
- Provide a `McpClient` with paths and flags.
- Override `GetInstallationSteps`.
- **Special cases** like Codex (TOML) and Claude Code (CLI) have dedicated base classes.
- **No manual registration** is needed: `McpClientRegistry` auto-discovers all configurators with a public parameterless constructor.
Following these patterns keeps all MCP client integrations consistent and lets users configure everything from the MCP for Unity window with minimal friction.