Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
Expand Down Expand Up @@ -179,19 +180,21 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
JToken unityToken = null;
if (rootConfig != null)
{
string containerKey = string.IsNullOrEmpty(client.ServerContainerKey)
? "mcpServers" : client.ServerContainerKey;
unityToken = client.IsVsCodeLayout
? rootConfig["servers"]?["unityMCP"]
?? rootConfig["mcp"]?["servers"]?["unityMCP"]
: rootConfig[containerKey]?["unityMCP"];
unityToken = McpConfigurationHelper.GetJsonUnityEntry(rootConfig, client);
}

if (unityToken is JObject unityObj)
{
configExists = true;

var argsToken = unityObj["args"];
if (argsToken != null && (!(argsToken is JArray argsArray)
|| argsArray.Any(value => value.Type != JTokenType.String)))
{
client.SetStatus(McpStatus.Error, "The stdio args field must be an array of strings.");
client.configuredTransport = Models.ConfiguredTransport.Unknown;
return client.status;
}
if (argsToken is JArray)
{
args = argsToken.ToObject<string[]>();
Expand All @@ -200,10 +203,13 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
// Clients diverge on the HTTP URL property name: "url" (Cursor/VSCode/Claude),
// "serverUrl" (Antigravity/Windsurf), "httpUrl" (Gemini CLI). Accept all three
// so CheckStatus matches what Configure() actually wrote.
var urlToken = unityObj["url"] ?? unityObj["serverUrl"] ?? unityObj["httpUrl"];
if (urlToken != null && urlToken.Type != JTokenType.Null)
configuredUrl = McpConfigurationHelper.GetConfiguredHttpUrl(unityObj);

if (McpConfigurationHelper.HasCustomStdioLauncher(unityObj, client))
{
configuredUrl = urlToken.ToString();
client.SetStatus(McpStatus.Configured);
client.configuredTransport = Models.ConfiguredTransport.Stdio;
return client.status;
}
}
}
Expand Down Expand Up @@ -339,6 +345,14 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
return client.status;
}

internal bool HasCustomStdioConfiguration()
{
string path = GetConfigPath();
if (!File.Exists(path)) return false;
var entry = McpConfigurationHelper.GetJsonUnityEntry(JObject.Parse(File.ReadAllText(path)), client);
return McpConfigurationHelper.HasCustomStdioLauncher(entry, client);
}

public override void Configure()
{
// Always idempotent-write. The per-client UI button routes through Unregister
Expand Down
78 changes: 78 additions & 0 deletions MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,84 @@ public static void EnsureConfigDirectoryExists(string configPath)
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
}

internal static JObject GetJsonUnityEntry(JObject root, McpClient client)
{
string container = string.IsNullOrEmpty(client.ServerContainerKey)
? "mcpServers" : client.ServerContainerKey;
return (client.IsVsCodeLayout
? root["servers"]?["unityMCP"] ?? root["mcp"]?["servers"]?["unityMCP"]
: root[container]?["unityMCP"]) as JObject;
}

internal static string GetConfiguredHttpUrl(JObject entry)
{
foreach (string key in new[] { "url", "serverUrl", "httpUrl" })
{
var value = entry[key];
if (value != null && value.Type != JTokenType.Null
&& !string.IsNullOrWhiteSpace(value.ToString()))
return value.ToString();
}
return null;
}

