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 fashionmain
parent
7b25f7ce3e
commit
f94cb2460a
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c9d47f01d06964ee7843765d1bd71205
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 59ff83375c2c74c8385c4a22549778dd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 711b86bbc1f661e4fb2c822e14970e16
|
||||
guid: 331b33961513042e3945d0a1d06615b5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1
|
||||
guid: d0d22681fc594475db1c189f2d9abdf7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d5e5d87c9db57495f842dc366f1ebd65
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c7037ef8b168e49f79247cb31c3be75a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b708eda314746481fb8f4a1fb0652b03
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e9b73ff071a6043dda1f2ec7d682ef71
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b3ab39e22ae0948ab94beae307f9902e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bcc7ead475a4d4ea2978151c217757b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b528971e189f141d38db577f155bd222
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f5a5078d9e6e14027a1abfebf4018634
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8d408fd7733cb4a1eb80f785307db2ff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4ce08555f995e4e848a826c63f18cb35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,14 +94,8 @@ namespace MCPForUnity.Editor.Migrations
|
|||
|
||||
private static bool ConfigUsesStdIo(McpClient client)
|
||||
{
|
||||
switch (client.mcpType)
|
||||
{
|
||||
case McpTypes.Codex:
|
||||
return CodexConfigUsesStdIo(client);
|
||||
default:
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
namespace MCPForUnity.Editor.Models
|
||||
{
|
||||
public enum McpTypes
|
||||
{
|
||||
ClaudeCode,
|
||||
ClaudeDesktop,
|
||||
Codex,
|
||||
Cursor,
|
||||
Kiro,
|
||||
VSCode,
|
||||
Windsurf,
|
||||
Trae,
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
|
||||
McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
|
||||
|
||||
string result = client.mcpType == McpTypes.Codex
|
||||
? McpConfigurationHelper.ConfigureCodexClient(configPath, client)
|
||||
: McpConfigurationHelper.WriteMcpConfiguration(configPath, client);
|
||||
|
||||
if (result == "Configured successfully")
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
configurators = McpClientRegistry.All.ToList();
|
||||
}
|
||||
else
|
||||
|
||||
public IReadOnlyList<IMcpClientConfigurator> GetAllClients() => configurators;
|
||||
|
||||
public void ConfigureClient(IMcpClientConfigurator configurator)
|
||||
{
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
throw new InvalidOperationException($"Configuration failed: {result}");
|
||||
}
|
||||
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();
|
||||
configurator.CheckStatus(attemptAutoRewrite: false);
|
||||
configurator.Configure();
|
||||
summary.SuccessCount++;
|
||||
summary.Messages.Add($"✓ {client.name}: Re-registered successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
ConfigureClient(client);
|
||||
summary.SuccessCount++;
|
||||
summary.Messages.Add($"✓ {client.name}: Configured successfully");
|
||||
}
|
||||
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;
|
||||
var previous = configurator.Status;
|
||||
var current = configurator.CheckStatus(attemptAutoRewrite);
|
||||
return current != previous;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
bool isConfigured = client.status == McpStatus.Configured;
|
||||
configureButton.text = isConfigured ? "Unregister" : "Register";
|
||||
configureButton.text = client.GetConfigureActionLabel();
|
||||
}
|
||||
else
|
||||
|
||||
private string GetStatusDisplayString(McpStatus status)
|
||||
{
|
||||
configureButton.text = "Configure";
|
||||
}
|
||||
return status switch
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ 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.Windows.Components.Settings;
|
||||
|
|
@ -20,8 +19,6 @@ namespace MCPForUnity.Editor.Windows
|
|||
private McpConnectionSection connectionSection;
|
||||
private McpClientConfigSection clientConfigSection;
|
||||
|
||||
// Data
|
||||
private readonly McpClients mcpClients = new();
|
||||
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
|
||||
|
||||
public static void ShowWindow()
|
||||
|
|
@ -105,7 +102,7 @@ namespace MCPForUnity.Editor.Windows
|
|||
{
|
||||
var clientConfigRoot = clientConfigTree.Instantiate();
|
||||
sectionsContainer.Add(clientConfigRoot);
|
||||
clientConfigSection = new McpClientConfigSection(clientConfigRoot, mcpClients);
|
||||
clientConfigSection = new McpClientConfigSection(clientConfigRoot);
|
||||
}
|
||||
|
||||
// Initial updates
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Reference in New Issue