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
|
fileFormatVersion: 2
|
||||||
guid: 711b86bbc1f661e4fb2c822e14970e16
|
guid: 331b33961513042e3945d0a1d06615b5
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
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
|
fileFormatVersion: 2
|
||||||
guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1
|
guid: d0d22681fc594475db1c189f2d9abdf7
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
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 Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using MCPForUnity.Editor.Models;
|
using MCPForUnity.Editor.Models;
|
||||||
|
|
@ -13,16 +14,8 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
public static string BuildManualConfigJson(string uvPath, McpClient client)
|
public static string BuildManualConfigJson(string uvPath, McpClient client)
|
||||||
{
|
{
|
||||||
var root = new JObject();
|
var root = new JObject();
|
||||||
bool isVSCode = client?.mcpType == McpTypes.VSCode;
|
bool isVSCode = client?.IsVsCodeLayout == true;
|
||||||
JObject container;
|
JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
|
||||||
if (isVSCode)
|
|
||||||
{
|
|
||||||
container = EnsureObject(root, "servers");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
container = EnsureObject(root, "mcpServers");
|
|
||||||
}
|
|
||||||
|
|
||||||
var unity = new JObject();
|
var unity = new JObject();
|
||||||
PopulateUnityNode(unity, uvPath, client, isVSCode);
|
PopulateUnityNode(unity, uvPath, client, isVSCode);
|
||||||
|
|
@ -35,7 +28,7 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, McpClient client)
|
public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, McpClient client)
|
||||||
{
|
{
|
||||||
if (root == null) root = new JObject();
|
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 container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
|
||||||
JObject unity = container["unityMCP"] as JObject ?? new JObject();
|
JObject unity = container["unityMCP"] as JObject ?? new JObject();
|
||||||
PopulateUnityNode(unity, uvPath, client, isVSCode);
|
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)
|
private static void PopulateUnityNode(JObject unity, string uvPath, McpClient client, bool isVSCode)
|
||||||
{
|
{
|
||||||
// Get transport preference (default to HTTP)
|
// Get transport preference (default to HTTP)
|
||||||
bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
|
bool useHttpTransport = client?.SupportsHttpTransport != false && EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
|
||||||
bool isWindsurf = client?.mcpType == McpTypes.Windsurf;
|
string httpProperty = string.IsNullOrEmpty(client?.HttpUrlProperty) ? "url" : client.HttpUrlProperty;
|
||||||
|
var urlPropsToRemove = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "url", "serverUrl" };
|
||||||
|
urlPropsToRemove.Remove(httpProperty);
|
||||||
|
|
||||||
if (useHttpTransport)
|
if (useHttpTransport)
|
||||||
{
|
{
|
||||||
// HTTP mode: Use URL, no command
|
// HTTP mode: Use URL, no command
|
||||||
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
||||||
string httpProperty = isWindsurf ? "serverUrl" : "url";
|
|
||||||
unity[httpProperty] = httpUrl;
|
unity[httpProperty] = httpUrl;
|
||||||
|
|
||||||
// Remove legacy property for Windsurf (or vice versa)
|
foreach (var prop in urlPropsToRemove)
|
||||||
string staleProperty = isWindsurf ? "url" : "serverUrl";
|
|
||||||
if (unity[staleProperty] != null)
|
|
||||||
{
|
{
|
||||||
unity.Remove(staleProperty);
|
if (unity[prop] != null) unity.Remove(prop);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove command/args if they exist from previous config
|
// 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
|
// Remove url/serverUrl if they exist from previous config
|
||||||
if (unity["url"] != null) unity.Remove("url");
|
if (unity["url"] != null) unity.Remove("url");
|
||||||
if (unity["serverUrl"] != null) unity.Remove("serverUrl");
|
if (unity["serverUrl"] != null) unity.Remove("serverUrl");
|
||||||
|
foreach (var prop in urlPropsToRemove)
|
||||||
|
{
|
||||||
|
if (unity[prop] != null) unity.Remove(prop);
|
||||||
|
}
|
||||||
|
|
||||||
if (isVSCode)
|
if (isVSCode)
|
||||||
{
|
{
|
||||||
|
|
@ -115,8 +111,8 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
unity.Remove("type");
|
unity.Remove("type");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool requiresEnv = client?.mcpType == McpTypes.Kiro;
|
bool requiresEnv = client?.EnsureEnvObject == true;
|
||||||
bool requiresDisabled = client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro);
|
bool stripEnv = client?.StripEnvWhenNotRequired == true;
|
||||||
|
|
||||||
if (requiresEnv)
|
if (requiresEnv)
|
||||||
{
|
{
|
||||||
|
|
@ -125,14 +121,20 @@ namespace MCPForUnity.Editor.Helpers
|
||||||
unity["env"] = new JObject();
|
unity["env"] = new JObject();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (isWindsurf && unity["env"] != null)
|
else if (stripEnv && unity["env"] != null)
|
||||||
{
|
{
|
||||||
unity.Remove("env");
|
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)
|
// Determine existing entry references (command/args)
|
||||||
string existingCommand = null;
|
string existingCommand = null;
|
||||||
string[] existingArgs = null;
|
string[] existingArgs = null;
|
||||||
bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode);
|
bool isVSCode = (mcpClient?.IsVsCodeLayout == true);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (isVSCode)
|
if (isVSCode)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using MCPForUnity.Editor.Data;
|
using MCPForUnity.Editor.Clients;
|
||||||
using MCPForUnity.Editor.Helpers;
|
using MCPForUnity.Editor.Helpers;
|
||||||
using MCPForUnity.Editor.Models;
|
using MCPForUnity.Editor.Models;
|
||||||
using MCPForUnity.Editor.Services;
|
using MCPForUnity.Editor.Services;
|
||||||
|
|
@ -8,6 +8,7 @@ using Newtonsoft.Json.Linq;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using MCPForUnity.Editor.Constants;
|
using MCPForUnity.Editor.Constants;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Migrations
|
namespace MCPForUnity.Editor.Migrations
|
||||||
{
|
{
|
||||||
|
|
@ -48,21 +49,24 @@ namespace MCPForUnity.Editor.Migrations
|
||||||
bool hadFailures = false;
|
bool hadFailures = false;
|
||||||
bool touchedAny = false;
|
bool touchedAny = false;
|
||||||
|
|
||||||
var clients = new McpClients().clients;
|
var configurators = McpClientRegistry.All.OfType<McpClientConfiguratorBase>().ToList();
|
||||||
foreach (var client in clients)
|
foreach (var configurator in configurators)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!ConfigUsesStdIo(client))
|
if (!ConfigUsesStdIo(configurator.Client))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
MCPServiceLocator.Client.ConfigureClient(client);
|
if (!configurator.SupportsAutoConfigure)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
MCPServiceLocator.Client.ConfigureClient(configurator);
|
||||||
touchedAny = true;
|
touchedAny = true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
hadFailures = true;
|
hadFailures = true;
|
||||||
McpLog.Warn($"Failed to refresh stdio config for {client.name}: {ex.Message}");
|
McpLog.Warn($"Failed to refresh stdio config for {configurator.DisplayName}: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,13 +94,7 @@ namespace MCPForUnity.Editor.Migrations
|
||||||
|
|
||||||
private static bool ConfigUsesStdIo(McpClient client)
|
private static bool ConfigUsesStdIo(McpClient client)
|
||||||
{
|
{
|
||||||
switch (client.mcpType)
|
return JsonConfigUsesStdIo(client);
|
||||||
{
|
|
||||||
case McpTypes.Codex:
|
|
||||||
return CodexConfigUsesStdIo(client);
|
|
||||||
default:
|
|
||||||
return JsonConfigUsesStdIo(client);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool JsonConfigUsesStdIo(McpClient client)
|
private static bool JsonConfigUsesStdIo(McpClient client)
|
||||||
|
|
@ -112,7 +110,7 @@ namespace MCPForUnity.Editor.Migrations
|
||||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||||
|
|
||||||
JToken unityNode = null;
|
JToken unityNode = null;
|
||||||
if (client.mcpType == McpTypes.VSCode)
|
if (client.IsVsCodeLayout)
|
||||||
{
|
{
|
||||||
unityNode = root.SelectToken("servers.unityMCP")
|
unityNode = root.SelectToken("servers.unityMCP")
|
||||||
?? root.SelectToken("mcp.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
|
namespace MCPForUnity.Editor.Models
|
||||||
{
|
{
|
||||||
public class McpClient
|
public class McpClient
|
||||||
|
|
@ -6,10 +8,17 @@ namespace MCPForUnity.Editor.Models
|
||||||
public string windowsConfigPath;
|
public string windowsConfigPath;
|
||||||
public string macConfigPath;
|
public string macConfigPath;
|
||||||
public string linuxConfigPath;
|
public string linuxConfigPath;
|
||||||
public McpTypes mcpType;
|
|
||||||
public string configStatus;
|
public string configStatus;
|
||||||
public McpStatus status = McpStatus.NotConfigured;
|
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
|
// Helper method to convert the enum to a display string
|
||||||
public string GetStatusDisplayString()
|
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;
|
||||||
using System.IO;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using MCPForUnity.Editor.Clients;
|
||||||
using MCPForUnity.Editor.Constants;
|
|
||||||
using MCPForUnity.Editor.Data;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
|
||||||
using MCPForUnity.Editor.Models;
|
using MCPForUnity.Editor.Models;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Services
|
namespace MCPForUnity.Editor.Services
|
||||||
{
|
{
|
||||||
|
|
@ -18,565 +11,49 @@ namespace MCPForUnity.Editor.Services
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ClientConfigurationService : IClientConfigurationService
|
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;
|
configurators = McpClientRegistry.All.ToList();
|
||||||
string uvxPath = pathService.GetUvxPath();
|
}
|
||||||
|
|
||||||
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
|
public IReadOnlyList<IMcpClientConfigurator> GetAllClients() => configurators;
|
||||||
McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
|
|
||||||
|
|
||||||
string result = client.mcpType == McpTypes.Codex
|
public void ConfigureClient(IMcpClientConfigurator configurator)
|
||||||
? McpConfigurationHelper.ConfigureCodexClient(configPath, client)
|
{
|
||||||
: McpConfigurationHelper.WriteMcpConfiguration(configPath, client);
|
configurator.Configure();
|
||||||
|
|
||||||
if (result == "Configured successfully")
|
|
||||||
{
|
|
||||||
client.SetStatus(McpStatus.Configured);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.SetStatus(McpStatus.NotConfigured);
|
|
||||||
throw new InvalidOperationException($"Configuration failed: {result}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientConfigurationSummary ConfigureAllDetectedClients()
|
public ClientConfigurationSummary ConfigureAllDetectedClients()
|
||||||
{
|
{
|
||||||
var summary = new ClientConfigurationSummary();
|
var summary = new ClientConfigurationSummary();
|
||||||
var pathService = MCPServiceLocator.Paths;
|
foreach (var configurator in configurators)
|
||||||
|
|
||||||
foreach (var client in mcpClients.clients)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Always re-run configuration so core fields stay current
|
// Always re-run configuration so core fields stay current
|
||||||
CheckClientStatus(client, attemptAutoRewrite: false);
|
configurator.CheckStatus(attemptAutoRewrite: false);
|
||||||
|
configurator.Configure();
|
||||||
// Check if required tools are available
|
summary.SuccessCount++;
|
||||||
if (client.mcpType == McpTypes.ClaudeCode)
|
summary.Messages.Add($"✓ {configurator.DisplayName}: Configured successfully");
|
||||||
{
|
|
||||||
if (!pathService.IsClaudeCliDetected())
|
|
||||||
{
|
|
||||||
summary.SkippedCount++;
|
|
||||||
summary.Messages.Add($"➜ {client.name}: Claude CLI not found");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force a fresh registration so transport settings stay current
|
|
||||||
UnregisterClaudeCode();
|
|
||||||
RegisterClaudeCode();
|
|
||||||
summary.SuccessCount++;
|
|
||||||
summary.Messages.Add($"✓ {client.name}: Re-registered successfully");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ConfigureClient(client);
|
|
||||||
summary.SuccessCount++;
|
|
||||||
summary.Messages.Add($"✓ {client.name}: Configured successfully");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
summary.FailureCount++;
|
summary.FailureCount++;
|
||||||
summary.Messages.Add($"⚠ {client.name}: {ex.Message}");
|
summary.Messages.Add($"⚠ {configurator.DisplayName}: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true)
|
public bool CheckClientStatus(IMcpClientConfigurator configurator, bool attemptAutoRewrite = true)
|
||||||
{
|
{
|
||||||
var previousStatus = client.status;
|
var previous = configurator.Status;
|
||||||
|
var current = configurator.CheckStatus(attemptAutoRewrite);
|
||||||
try
|
return current != previous;
|
||||||
{
|
|
||||||
// Special handling for Claude Code
|
|
||||||
if (client.mcpType == McpTypes.ClaudeCode)
|
|
||||||
{
|
|
||||||
CheckClaudeCodeConfiguration(client);
|
|
||||||
return client.status != previousStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
|
|
||||||
|
|
||||||
if (!File.Exists(configPath))
|
|
||||||
{
|
|
||||||
client.SetStatus(McpStatus.NotConfigured);
|
|
||||||
return client.status != previousStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
string configJson = File.ReadAllText(configPath);
|
|
||||||
// Check configuration based on client type
|
|
||||||
string[] args = null;
|
|
||||||
string configuredUrl = null;
|
|
||||||
bool configExists = false;
|
|
||||||
|
|
||||||
switch (client.mcpType)
|
|
||||||
{
|
|
||||||
case McpTypes.VSCode:
|
|
||||||
var vsConfig = JsonConvert.DeserializeObject<JToken>(configJson) as JObject;
|
|
||||||
if (vsConfig != null)
|
|
||||||
{
|
|
||||||
var unityToken =
|
|
||||||
vsConfig["servers"]?["unityMCP"]
|
|
||||||
?? vsConfig["mcp"]?["servers"]?["unityMCP"];
|
|
||||||
|
|
||||||
if (unityToken is JObject unityObj)
|
|
||||||
{
|
|
||||||
configExists = true;
|
|
||||||
|
|
||||||
var argsToken = unityObj["args"];
|
|
||||||
if (argsToken is JArray)
|
|
||||||
{
|
|
||||||
args = argsToken.ToObject<string[]>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var urlToken = unityObj["url"] ?? unityObj["serverUrl"];
|
|
||||||
if (urlToken != null && urlToken.Type != JTokenType.Null)
|
|
||||||
{
|
|
||||||
configuredUrl = urlToken.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case McpTypes.Codex:
|
|
||||||
if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs, out var codexUrl))
|
|
||||||
{
|
|
||||||
args = codexArgs;
|
|
||||||
configuredUrl = codexUrl;
|
|
||||||
configExists = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
McpConfig standardConfig = JsonConvert.DeserializeObject<McpConfig>(configJson);
|
|
||||||
if (standardConfig?.mcpServers?.unityMCP != null)
|
|
||||||
{
|
|
||||||
args = standardConfig.mcpServers.unityMCP.args;
|
|
||||||
configExists = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configExists)
|
|
||||||
{
|
|
||||||
bool matches = false;
|
|
||||||
|
|
||||||
if (args != null && args.Length > 0)
|
|
||||||
{
|
|
||||||
string expectedUvxUrl = AssetPathUtility.GetMcpServerGitUrl();
|
|
||||||
string configuredUvxUrl = McpConfigurationHelper.ExtractUvxUrl(args);
|
|
||||||
matches = !string.IsNullOrEmpty(configuredUvxUrl) &&
|
|
||||||
McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl);
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(configuredUrl))
|
|
||||||
{
|
|
||||||
string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
|
||||||
matches = UrlsEqual(configuredUrl, expectedUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matches)
|
|
||||||
{
|
|
||||||
client.SetStatus(McpStatus.Configured);
|
|
||||||
}
|
|
||||||
else if (attemptAutoRewrite)
|
|
||||||
{
|
|
||||||
// Attempt auto-rewrite if path mismatch detected
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string rewriteResult = client.mcpType == McpTypes.Codex
|
|
||||||
? McpConfigurationHelper.ConfigureCodexClient(configPath, client)
|
|
||||||
: McpConfigurationHelper.WriteMcpConfiguration(configPath, client);
|
|
||||||
|
|
||||||
if (rewriteResult == "Configured successfully")
|
|
||||||
{
|
|
||||||
bool debugLogsEnabled = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
|
|
||||||
if (debugLogsEnabled)
|
|
||||||
{
|
|
||||||
string targetDescriptor = args != null && args.Length > 0
|
|
||||||
? AssetPathUtility.GetMcpServerGitUrl()
|
|
||||||
: HttpEndpointUtility.GetMcpRpcUrl();
|
|
||||||
McpLog.Info($"Auto-updated MCP config for '{client.name}' to new version: {targetDescriptor}", always: false);
|
|
||||||
}
|
|
||||||
client.SetStatus(McpStatus.Configured);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.SetStatus(McpStatus.IncorrectPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
client.SetStatus(McpStatus.IncorrectPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.SetStatus(McpStatus.IncorrectPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.SetStatus(McpStatus.MissingConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
client.SetStatus(McpStatus.Error, ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.status != previousStatus;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
using MCPForUnity.Editor.Models;
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Services
|
namespace MCPForUnity.Editor.Services
|
||||||
|
|
@ -11,7 +13,7 @@ namespace MCPForUnity.Editor.Services
|
||||||
/// Configures a specific MCP client
|
/// Configures a specific MCP client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="client">The client to configure</param>
|
/// <param name="client">The client to configure</param>
|
||||||
void ConfigureClient(McpClient client);
|
void ConfigureClient(IMcpClientConfigurator configurator);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures all detected/installed MCP clients (skips clients where CLI/tools not found)
|
/// 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="client">The client to check</param>
|
||||||
/// <param name="attemptAutoRewrite">If true, attempts to auto-fix mismatched paths</param>
|
/// <param name="attemptAutoRewrite">If true, attempts to auto-fix mismatched paths</param>
|
||||||
/// <returns>True if status changed, false otherwise</returns>
|
/// <returns>True if status changed, false otherwise</returns>
|
||||||
bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true);
|
bool CheckClientStatus(IMcpClientConfigurator configurator, bool attemptAutoRewrite = true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Gets the registry of discovered configurators.</summary>
|
||||||
/// Registers MCP for Unity with Claude Code CLI
|
IReadOnlyList<IMcpClientConfigurator> GetAllClients();
|
||||||
/// </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>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MCPForUnity.Editor.Constants;
|
using MCPForUnity.Editor.Constants;
|
||||||
using MCPForUnity.Editor.Data;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
using MCPForUnity.Editor.Helpers;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -6,7 +7,7 @@ using System.Runtime.InteropServices;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UIElements;
|
using UnityEngine.UIElements;
|
||||||
using MCPForUnity.Editor.Data;
|
using MCPForUnity.Editor.Clients;
|
||||||
using MCPForUnity.Editor.Helpers;
|
using MCPForUnity.Editor.Helpers;
|
||||||
using MCPForUnity.Editor.Models;
|
using MCPForUnity.Editor.Models;
|
||||||
using MCPForUnity.Editor.Services;
|
using MCPForUnity.Editor.Services;
|
||||||
|
|
@ -36,15 +37,15 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
||||||
private Label installationStepsLabel;
|
private Label installationStepsLabel;
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
private readonly McpClients mcpClients;
|
private readonly List<IMcpClientConfigurator> configurators;
|
||||||
private int selectedClientIndex = 0;
|
private int selectedClientIndex = 0;
|
||||||
|
|
||||||
public VisualElement Root { get; private set; }
|
public VisualElement Root { get; private set; }
|
||||||
|
|
||||||
public McpClientConfigSection(VisualElement root, McpClients clients)
|
public McpClientConfigSection(VisualElement root)
|
||||||
{
|
{
|
||||||
Root = root;
|
Root = root;
|
||||||
mcpClients = clients;
|
configurators = MCPServiceLocator.Client.GetAllClients().ToList();
|
||||||
CacheUIElements();
|
CacheUIElements();
|
||||||
InitializeUI();
|
InitializeUI();
|
||||||
RegisterCallbacks();
|
RegisterCallbacks();
|
||||||
|
|
@ -70,7 +71,7 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
||||||
|
|
||||||
private void InitializeUI()
|
private void InitializeUI()
|
||||||
{
|
{
|
||||||
var clientNames = mcpClients.clients.Select(c => c.name).ToList();
|
var clientNames = configurators.Select(c => c.DisplayName).ToList();
|
||||||
clientDropdown.choices = clientNames;
|
clientDropdown.choices = clientNames;
|
||||||
if (clientNames.Count > 0)
|
if (clientNames.Count > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -100,20 +101,20 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
||||||
|
|
||||||
public void UpdateClientStatus()
|
public void UpdateClientStatus()
|
||||||
{
|
{
|
||||||
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
|
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var client = mcpClients.clients[selectedClientIndex];
|
var client = configurators[selectedClientIndex];
|
||||||
MCPServiceLocator.Client.CheckClientStatus(client);
|
MCPServiceLocator.Client.CheckClientStatus(client);
|
||||||
|
|
||||||
clientStatusLabel.text = client.GetStatusDisplayString();
|
clientStatusLabel.text = GetStatusDisplayString(client.Status);
|
||||||
clientStatusLabel.style.color = StyleKeyword.Null;
|
clientStatusLabel.style.color = StyleKeyword.Null;
|
||||||
|
|
||||||
clientStatusIndicator.RemoveFromClassList("configured");
|
clientStatusIndicator.RemoveFromClassList("configured");
|
||||||
clientStatusIndicator.RemoveFromClassList("not-configured");
|
clientStatusIndicator.RemoveFromClassList("not-configured");
|
||||||
clientStatusIndicator.RemoveFromClassList("warning");
|
clientStatusIndicator.RemoveFromClassList("warning");
|
||||||
|
|
||||||
switch (client.status)
|
switch (client.Status)
|
||||||
{
|
{
|
||||||
case McpStatus.Configured:
|
case McpStatus.Configured:
|
||||||
case McpStatus.Running:
|
case McpStatus.Running:
|
||||||
|
|
@ -130,42 +131,60 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.mcpType == McpTypes.ClaudeCode)
|
configureButton.text = client.GetConfigureActionLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetStatusDisplayString(McpStatus status)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
{
|
{
|
||||||
bool isConfigured = client.status == McpStatus.Configured;
|
McpStatus.NotConfigured => "Not Configured",
|
||||||
configureButton.text = isConfigured ? "Unregister" : "Register";
|
McpStatus.Configured => "Configured",
|
||||||
}
|
McpStatus.Running => "Running",
|
||||||
else
|
McpStatus.Connected => "Connected",
|
||||||
{
|
McpStatus.IncorrectPath => "Incorrect Path",
|
||||||
configureButton.text = "Configure";
|
McpStatus.CommunicationError => "Communication Error",
|
||||||
}
|
McpStatus.NoResponse => "No Response",
|
||||||
|
McpStatus.UnsupportedOS => "Unsupported OS",
|
||||||
|
McpStatus.MissingConfig => "Missing MCPForUnity Config",
|
||||||
|
McpStatus.Error => "Error",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateManualConfiguration()
|
public void UpdateManualConfiguration()
|
||||||
{
|
{
|
||||||
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
|
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var client = mcpClients.clients[selectedClientIndex];
|
var client = configurators[selectedClientIndex];
|
||||||
|
|
||||||
string configPath = MCPServiceLocator.Client.GetConfigPath(client);
|
string configPath = client.GetConfigPath();
|
||||||
configPathField.value = configPath;
|
configPathField.value = configPath;
|
||||||
|
|
||||||
string configJson = MCPServiceLocator.Client.GenerateConfigJson(client);
|
string configJson = client.GetManualSnippet();
|
||||||
configJsonField.value = configJson;
|
configJsonField.value = configJson;
|
||||||
|
|
||||||
string steps = MCPServiceLocator.Client.GetInstallationSteps(client);
|
var steps = client.GetInstallationSteps();
|
||||||
installationStepsLabel.text = steps;
|
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()
|
private void UpdateClaudeCliPathVisibility()
|
||||||
{
|
{
|
||||||
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
|
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var client = mcpClients.clients[selectedClientIndex];
|
var client = configurators[selectedClientIndex];
|
||||||
|
|
||||||
if (client.mcpType == McpTypes.ClaudeCode)
|
if (client is ClaudeCliMcpConfigurator)
|
||||||
{
|
{
|
||||||
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
||||||
if (string.IsNullOrEmpty(claudePath))
|
if (string.IsNullOrEmpty(claudePath))
|
||||||
|
|
@ -199,7 +218,7 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
||||||
|
|
||||||
EditorUtility.DisplayDialog("Configure All Clients", message, "OK");
|
EditorUtility.DisplayDialog("Configure All Clients", message, "OK");
|
||||||
|
|
||||||
if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count)
|
if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count)
|
||||||
{
|
{
|
||||||
UpdateClientStatus();
|
UpdateClientStatus();
|
||||||
UpdateManualConfiguration();
|
UpdateManualConfiguration();
|
||||||
|
|
@ -213,30 +232,14 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
||||||
|
|
||||||
private void OnConfigureClicked()
|
private void OnConfigureClicked()
|
||||||
{
|
{
|
||||||
if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count)
|
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var client = mcpClients.clients[selectedClientIndex];
|
var client = configurators[selectedClientIndex];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (client.mcpType == McpTypes.ClaudeCode)
|
MCPServiceLocator.Client.ConfigureClient(client);
|
||||||
{
|
|
||||||
bool isConfigured = client.status == McpStatus.Configured;
|
|
||||||
if (isConfigured)
|
|
||||||
{
|
|
||||||
MCPServiceLocator.Client.UnregisterClaudeCode();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MCPServiceLocator.Client.RegisterClaudeCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MCPServiceLocator.Client.ConfigureClient(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateClientStatus();
|
UpdateClientStatus();
|
||||||
UpdateManualConfiguration();
|
UpdateManualConfiguration();
|
||||||
}
|
}
|
||||||
|
|
@ -308,9 +311,9 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
||||||
|
|
||||||
public void RefreshSelectedClient()
|
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);
|
MCPServiceLocator.Client.CheckClientStatus(client);
|
||||||
UpdateClientStatus();
|
UpdateClientStatus();
|
||||||
UpdateManualConfiguration();
|
UpdateManualConfiguration();
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,8 @@ using System.Threading.Tasks;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UIElements;
|
using UnityEngine.UIElements;
|
||||||
using MCPForUnity.Editor.Data;
|
using MCPForUnity.Editor.Helpers;
|
||||||
using MCPForUnity.Editor.Helpers;
|
using MCPForUnity.Editor.Services;
|
||||||
using MCPForUnity.Editor.Services;
|
|
||||||
using MCPForUnity.Editor.Windows.Components.Settings;
|
using MCPForUnity.Editor.Windows.Components.Settings;
|
||||||
using MCPForUnity.Editor.Windows.Components.Connection;
|
using MCPForUnity.Editor.Windows.Components.Connection;
|
||||||
using MCPForUnity.Editor.Windows.Components.ClientConfig;
|
using MCPForUnity.Editor.Windows.Components.ClientConfig;
|
||||||
|
|
@ -20,9 +19,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
private McpConnectionSection connectionSection;
|
private McpConnectionSection connectionSection;
|
||||||
private McpClientConfigSection clientConfigSection;
|
private McpClientConfigSection clientConfigSection;
|
||||||
|
|
||||||
// Data
|
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
|
||||||
private readonly McpClients mcpClients = new();
|
|
||||||
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
|
|
||||||
|
|
||||||
public static void ShowWindow()
|
public static void ShowWindow()
|
||||||
{
|
{
|
||||||
|
|
@ -105,8 +102,8 @@ namespace MCPForUnity.Editor.Windows
|
||||||
{
|
{
|
||||||
var clientConfigRoot = clientConfigTree.Instantiate();
|
var clientConfigRoot = clientConfigTree.Instantiate();
|
||||||
sectionsContainer.Add(clientConfigRoot);
|
sectionsContainer.Add(clientConfigRoot);
|
||||||
clientConfigSection = new McpClientConfigSection(clientConfigRoot, mcpClients);
|
clientConfigSection = new McpClientConfigSection(clientConfigRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial updates
|
// Initial updates
|
||||||
RefreshAllData();
|
RefreshAllData();
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,11 @@ What changed and why:
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import socket
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
from models.models import UnityInstanceInfo
|
from models.models import UnityInstanceInfo
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,13 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
var configPath = Path.Combine(_tempRoot, "windsurf.json");
|
var configPath = Path.Combine(_tempRoot, "windsurf.json");
|
||||||
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
|
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);
|
InvokeWriteToConfig(configPath, client);
|
||||||
|
|
||||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||||
|
|
@ -84,7 +90,7 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||||
Assert.IsNull(unity["env"], "Windsurf configs should not include an env block");
|
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");
|
Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Windsurf when missing");
|
||||||
AssertTransportConfiguration(unity, McpTypes.Windsurf);
|
AssertTransportConfiguration(unity, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -93,7 +99,12 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
var configPath = Path.Combine(_tempRoot, "kiro.json");
|
var configPath = Path.Combine(_tempRoot, "kiro.json");
|
||||||
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
|
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);
|
InvokeWriteToConfig(configPath, client);
|
||||||
|
|
||||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
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.NotNull(unity["env"], "env should be present for all clients");
|
||||||
Assert.IsTrue(unity["env"]!.Type == JTokenType.Object, "env should be an object");
|
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");
|
Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Kiro when missing");
|
||||||
AssertTransportConfiguration(unity, McpTypes.Kiro);
|
AssertTransportConfiguration(unity, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -111,7 +122,7 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
var configPath = Path.Combine(_tempRoot, "cursor.json");
|
var configPath = Path.Combine(_tempRoot, "cursor.json");
|
||||||
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
|
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);
|
InvokeWriteToConfig(configPath, client);
|
||||||
|
|
||||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||||
|
|
@ -119,7 +130,7 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||||
Assert.IsNull(unity["env"], "env should not be added for non-Windsurf/Kiro clients");
|
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");
|
Assert.IsNull(unity["disabled"], "disabled should not be added for non-Windsurf/Kiro clients");
|
||||||
AssertTransportConfiguration(unity, McpTypes.Cursor);
|
AssertTransportConfiguration(unity, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -128,7 +139,7 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
var configPath = Path.Combine(_tempRoot, "vscode.json");
|
var configPath = Path.Combine(_tempRoot, "vscode.json");
|
||||||
WriteInitialConfig(configPath, isVSCode: true, command: _fakeUvPath, directory: "/old/path");
|
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);
|
InvokeWriteToConfig(configPath, client);
|
||||||
|
|
||||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||||
|
|
@ -136,7 +147,7 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
Assert.NotNull(unity, "Expected servers.unityMCP node");
|
Assert.NotNull(unity, "Expected servers.unityMCP node");
|
||||||
Assert.IsNull(unity["env"], "env should not be added for VSCode client");
|
Assert.IsNull(unity["env"], "env should not be added for VSCode client");
|
||||||
Assert.IsNull(unity["disabled"], "disabled 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]
|
[Test]
|
||||||
|
|
@ -145,12 +156,7 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
var configPath = Path.Combine(_tempRoot, "trae.json");
|
var configPath = Path.Combine(_tempRoot, "trae.json");
|
||||||
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
|
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
|
||||||
|
|
||||||
if (!Enum.TryParse<McpTypes>("Trae", out var traeValue))
|
var client = new McpClient { name = "Trae" };
|
||||||
{
|
|
||||||
Assert.Ignore("McpTypes.Trae not available in this package version; skipping test.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = new McpClient { name = "Trae", mcpType = traeValue };
|
|
||||||
InvokeWriteToConfig(configPath, client);
|
InvokeWriteToConfig(configPath, client);
|
||||||
|
|
||||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||||
|
|
@ -158,7 +164,7 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||||
Assert.IsNull(unity["env"], "env should not be added for Trae client");
|
Assert.IsNull(unity["env"], "env should not be added for Trae client");
|
||||||
Assert.IsNull(unity["disabled"], "disabled 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]
|
[Test]
|
||||||
|
|
@ -182,7 +188,12 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
};
|
};
|
||||||
File.WriteAllText(configPath, json.ToString());
|
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);
|
InvokeWriteToConfig(configPath, client);
|
||||||
|
|
||||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||||
|
|
@ -190,7 +201,7 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||||
Assert.AreEqual("bar", (string)unity["env"]!["FOO"], "Existing env should be preserved");
|
Assert.AreEqual("bar", (string)unity["env"]!["FOO"], "Existing env should be preserved");
|
||||||
Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved");
|
Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved");
|
||||||
AssertTransportConfiguration(unity, McpTypes.Kiro);
|
AssertTransportConfiguration(unity, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -213,7 +224,13 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
};
|
};
|
||||||
File.WriteAllText(configPath, json.ToString());
|
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);
|
InvokeWriteToConfig(configPath, client);
|
||||||
|
|
||||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||||
|
|
@ -221,7 +238,7 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||||
Assert.IsNull(unity["env"], "Windsurf config should strip any existing env block");
|
Assert.IsNull(unity["env"], "Windsurf config should strip any existing env block");
|
||||||
Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved");
|
Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved");
|
||||||
AssertTransportConfiguration(unity, McpTypes.Windsurf);
|
AssertTransportConfiguration(unity, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -232,13 +249,19 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
|
|
||||||
WithTransportPreference(false, () =>
|
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);
|
InvokeWriteToConfig(configPath, client);
|
||||||
|
|
||||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||||
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
||||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||||
AssertTransportConfiguration(unity, McpTypes.Windsurf);
|
AssertTransportConfiguration(unity, client);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,13 +273,13 @@ namespace MCPForUnityTests.Editor.Helpers
|
||||||
|
|
||||||
WithTransportPreference(false, () =>
|
WithTransportPreference(false, () =>
|
||||||
{
|
{
|
||||||
var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode };
|
var client = new McpClient { name = "VSCode", IsVsCodeLayout = true };
|
||||||
InvokeWriteToConfig(configPath, client);
|
InvokeWriteToConfig(configPath, client);
|
||||||
|
|
||||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||||
var unity = (JObject)root.SelectToken("servers.unityMCP");
|
var unity = (JObject)root.SelectToken("servers.unityMCP");
|
||||||
Assert.NotNull(unity, "Expected servers.unityMCP node");
|
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");
|
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 useHttp = EditorPrefs.GetBool(UseHttpTransportPrefKey, true);
|
||||||
bool isVSCode = clientType == McpTypes.VSCode;
|
bool isVSCode = client.IsVsCodeLayout;
|
||||||
bool isWindsurf = clientType == McpTypes.Windsurf;
|
bool isWindsurf = string.Equals(client.HttpUrlProperty, "serverUrl", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (useHttp)
|
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