internal static bool HasCustomStdioLauncher(JObject entry, McpClient client)
{
var command = entry?["command"];
if (command?.Type != JTokenType.String
|| string.IsNullOrWhiteSpace(command.Value<string>())
|| GetConfiguredHttpUrl(entry) != null)
return false;

var argsToken = entry["args"];
if (argsToken != null && (!(argsToken is JArray argsArray)
|| argsArray.Any(value => value.Type != JTokenType.String)))
return false;

var type = entry["type"];
if (type != null && (type.Type != JTokenType.String
|| !(string.Equals(type.Value<string>(), "stdio", StringComparison.OrdinalIgnoreCase)
|| string.Equals(type.Value<string>(), client.StdioTypeValue, StringComparison.OrdinalIgnoreCase))))
return false;

string executable = Path.GetFileName(command.Value<string>().Trim().Replace('\\', '/'));
if (string.Equals(executable, "uvx", StringComparison.OrdinalIgnoreCase)
|| string.Equals(executable, "uvx.exe", StringComparison.OrdinalIgnoreCase))
return false;

bool uv = string.Equals(executable, "uv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(executable, "uv.exe", StringComparison.OrdinalIgnoreCase);
return !(uv && IsUvToolRun(argsToken as JArray));
}

private static bool IsUvToolRun(JArray args)
{
if (args == null) return false;
int index = SkipUvGlobalOptions(args, 0);
if (index >= args.Count || !string.Equals(args[index].Value<string>(), "tool", StringComparison.OrdinalIgnoreCase))
return false;
index = SkipUvGlobalOptions(args, index + 1);
return index < args.Count && string.Equals(args[index].Value<string>(), "run", StringComparison.OrdinalIgnoreCase);
}

private static int SkipUvGlobalOptions(JArray args, int index)
{
while (index < args.Count)
{
string option = args[index].Value<string>();
if (option == "--") return index + 1;
if (!option.StartsWith("-", StringComparison.Ordinal)) break;
index++;
// uv's global options with a separate value; --option=value needs no extra skip.
if (option == "--cache-dir" || option == "--color" || option == "--allow-insecure-host"
|| option == "--trusted-host" || option == "--directory" || option == "--project"
|| option == "--config-file" || option == "--python-preference" || option == "--python-fetch"
|| option == "--preview-features" || option == "--preview-feature")
index++;
}
return index;
}

public static string ExtractUvxUrl(string[] args)
{
if (args == null) return null;
Expand Down
46 changes: 45 additions & 1 deletion MCPForUnity/Editor/Migrations/LegacyServerSrcMigration.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using MCPForUnity.Editor.Clients;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
using UnityEditor;
using UnityEngine;
Expand All @@ -24,6 +26,48 @@ static LegacyServerSrcMigration()
EditorApplication.delayCall += RunMigrationIfNeeded;
}

internal static ClientConfigurationSummary MigrateDetectedClients(IClientConfigurationService clientService)
{
var summary = new ClientConfigurationSummary();
foreach (var configurator in clientService.GetAllClients())
{
if (!configurator.IsInstalled)
{
summary.SkippedCount++;
continue;
}
try
{
// Automatic migration must leave user-managed launchers untouched.
// Explicit Configure All continues to replace them.
if (configurator is JsonFileMcpConfigurator json && json.HasCustomStdioConfiguration())
{
summary.SkippedCount++;
continue;
}
var status = configurator.CheckStatus(attemptAutoRewrite: false);
if (configurator is JsonFileMcpConfigurator && status == McpStatus.Error)
{
summary.FailureCount++;
summary.Messages.Add($"⚠ {configurator.DisplayName}: Configuration is invalid; update it manually before migrating.");
continue;
}
// Match the CLI's automatic re-registration path, without changing its explicit toggle.
if (configurator is ClaudeCliMcpConfigurator cli && status == McpStatus.Configured)
cli.Client.SetStatus(McpStatus.IncorrectPath);
clientService.ConfigureClient(configurator);
summary.SuccessCount++;
summary.Messages.Add($"✓ {configurator.DisplayName}: Configured successfully");
}
catch (Exception ex)
{
summary.FailureCount++;
summary.Messages.Add($"⚠ {configurator.DisplayName}: {ex.Message}");
}
}
return summary;
}

private static void RunMigrationIfNeeded()
{
EditorApplication.delayCall -= RunMigrationIfNeeded;
Expand All @@ -40,7 +84,7 @@ private static void RunMigrationIfNeeded()
{
McpLog.Info("Detected legacy embedded MCP server configuration. Updating all client configs...");

var summary = MCPServiceLocator.Client.ConfigureAllDetectedClients();
var summary = MigrateDetectedClients(MCPServiceLocator.Client);

if (summary.FailureCount > 0)
{
Expand Down
49 changes: 21 additions & 28 deletions MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,8 @@ private static void RunMigrationIfNeeded()
continue;
}

// Handle JSON file-based configurators
if (!ConfigUsesStdIo(configurator.Client))
continue;

// Skip clients that don't support the current transport setting —
// Configure() would throw (e.g., Claude Desktop when HTTP is enabled).
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
if (useHttp && !configurator.Client.SupportsHttpTransport)
continue;

MCPServiceLocator.Client.ConfigureClient(configurator);
touchedAny = true;
if (RefreshStdioConfiguration(configurator, MCPServiceLocator.Client))
touchedAny = true;
}
catch (Exception ex)
{
Expand Down Expand Up @@ -112,9 +102,19 @@ private static void RunMigrationIfNeeded()
McpLog.Info($"Updated stdio MCP configs to package version {currentVersion}.");
}

private static bool ConfigUsesStdIo(McpClient client)
internal static bool RefreshStdioConfiguration(
McpClientConfiguratorBase configurator, IClientConfigurationService clientService)
{
return JsonConfigUsesStdIo(client);
if (!configurator.SupportsAutoConfigure || !JsonConfigUsesStdIo(configurator.Client))
return false;

// Preserve the migration's existing transport guard.
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
if (useHttp && !configurator.Client.SupportsHttpTransport)
return false;

clientService.ConfigureClient(configurator);
return true;
}

private static bool JsonConfigUsesStdIo(McpClient client)
Expand All @@ -129,20 +129,13 @@ private static bool JsonConfigUsesStdIo(McpClient client)
{
var root = JObject.Parse(File.ReadAllText(configPath));

JToken unityNode = null;
if (client.IsVsCodeLayout)
{
unityNode = root.SelectToken("servers.unityMCP")
?? root.SelectToken("mcp.servers.unityMCP");
}
else
{
unityNode = root.SelectToken("mcpServers.unityMCP");
}

if (unityNode == null) return false;

return unityNode["command"] != null;
var unityNode = McpConfigurationHelper.GetJsonUnityEntry(root, client);
var argsToken = unityNode?["args"];
if (argsToken != null && (!(argsToken is JArray argsArray)
|| argsArray.Any(value => value.Type != JTokenType.String)))
return false;
return unityNode?["command"] != null
&& !McpConfigurationHelper.HasCustomStdioLauncher(unityNode, client);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
catch
{
Expand Down
Loading
Loading