Files
Shutong Wu d29f2132ab fix(clients): transport-coercion HttpRemote + StartupConfigRewrite safety (PR review)
Two correctness bugs flagged on PR #1142 (Copilot + CodeRabbit).

CoerceTransportFor only treated `ConfiguredTransport.Http` as an HTTP
choice, so a client that declares `SupportedTransports = { HttpRemote }`
would get flipped to stdio whenever the user preferred HTTP. The
fallback selection was also order-dependent (`supported[0]`), which
could pick stdio over Http when the user wanted HTTP and the client
supported both. Now: any HTTP variant satisfies an HTTP request, and
when we must fall back we prefer the variant that matches user intent
(HTTP family vs stdio).

StartupConfigRewrite shared two problems with the CommandRegistry path
from #1134: it fired in the AssetImportWorker subprocess too (where
half-loaded domains can crash Mono on reflection and where writing
client configs from a worker is plain wrong), and it skipped rewrite
when in-memory Status was NotConfigured *before* refreshing from disk
— on a fresh editor load that miscategorized clients that were already
configured on disk and prevented the auto-rewrite. Added the same
AssetImportWorker guard as #1134 (reflective lookup + cmdline fallback)
and dropped the pre-refresh status skip so CheckStatus always reads
the file before deciding.
2026-05-22 19:36:23 +08:00

147 lines
5.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using MCPForUnity.Editor.Clients;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Services
{
/// <summary>
/// Implementation of client configuration service
/// </summary>
public class ClientConfigurationService : IClientConfigurationService
{
private readonly List<IMcpClientConfigurator> configurators;
public ClientConfigurationService()
{
configurators = McpClientRegistry.All.ToList();
}
public IReadOnlyList<IMcpClientConfigurator> GetAllClients() => configurators;
public void ConfigureClient(IMcpClientConfigurator configurator)
{
// When using a local server path, clean stale build artifacts first.
// This prevents old deleted .py files from being picked up by Python's auto-discovery.
if (AssetPathUtility.IsLocalServerPath())
{
AssetPathUtility.CleanLocalServerBuildArtifacts();
}
ConfigureWithTransportCoercion(configurator);
}
public ClientConfigurationSummary ConfigureAllDetectedClients()
{
// When using a local server path, clean stale build artifacts once before configuring all clients.
if (AssetPathUtility.IsLocalServerPath())
{
AssetPathUtility.CleanLocalServerBuildArtifacts();
}
var summary = new ClientConfigurationSummary();
foreach (var configurator in configurators)
{
if (!configurator.IsInstalled)
{
summary.SkippedCount++;
continue;
}
try
{
// Always re-run configuration so core fields stay current
configurator.CheckStatus(attemptAutoRewrite: false);
ConfigureWithTransportCoercion(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 ConfigureWithTransportCoercion(IMcpClientConfigurator configurator)
{
bool originalHttp = EditorConfigurationCache.Instance.UseHttpTransport;
try
{
CoerceTransportFor(configurator);
configurator.Configure();
}
finally
{
EditorConfigurationCache.Instance.SetUseHttpTransport(originalHttp);
}
}
public bool CheckClientStatus(IMcpClientConfigurator configurator, bool attemptAutoRewrite = true)
{
var previous = configurator.Status;
var current = configurator.CheckStatus(attemptAutoRewrite);
return current != previous;
}
private static void CoerceTransportFor(IMcpClientConfigurator configurator)
{
var supported = configurator.SupportedTransports;
if (supported == null || supported.Count == 0) return;
bool currentlyHttp = EditorConfigurationCache.Instance.UseHttpTransport;
var requested = currentlyHttp ? ConfiguredTransport.Http : ConfiguredTransport.Stdio;
// Accept any HTTP variant (Http, HttpRemote) when the user wants HTTP — a client that
// only supports HttpRemote should not get coerced to stdio just because Http isn't
// explicitly listed.
if (SupportsRequested(supported, requested)) return;
// Fall back in the direction of the user's intent: if they wanted HTTP, prefer any
// HTTP variant the client does support; otherwise prefer stdio. Honors the
// configurator's declared order when more than one option remains.
ConfiguredTransport chosen = PickFallback(supported, requested);
bool needHttp = IsHttpVariant(chosen);
if (EditorConfigurationCache.Instance.UseHttpTransport != needHttp)
{
EditorConfigurationCache.Instance.SetUseHttpTransport(needHttp);
McpLog.Info(
$"[{configurator.DisplayName}] auto-selected {chosen} transport (client does not support {requested}).");
}
}
private static bool IsHttpVariant(ConfiguredTransport t)
=> t == ConfiguredTransport.Http || t == ConfiguredTransport.HttpRemote;
private static bool SupportsRequested(IReadOnlyList<ConfiguredTransport> supported, ConfiguredTransport requested)
{
if (requested == ConfiguredTransport.Http)
{
foreach (var t in supported)
if (IsHttpVariant(t)) return true;
return false;
}
return supported.Contains(requested);
}
private static ConfiguredTransport PickFallback(IReadOnlyList<ConfiguredTransport> supported, ConfiguredTransport requested)
{
if (requested == ConfiguredTransport.Http)
{
foreach (var t in supported)
if (IsHttpVariant(t)) return t;
}
else
{
foreach (var t in supported)
if (t == ConfiguredTransport.Stdio) return t;
}
return supported[0];
}
}
}