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 ConfiguredTransport ConfiguredTransport => client.configuredTransport;
        public virtual bool SupportsAutoConfigure => true;
        // Default to a filesystem check on the configured path. Concrete configurators
        // whose presence isn't path-based (CLI binaries, etc.) override this. This makes
        // any future configurator that forgets to override fail-closed rather than be
        // treated as "detected" by ConfigureAllDetectedClients.
        public virtual bool IsInstalled => ParentDirectoryExists(GetConfigPath());
        private static readonly ConfiguredTransport[] DefaultTransports =
            { ConfiguredTransport.Stdio, ConfiguredTransport.Http };
        public virtual IReadOnlyList<ConfiguredTransport> SupportedTransports => DefaultTransports;
        public virtual bool SupportsSkills => false;
        public virtual string GetConfigureActionLabel() => "Configure";
        public virtual string GetSkillInstallPath() => null;

        public abstract string GetConfigPath();
        public abstract McpStatus CheckStatus(bool attemptAutoRewrite = true);
        public abstract void Configure();

        /// <summary>Default Unregister is a no-op. Override in JsonFileMcpConfigurator /
        /// ClaudeCliMcpConfigurator etc. where removal has a concrete implementation.</summary>
        public virtual void Unregister() { }

        public abstract string GetManualSnippet();
        public abstract IList<string> GetInstallationSteps();

        protected string GetUvxPathOrError()
        {
            string uvx = MCPServiceLocator.Paths.GetUvxPath();
            if (string.IsNullOrEmpty(uvx))
            {
                throw new InvalidOperationException("uvx 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 static bool ParentDirectoryExists(string configPath)
        {
            try
            {
                if (string.IsNullOrEmpty(configPath)) return false;
                string parent = Path.GetDirectoryName(configPath);
                return !string.IsNullOrEmpty(parent) && Directory.Exists(parent);
            }
            catch { return false; }
        }

        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>
        /// Gets the expected package source for validation based on the installed package version.
        /// This should match what Configure() would actually use for the --from argument.
        /// MUST be called from the main thread due to EditorPrefs access.
        /// </summary>
        protected static string GetExpectedPackageSourceForValidation()
        {
            // Includes explicit override, stable pin, or prerelease range depending on package version.
            return AssetPathUtility.GetMcpServerPackageSource();
        }

        /// <summary>
        /// Checks if a package source string represents a beta/prerelease version.
        /// Beta versions include:
        /// - PyPI beta: "mcpforunityserver==9.4.0b20250203..." (contains 'b' before timestamp)
        /// - PyPI prerelease range: "mcpforunityserver>=0.0.0a0" (used for prerelease package builds)
        /// - Git beta branch: contains "@beta" or "-beta"
        /// </summary>
        protected static bool IsBetaPackageSource(string packageSource)
        {
            if (string.IsNullOrEmpty(packageSource))
                return false;

            // PyPI beta format: mcpforunityserver==X.Y.Zb<timestamp>
            // The 'b' suffix before numbers indicates a PEP 440 beta version
            if (System.Text.RegularExpressions.Regex.IsMatch(packageSource, @"==\d+\.\d+\.\d+b\d+"))
                return true;

            // PyPI prerelease range: >=0.0.0a0 (used for prerelease package builds)
            if (packageSource.Contains(">=0.0.0a0", StringComparison.OrdinalIgnoreCase))
                return true;

            // Git-based beta references
            if (packageSource.Contains("@beta", StringComparison.OrdinalIgnoreCase))
                return true;

            if (packageSource.Contains("-beta", StringComparison.OrdinalIgnoreCase))
                return true;

            return false;
        }
    }

    /// <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 bool IsInstalled => ParentDirectoryExists(GetConfigPath());

        public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
        {
            try
            {
                string path = GetConfigPath();
                if (!File.Exists(path))
                {
                    client.SetStatus(McpStatus.NotConfigured);
                    client.configuredTransport = Models.ConfiguredTransport.Unknown;
                    return client.status;
                }

                string configJson = File.ReadAllText(path);
                string[] args = null;
                string configuredUrl = null;
                bool configExists = false;

                {
                    var rootConfig = JsonConvert.DeserializeObject<JToken>(configJson) as JObject;
                    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"];
                    }

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

                        var argsToken = unityObj["args"];
                        if (argsToken is JArray)
                        {
                            args = argsToken.ToObject<string[]>();
                        }

                        // 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 = urlToken.ToString();
                        }
                    }
                }

                if (!configExists)
                {
                    client.SetStatus(McpStatus.MissingConfig);
                    client.configuredTransport = Models.ConfiguredTransport.Unknown;
                    return client.status;
                }

                // Determine and set the configured transport type
                if (args != null && args.Length > 0)
                {
                    client.configuredTransport = Models.ConfiguredTransport.Stdio;
                }
                else if (!string.IsNullOrEmpty(configuredUrl))
                {
                    // Distinguish HTTP Local from HTTP Remote by matching against both URLs
                    string localRpcUrl = HttpEndpointUtility.GetLocalMcpRpcUrl();
                    string remoteRpcUrl = HttpEndpointUtility.GetRemoteMcpRpcUrl();
                    if (!string.IsNullOrEmpty(remoteRpcUrl) && UrlsEqual(configuredUrl, remoteRpcUrl))
                    {
                        client.configuredTransport = Models.ConfiguredTransport.HttpRemote;
                    }
                    else
                    {
                        client.configuredTransport = Models.ConfiguredTransport.Http;
                    }
                }
                else
                {
                    client.configuredTransport = Models.ConfiguredTransport.Unknown;
                }

                bool matches = false;
                bool hasVersionMismatch = false;
                string mismatchReason = null;

                if (args != null && args.Length > 0)
                {
                    // Use beta-aware expected package source for comparison
                    string expectedUvxUrl = GetExpectedPackageSourceForValidation();
                    string configuredUvxUrl = McpConfigurationHelper.ExtractUvxUrl(args);

                    if (!string.IsNullOrEmpty(configuredUvxUrl) && !string.IsNullOrEmpty(expectedUvxUrl))
                    {
                        if (McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl))
                        {
                            matches = true;
                        }
                        else
                        {
                            // Check for beta/stable mismatch
                            bool configuredIsBeta = IsBetaPackageSource(configuredUvxUrl);
                            bool expectedIsBeta = IsBetaPackageSource(expectedUvxUrl);

                            if (configuredIsBeta && !expectedIsBeta)
                            {
                                hasVersionMismatch = true;
                                mismatchReason = "Configured for prerelease server, but this package is stable. Re-configure to switch to stable.";
                            }
                            else if (!configuredIsBeta && expectedIsBeta)
                            {
                                hasVersionMismatch = true;
                                mismatchReason = "Configured for stable server, but this package is prerelease. Re-configure to switch to prerelease.";
                            }
                            else
                            {
                                hasVersionMismatch = true;
                                mismatchReason = "Server version doesn't match the plugin. Re-configure to update.";
                            }
                        }
                    }
                }
                else if (!string.IsNullOrEmpty(configuredUrl))
                {
                    // Match against the active scope's URL
                    string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl();
                    matches = UrlsEqual(configuredUrl, expectedUrl);
                }

                if (matches)
                {
                    client.SetStatus(McpStatus.Configured);
                    return client.status;
                }

                if (hasVersionMismatch)
                {
                    if (attemptAutoRewrite)
                    {
                        var result = McpConfigurationHelper.WriteMcpConfiguration(path, client);
                        if (result == "Configured successfully")
                        {
                            client.SetStatus(McpStatus.Configured);
                            client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
                        }
                        else
                        {
                            client.SetStatus(McpStatus.VersionMismatch, mismatchReason);
                        }
                    }
                    else
                    {
                        client.SetStatus(McpStatus.VersionMismatch, mismatchReason);
                    }
                }
                else if (attemptAutoRewrite)
                {
                    var result = McpConfigurationHelper.WriteMcpConfiguration(path, client);
                    if (result == "Configured successfully")
                    {
                        client.SetStatus(McpStatus.Configured);
                        client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
                    }
                    else
                    {
                        client.SetStatus(McpStatus.IncorrectPath);
                    }
                }
                else
                {
                    client.SetStatus(McpStatus.IncorrectPath);
                }
            }
            catch (Exception ex)
            {
                client.SetStatus(McpStatus.Error, ex.Message);
                client.configuredTransport = Models.ConfiguredTransport.Unknown;
            }

            return client.status;
        }

        public override void Configure()
        {
            // Always idempotent-write. The per-client UI button routes through Unregister
            // when the user clicks while the client is already Configured; the bulk
            // "Configure All" path calls this directly and expects an unconditional write.
            string path = GetConfigPath();
            McpConfigurationHelper.EnsureConfigDirectoryExists(path);
            string result = McpConfigurationHelper.WriteMcpConfiguration(path, client);
            if (result == "Configured successfully")
            {
                client.SetStatus(McpStatus.Configured);
                client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
            }
            else
            {
                throw new InvalidOperationException(result);
            }
        }

        public override string GetConfigureActionLabel()
            => client.status == McpStatus.Configured ? "Unregister" : "Configure";

        /// <summary>
        /// Removes the unityMCP entry from the client's JSON config (VS Code-style
        /// `servers` / `mcp.servers` layouts, the standard `mcpServers` layout, or a
        /// client-specific container such as Kilo's `mcp`). Leaves the file in place so we
        /// don't clobber other servers the user has configured.
        /// </summary>
        public override void Unregister()
        {
            string path = GetConfigPath();
            try
            {
                if (!File.Exists(path))
                {
                    client.SetStatus(McpStatus.NotConfigured);
                    client.configuredTransport = Models.ConfiguredTransport.Unknown;
                    return;
                }

                var root = JsonConvert.DeserializeObject<JToken>(File.ReadAllText(path)) as JObject;
                if (root == null)
                {
                    client.SetStatus(McpStatus.NotConfigured);
                    client.configuredTransport = Models.ConfiguredTransport.Unknown;
                    return;
                }

                bool removed = false;
                if (client.IsVsCodeLayout)
                {
                    if ((root["servers"] as JObject)?.Remove("unityMCP") == true) removed = true;
                    if ((root["mcp"]?["servers"] as JObject)?.Remove("unityMCP") == true) removed = true;
                }
                else
                {
                    string containerKey = string.IsNullOrEmpty(client.ServerContainerKey)
                        ? "mcpServers" : client.ServerContainerKey;
                    if ((root[containerKey] as JObject)?.Remove("unityMCP") == true) removed = true;
                }

                if (removed)
                {
                    File.WriteAllText(path, root.ToString(Formatting.Indented));
                }

                client.SetStatus(McpStatus.NotConfigured);
                client.configuredTransport = Models.ConfiguredTransport.Unknown;
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException($"Failed to unregister: {ex.Message}", ex);
            }
        }

        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 bool IsInstalled => ParentDirectoryExists(GetConfigPath());

        public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
        {
            try
            {
                string path = GetConfigPath();
                if (!File.Exists(path))
                {
                    client.SetStatus(McpStatus.NotConfigured);
                    client.configuredTransport = Models.ConfiguredTransport.Unknown;
                    return client.status;
                }

                string toml = File.ReadAllText(path);
                if (CodexConfigHelper.TryParseCodexServer(toml, out _, out var args, out var url))
                {
                    // Determine and set the configured transport type
                    if (!string.IsNullOrEmpty(url))
                    {
                        // Distinguish HTTP Local from HTTP Remote
                        string remoteRpcUrl = HttpEndpointUtility.GetRemoteMcpRpcUrl();
                        if (!string.IsNullOrEmpty(remoteRpcUrl) && UrlsEqual(url, remoteRpcUrl))
                        {
                            client.configuredTransport = Models.ConfiguredTransport.HttpRemote;
                        }
                        else
                        {
                            client.configuredTransport = Models.ConfiguredTransport.Http;
                        }
                    }
                    else if (args != null && args.Length > 0)
                    {
                        client.configuredTransport = Models.ConfiguredTransport.Stdio;
                    }
                    else
                    {
                        client.configuredTransport = Models.ConfiguredTransport.Unknown;
                    }

                    bool matches = false;
                    bool hasVersionMismatch = false;
                    string mismatchReason = null;

                    if (!string.IsNullOrEmpty(url))
                    {
                        // Match against the active scope's URL
                        matches = UrlsEqual(url, HttpEndpointUtility.GetMcpRpcUrl());
                    }
                    else if (args != null && args.Length > 0)
                    {
                        // Use beta-aware expected package source for comparison
                        string expected = GetExpectedPackageSourceForValidation();
                        string configured = McpConfigurationHelper.ExtractUvxUrl(args);

                        if (!string.IsNullOrEmpty(configured) && !string.IsNullOrEmpty(expected))
                        {
                            if (McpConfigurationHelper.PathsEqual(configured, expected))
                            {
                                matches = true;
                            }
                            else
                            {
                                // Check for beta/stable mismatch
                                bool configuredIsBeta = IsBetaPackageSource(configured);
                                bool expectedIsBeta = IsBetaPackageSource(expected);

                                if (configuredIsBeta && !expectedIsBeta)
                                {
                                    hasVersionMismatch = true;
                                    mismatchReason = "Configured for prerelease server, but this package is stable. Re-configure to switch to stable.";
                                }
                                else if (!configuredIsBeta && expectedIsBeta)
                                {
                                    hasVersionMismatch = true;
                                    mismatchReason = "Configured for stable server, but this package is prerelease. Re-configure to switch to prerelease.";
                                }
                                else
                                {
                                    hasVersionMismatch = true;
                                    mismatchReason = "Server version doesn't match the plugin. Re-configure to update.";
                                }
                            }
                        }
                    }

                    if (matches)
                    {
                        client.SetStatus(McpStatus.Configured);
                        return client.status;
                    }

                    if (hasVersionMismatch)
                    {
                        if (attemptAutoRewrite)
                        {
                            string result = McpConfigurationHelper.ConfigureCodexClient(path, client);
                            if (result == "Configured successfully")
                            {
                                client.SetStatus(McpStatus.Configured);
                                client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
                                return client.status;
                            }
                        }
                        client.SetStatus(McpStatus.VersionMismatch, mismatchReason);
                        return client.status;
                    }
                }
                else
                {
                    client.configuredTransport = Models.ConfiguredTransport.Unknown;
                }

                if (attemptAutoRewrite)
                {
                    string result = McpConfigurationHelper.ConfigureCodexClient(path, client);
                    if (result == "Configured successfully")
                    {
                        client.SetStatus(McpStatus.Configured);
                        client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
                    }
                    else
                    {
                        client.SetStatus(McpStatus.IncorrectPath);
                    }
                }
                else
                {
                    client.SetStatus(McpStatus.IncorrectPath);
                }
            }
            catch (Exception ex)
            {
                client.SetStatus(McpStatus.Error, ex.Message);
                client.configuredTransport = Models.ConfiguredTransport.Unknown;
            }

            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);
                client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
            }
            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" : "Configure";

        public override string GetConfigPath() => "Managed via Claude CLI";

        public override bool IsInstalled => MCPServiceLocator.Paths.IsClaudeCliDetected();

        /// <summary>
        /// Returns the project directory that CLI-based configurators will use as the working directory
        /// for `claude mcp add/remove --scope local`. Checks for an explicit override in EditorPrefs
        /// first, then falls back to the current Unity project directory.
        /// The override is useful when the Claude Code workspace is at a different path than the Unity project
        /// (e.g., plugin developers running CC from the repo root while Unity is open with a test project).
        /// MUST be called from the main Unity thread (accesses Application.dataPath and EditorPrefs).
        /// </summary>
        internal static string GetClientProjectDir()
        {
            string overrideDir = EditorPrefs.GetString(EditorPrefKeys.ClientProjectDirOverride, string.Empty);
            if (!string.IsNullOrEmpty(overrideDir) && Directory.Exists(overrideDir))
                return overrideDir;
            return Path.GetDirectoryName(Application.dataPath);
        }

        /// <summary>
        /// Returns true if a valid client project directory override is set.
        /// </summary>
        internal static bool HasClientProjectDirOverride
        {
            get
            {
                string overrideDir = EditorPrefs.GetString(EditorPrefKeys.ClientProjectDirOverride, string.Empty);
                return !string.IsNullOrEmpty(overrideDir) && Directory.Exists(overrideDir);
            }
        }
        /// Checks the Claude CLI registration status.
        /// MUST be called from the main Unity thread due to EditorPrefs and Application.dataPath access.
        /// </summary>
        public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
        {
            // Capture main-thread-only values before delegating to thread-safe method
            string projectDir = GetClientProjectDir();
            bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
            // Resolve claudePath on the main thread (EditorPrefs access)
            string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
            RuntimePlatform platform = Application.platform;
            bool isRemoteScope = HttpEndpointUtility.IsRemoteScope();
            // Get expected package source for the installed package version (matches what Register() would use)
            string expectedPackageSource = GetExpectedPackageSourceForValidation();
            return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite, HasClientProjectDirOverride);
        }

        /// <summary>
        /// Internal thread-safe version of CheckStatus.
        /// Can be called from background threads because all main-thread-only values are passed as parameters.
        /// projectDir, useHttpTransport, claudePath, platform, isRemoteScope, and expectedPackageSource are REQUIRED
        /// (non-nullable where applicable) to enforce thread safety at compile time.
        /// NOTE: attemptAutoRewrite is NOT fully thread-safe because Configure() requires the main thread.
        /// When called from a background thread, pass attemptAutoRewrite=false and handle re-registration
        /// on the main thread based on the returned status.
        /// </summary>
        internal McpStatus CheckStatusWithProjectDir(
            string projectDir, bool useHttpTransport, string claudePath, RuntimePlatform platform,
            bool isRemoteScope, string expectedPackageSource,
            bool attemptAutoRewrite = false, bool hasProjectDirOverride = false)
        {
            try
            {
                if (string.IsNullOrEmpty(claudePath))
                {
                    client.SetStatus(McpStatus.NotConfigured, "Claude CLI not found");
                    client.configuredTransport = Models.ConfiguredTransport.Unknown;
                    return client.status;
                }

                // projectDir is required - no fallback to Application.dataPath
                if (string.IsNullOrEmpty(projectDir))
                {
                    throw new ArgumentNullException(nameof(projectDir), "Project directory must be provided for thread-safe execution");
                }

                // Read Claude Code config directly from ~/.claude.json instead of using slow CLI
                // This is instant vs 15+ seconds for `claude mcp list` which does health checks
                var configResult = ReadClaudeCodeConfig(projectDir);
                if (configResult.error != null)
                {
                    client.SetStatus(McpStatus.NotConfigured, configResult.error);
                    client.configuredTransport = Models.ConfiguredTransport.Unknown;
                    return client.status;
                }

                if (configResult.serverConfig == null)
                {
                    // UnityMCP not found in config
                    client.SetStatus(McpStatus.NotConfigured);
                    client.configuredTransport = Models.ConfiguredTransport.Unknown;
                    return client.status;
                }

                // UnityMCP is registered - check transport and version
                bool currentUseHttp = useHttpTransport;
                var serverConfig = configResult.serverConfig;

                // Determine registered transport type
                string registeredType = serverConfig["type"]?.ToString()?.ToLowerInvariant() ?? "";
                bool registeredWithHttp = registeredType == "http";
                bool registeredWithStdio = registeredType == "stdio";

                // Set the configured transport based on what we detected
                if (registeredWithHttp)
                {
                    client.configuredTransport = isRemoteScope
                        ? Models.ConfiguredTransport.HttpRemote
                        : Models.ConfiguredTransport.Http;
                }
                else if (registeredWithStdio)
                {
                    client.configuredTransport = Models.ConfiguredTransport.Stdio;
                }
                else
                {
                    client.configuredTransport = Models.ConfiguredTransport.Unknown;
                }

                // Check for transport mismatch.
                // When a project dir override is active, the local UseHttpTransport
                // GUI setting may legitimately differ from the registered transport
                // in the overridden project, so skip this check.
                bool hasTransportMismatch = !hasProjectDirOverride
                    && ((currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp));

                // For stdio transport, also check package version
                bool hasVersionMismatch = false;
                string configuredPackageSource = null;
                string mismatchReason = null;
                if (registeredWithStdio)
                {
                    configuredPackageSource = ExtractPackageSourceFromConfig(serverConfig);
                    if (!string.IsNullOrEmpty(configuredPackageSource) && !string.IsNullOrEmpty(expectedPackageSource))
                    {
                        // Check for exact match first
                        if (!string.Equals(configuredPackageSource, expectedPackageSource, StringComparison.OrdinalIgnoreCase))
                        {
                            hasVersionMismatch = true;

                            // Provide more specific mismatch reason for beta/stable differences
                            bool configuredIsBeta = IsBetaPackageSource(configuredPackageSource);
                            bool expectedIsBeta = IsBetaPackageSource(expectedPackageSource);

                            if (configuredIsBeta && !expectedIsBeta)
                            {
                                mismatchReason = "Configured for prerelease server, but this package is stable. Re-configure to switch to stable.";
                            }
                            else if (!configuredIsBeta && expectedIsBeta)
                            {
                                mismatchReason = "Configured for stable server, but this package is prerelease. Re-configure to switch to prerelease.";
                            }
                            else
                            {
                                mismatchReason = "Server version doesn't match the plugin. Re-configure to update.";
                            }
                        }
                    }
                }

                // If there's any mismatch and auto-rewrite is enabled, re-register
                if (hasTransportMismatch || hasVersionMismatch)
                {
                    // Configure() requires main thread (accesses EditorPrefs, Application.dataPath)
                    // Only attempt auto-rewrite if we're on the main thread
                    bool isMainThread = System.Threading.Thread.CurrentThread.ManagedThreadId == 1;
                    if (attemptAutoRewrite && isMainThread)
                    {
                        string reason = hasTransportMismatch
                            ? $"Transport mismatch (registered: {(registeredWithHttp ? "HTTP" : "stdio")}, expected: {(currentUseHttp ? "HTTP" : "stdio")})"
                            : mismatchReason ?? $"Package version mismatch";
                        McpLog.Info($"{reason}. Re-registering...");
                        try
                        {
                            // Force re-register by ensuring status is not Configured (which would toggle to Unregister)
                            client.SetStatus(McpStatus.IncorrectPath);
                            Configure();
                            return client.status;
                        }
                        catch (Exception ex)
                        {
                            McpLog.Warn($"Auto-reregister failed: {ex.Message}");
                            client.SetStatus(McpStatus.IncorrectPath, $"Configuration mismatch. Click Configure to re-register.");
                            return client.status;
                        }
                    }
                    else
                    {
                        if (hasTransportMismatch)
                        {
                            string errorMsg = $"Transport mismatch: Claude Code is registered with {(registeredWithHttp ? "HTTP" : "stdio")} but current setting is {(currentUseHttp ? "HTTP" : "stdio")}. Click Configure to re-register.";
                            client.SetStatus(McpStatus.Error, errorMsg);
                            McpLog.Warn(errorMsg);
                        }
                        else
                        {
                            client.SetStatus(McpStatus.VersionMismatch, mismatchReason);
                        }
                        return client.status;
                    }
                }

                client.SetStatus(McpStatus.Configured);
                return client.status;
            }
            catch (Exception ex)
            {
                McpLog.Warn($"[Claude Code] CheckStatus exception: {ex.GetType().Name}: {ex.Message}");
                client.SetStatus(McpStatus.Error, ex.Message);
                client.configuredTransport = Models.ConfiguredTransport.Unknown;
            }

            return client.status;
        }

        public override void Configure()
        {
            if (client.status == McpStatus.Configured)
            {
                Unregister();
            }
            else
            {
                Register();
            }
        }

        /// <summary>
        /// Thread-safe version of Configure that uses pre-captured main-thread values.
        /// All parameters must be captured on the main thread before calling this method.
        /// </summary>
        public void ConfigureWithCapturedValues(
            string projectDir, string claudePath, string pathPrepend,
            bool useHttpTransport, string httpUrl,
            string uvxPath, string fromArgs, string packageName, string uvxDevFlags,
            string apiKey,
            Models.ConfiguredTransport serverTransport)
        {
            if (client.status == McpStatus.Configured)
            {
                UnregisterWithCapturedValues(projectDir, claudePath, pathPrepend);
            }
            else
            {
                RegisterWithCapturedValues(projectDir, claudePath, pathPrepend,
                    useHttpTransport, httpUrl, uvxPath, fromArgs, packageName, uvxDevFlags,
                    apiKey, serverTransport);
            }
        }

        /// <summary>
        /// Thread-safe registration using pre-captured values.
        /// </summary>
        private void RegisterWithCapturedValues(
            string projectDir, string claudePath, string pathPrepend,
            bool useHttpTransport, string httpUrl,
            string uvxPath, string fromArgs, string packageName, string uvxDevFlags,
            string apiKey,
            Models.ConfiguredTransport serverTransport)
        {
            if (string.IsNullOrEmpty(claudePath))
            {
                throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
            }

            string args;
            if (useHttpTransport)
            {
                // Only include API key header for remote-hosted mode
                // Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
                if (serverTransport == Models.ConfiguredTransport.HttpRemote && !string.IsNullOrEmpty(apiKey))
                {
                    string safeKey = SanitizeShellHeaderValue(apiKey);
                    args = $"mcp add --scope local --transport http UnityMCP {httpUrl} --header \"{AuthConstants.ApiKeyHeader}: {safeKey}\"";
                }
                else
                {
                    args = $"mcp add --scope local --transport http UnityMCP {httpUrl}";
                }
            }
            else
            {
                // Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
                args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {uvxDevFlags}{fromArgs} {packageName}";
            }

            // Remove any existing registrations from ALL scopes to prevent stale config conflicts (#664)
            McpLog.Info("Removing any existing UnityMCP registrations from all scopes before adding...");
            RemoveFromAllScopes(claudePath, projectDir, pathPrepend);

            // Now add the registration
            if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
            {
                throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}");
            }

            McpLog.Info($"Successfully registered with Claude Code using {(useHttpTransport ? "HTTP" : "stdio")} transport.");
            client.SetStatus(McpStatus.Configured);
            client.configuredTransport = serverTransport;
        }

        /// <summary>
        /// Thread-safe unregistration using pre-captured values.
        /// </summary>
        private void UnregisterWithCapturedValues(string projectDir, string claudePath, string pathPrepend)
        {
            if (string.IsNullOrEmpty(claudePath))
            {
                throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
            }

            // Remove from ALL scopes to ensure complete cleanup (#664)
            McpLog.Info("Removing all UnityMCP registrations from all scopes...");
            RemoveFromAllScopes(claudePath, projectDir, pathPrepend);

            McpLog.Info("MCP server successfully unregistered from Claude Code.");
            client.SetStatus(McpStatus.NotConfigured);
            client.configuredTransport = Models.ConfiguredTransport.Unknown;
        }

        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 = EditorConfigurationCache.Instance.UseHttpTransport;

            string args;
            if (useHttpTransport)
            {
                string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
                // Only include API key header for remote-hosted mode
                // Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
                if (HttpEndpointUtility.IsRemoteScope())
                {
                    string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
                    if (!string.IsNullOrEmpty(apiKey))
                    {
                        string safeKey = SanitizeShellHeaderValue(apiKey);
                        args = $"mcp add --scope local --transport http UnityMCP {httpUrl} --header \"{AuthConstants.ApiKeyHeader}: {safeKey}\"";
                    }
                    else
                    {
                        args = $"mcp add --scope local --transport http UnityMCP {httpUrl}";
                    }
                }
                else
                {
                    args = $"mcp add --scope local --transport http UnityMCP {httpUrl}";
                }
            }
            else
            {
                var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
                string devFlags = AssetPathUtility.GetUvxDevFlags();
                string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
                // Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
                args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}{fromArgs} {packageName}";
            }

            string projectDir = GetClientProjectDir();

            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 { }

            // Remove any existing registrations from ALL scopes to prevent stale config conflicts (#664)
            McpLog.Info("Removing any existing UnityMCP registrations from all scopes before adding...");
            RemoveFromAllScopes(claudePath, projectDir, pathPrepend);

            // Now add the registration with the current transport mode
            if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
            {
                throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}");
            }

            McpLog.Info($"Successfully registered with Claude Code using {(useHttpTransport ? "HTTP" : "stdio")} transport.");

            // Set status to Configured immediately after successful registration
            // The UI will trigger an async verification check separately to avoid blocking
            client.SetStatus(McpStatus.Configured);
            client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
        }

        public override 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 = GetClientProjectDir();
            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";
            }

            // Remove from ALL scopes to ensure complete cleanup (#664)
            McpLog.Info("Removing all UnityMCP registrations from all scopes...");
            RemoveFromAllScopes(claudePath, projectDir, pathPrepend);

            McpLog.Info("MCP server successfully unregistered from Claude Code.");
            client.SetStatus(McpStatus.NotConfigured);
            client.configuredTransport = Models.ConfiguredTransport.Unknown;
        }

        public override string GetManualSnippet()
        {
            string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
            bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;

            if (useHttpTransport)
            {
                string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
                // Only include API key header for remote-hosted mode
                string headerArg = "";
                if (HttpEndpointUtility.IsRemoteScope())
                {
                    string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
                    headerArg = !string.IsNullOrEmpty(apiKey) ? $" --header \"{AuthConstants.ApiKeyHeader}: {SanitizeShellHeaderValue(apiKey)}\"" : "";
                }
                return "# Register the MCP server with Claude Code:\n" +
                       $"claude mcp add --scope local --transport http UnityMCP {httpUrl}{headerArg}\n\n" +
                       "# Unregister the MCP server (from all scopes to clean up any stale configs):\n" +
                       "claude mcp remove --scope local UnityMCP\n" +
                       "claude mcp remove --scope user UnityMCP\n" +
                       "claude mcp remove --scope project UnityMCP\n\n" +
                       "# List registered servers:\n" +
                       "claude mcp list";
            }

            if (string.IsNullOrEmpty(uvxPath))
            {
                return "# Error: Configuration not available - check paths in Advanced Settings";
            }

            string devFlags = AssetPathUtility.GetUvxDevFlags();
            string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);

            return "# Register the MCP server with Claude Code:\n" +
                   $"claude mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}{fromArgs} mcp-for-unity\n\n" +
                   "# Unregister the MCP server (from all scopes to clean up any stale configs):\n" +
                   "claude mcp remove --scope local UnityMCP\n" +
                   "claude mcp remove --scope user UnityMCP\n" +
                   "claude mcp remove --scope project UnityMCP\n\n" +
                   "# List registered servers:\n" +
                   "claude mcp list";
        }

        public override IList<string> GetInstallationSteps() => new List<string>
        {
            "Ensure Claude CLI is installed",
            "Use Configure to add UnityMCP (or run claude mcp add UnityMCP)",
            "Restart Claude Code"
        };

        /// <summary>
        /// Removes UnityMCP registration from all Claude Code configuration scopes (local, user, project).
        /// Also removes legacy entries from ~/.claude.json that the CLI scoped removal can't touch.
        /// This ensures no stale or conflicting configurations remain across different scopes.
        /// Also handles legacy "unityMCP" naming convention.
        /// </summary>
        private static void RemoveFromAllScopes(string claudePath, string projectDir, string pathPrepend)
        {
            // Remove from all three scopes to prevent stale configs causing connection issues.
            // See GitHub issue #664 - conflicting configs at different scopes can cause
            // Claude Code to connect with outdated/incorrect configuration.
            string[] scopes = { "local", "user", "project" };
            string[] names = { "UnityMCP", "unityMCP" }; // Include legacy naming

            foreach (var scope in scopes)
            {
                foreach (var name in names)
                {
                    ExecPath.TryRun(claudePath, $"mcp remove --scope {scope} {name}", projectDir, out _, out _, 5000, pathPrepend);
                }
            }

            // Also remove legacy entries directly from ~/.claude.json.
            // Older versions and manual CLI commands without --scope wrote mcpServers entries
            // into the projects section of ~/.claude.json. The scoped `claude mcp remove` commands
            // above won't touch these, leaving stale/conflicting configs behind.
            RemoveLegacyUserConfigEntries(projectDir);
        }

        /// <summary>
        /// Removes UnityMCP entries from the projects section of ~/.claude.json.
        /// These are legacy entries that were created by older versions or manual commands
        /// that didn't use --scope. The scoped `claude mcp remove` commands don't clean these up.
        /// </summary>
        private static void RemoveLegacyUserConfigEntries(string projectDir)
        {
            try
            {
                string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
                string configPath = Path.Combine(homeDir, ".claude.json");
                if (!File.Exists(configPath))
                    return;

                string json = File.ReadAllText(configPath);
                var config = JObject.Parse(json);
                var projects = config["projects"] as JObject;
                if (projects == null)
                    return;

                string normalizedProjectDir = NormalizePath(projectDir);
                bool modified = false;

                // Walk all project entries looking for ones that match our project path
                foreach (var project in projects.Properties())
                {
                    string normalizedKey = NormalizePath(project.Name);

                    // Match exact path or parent paths (same logic as ReadUserScopeConfig)
                    if (!string.Equals(normalizedKey, normalizedProjectDir, StringComparison.OrdinalIgnoreCase))
                    {
                        // Also check if projectDir is a child of this config entry
                        if (!normalizedProjectDir.StartsWith(normalizedKey + "/", StringComparison.OrdinalIgnoreCase))
                            continue;
                    }

                    var mcpServers = project.Value?["mcpServers"] as JObject;
                    if (mcpServers == null)
                        continue;

                    // Remove UnityMCP/unityMCP entries (case-insensitive)
                    var toRemove = new List<string>();
                    foreach (var server in mcpServers.Properties())
                    {
                        if (string.Equals(server.Name, "UnityMCP", StringComparison.OrdinalIgnoreCase))
                        {
                            toRemove.Add(server.Name);
                        }
                    }

                    foreach (var name in toRemove)
                    {
                        mcpServers.Remove(name);
                        modified = true;
                        McpLog.Info($"Removed legacy '{name}' entry from ~/.claude.json for project '{project.Name}'");
                    }
                }

                if (modified)
                {
                    File.WriteAllText(configPath, config.ToString(Formatting.Indented));
                }
            }
            catch (Exception ex)
            {
                McpLog.Warn($"Failed to clean up legacy ~/.claude.json entries: {ex.Message}");
            }
        }

        /// <summary>
        /// Sanitizes a value for safe inclusion inside a double-quoted shell argument.
        /// Escapes characters that are special within double quotes (", \, `, $, !)
        /// to prevent shell injection or argument splitting.
        /// </summary>
        private static string SanitizeShellHeaderValue(string value)
        {
            if (string.IsNullOrEmpty(value))
                return value;

            var sb = new System.Text.StringBuilder(value.Length);
            foreach (char c in value)
            {
                switch (c)
                {
                    case '"':
                    case '\\':
                    case '`':
                    case '$':
                    case '!':
                        sb.Append('\\');
                        sb.Append(c);
                        break;
                    default:
                        sb.Append(c);
                        break;
                }
            }
            return sb.ToString();
        }

        /// <summary>
        /// Extracts the package source (--from argument value) from claude mcp get output.
        /// The output format includes args like: --from "mcpforunityserver==9.0.1"
        /// </summary>
        private static string ExtractPackageSourceFromCliOutput(string cliOutput)
        {
            if (string.IsNullOrEmpty(cliOutput))
                return null;

            // Look for --from followed by the package source
            // The CLI output may have it quoted or unquoted
            int fromIndex = cliOutput.IndexOf("--from", StringComparison.OrdinalIgnoreCase);
            if (fromIndex < 0)
                return null;

            // Move past "--from" and any whitespace
            int startIndex = fromIndex + 6;
            while (startIndex < cliOutput.Length && char.IsWhiteSpace(cliOutput[startIndex]))
                startIndex++;

            if (startIndex >= cliOutput.Length)
                return null;

            // Check if value is quoted
            char quoteChar = cliOutput[startIndex];
            if (quoteChar == '"' || quoteChar == '\'')
            {
                startIndex++;
                int endIndex = cliOutput.IndexOf(quoteChar, startIndex);
                if (endIndex > startIndex)
                    return cliOutput.Substring(startIndex, endIndex - startIndex);
            }
            else
            {
                // Unquoted - read until whitespace or end of line
                int endIndex = startIndex;
                while (endIndex < cliOutput.Length && !char.IsWhiteSpace(cliOutput[endIndex]))
                    endIndex++;

                if (endIndex > startIndex)
                    return cliOutput.Substring(startIndex, endIndex - startIndex);
            }

            return null;
        }

        /// <summary>
        /// Reads Claude Code configuration from both local-scope (.claude/mcp.json in the project)
        /// and user-scope (~/.claude.json). Local scope takes precedence, matching Claude Code's
        /// own config resolution order.
        /// This is much faster than running `claude mcp list` which does health checks on all servers.
        /// </summary>
        private static (JObject serverConfig, string error) ReadClaudeCodeConfig(string projectDir)
        {
            try
            {
                // 1. Check local-scope config first: {projectDir}/.claude/mcp.json
                //    This is where `claude mcp add --scope local` writes.
                var localResult = ReadLocalScopeConfig(projectDir);
                if (localResult.serverConfig != null)
                    return localResult;
                if (localResult.error != null)
                    return localResult;

                // 2. Fall back to user-scope config: ~/.claude.json
                return ReadUserScopeConfig(projectDir);
            }
            catch (Exception ex)
            {
                return (null, $"Error reading Claude config: {ex.Message}");
            }
        }

        /// <summary>
        /// Reads UnityMCP config from the local-scope file: {projectDir}/.claude/mcp.json.
        /// This is where `claude mcp add --scope local` stores registrations.
        /// </summary>
        private static (JObject serverConfig, string error) ReadLocalScopeConfig(string projectDir)
        {
            try
            {
                if (string.IsNullOrEmpty(projectDir))
                    return (null, null);

                string localConfigPath = Path.Combine(projectDir, ".claude", "mcp.json");
                if (!File.Exists(localConfigPath))
                    return (null, null);

                string json = File.ReadAllText(localConfigPath);
                var config = JObject.Parse(json);
                var mcpServers = config["mcpServers"] as JObject;
                if (mcpServers == null)
                    return (null, null);

                foreach (var server in mcpServers.Properties())
                {
                    if (string.Equals(server.Name, "UnityMCP", StringComparison.OrdinalIgnoreCase))
                    {
                        return (server.Value as JObject, null);
                    }
                }

                return (null, null);
            }
            catch (Exception ex)
            {
                return (null, $"Error reading local Claude config: {ex.Message}");
            }
        }

        /// <summary>
        /// Reads UnityMCP config from the user-scope file: ~/.claude.json (projects section).
        /// This handles legacy configurations and direct user-level entries.
        /// </summary>
        private static (JObject serverConfig, string error) ReadUserScopeConfig(string projectDir)
        {
            try
            {
                string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
                string configPath = Path.Combine(homeDir, ".claude.json");

                if (!File.Exists(configPath))
                    return (null, null);

                string configJson = File.ReadAllText(configPath);
                var config = JObject.Parse(configJson);

                var projects = config["projects"] as JObject;
                if (projects == null)
                    return (null, null);

                // Build a dictionary of normalized paths for quick lookup
                // Use last entry for duplicates (forward/backslash variants) as it's typically more recent
                var normalizedProjects = new Dictionary<string, JObject>(StringComparer.OrdinalIgnoreCase);
                foreach (var project in projects.Properties())
                {
                    string normalizedPath = NormalizePath(project.Name);
                    normalizedProjects[normalizedPath] = project.Value as JObject;
                }

                // Walk up the directory tree to find a matching project config
                // Claude Code may be configured at a parent directory (e.g., repo root)
                // while Unity project is in a subdirectory (e.g., TestProjects/UnityMCPTests)
                string currentDir = NormalizePath(projectDir);
                while (!string.IsNullOrEmpty(currentDir))
                {
                    if (normalizedProjects.TryGetValue(currentDir, out var projectConfig))
                    {
                        var mcpServers = projectConfig?["mcpServers"] as JObject;
                        if (mcpServers != null)
                        {
                            foreach (var server in mcpServers.Properties())
                            {
                                if (string.Equals(server.Name, "UnityMCP", StringComparison.OrdinalIgnoreCase))
                                {
                                    return (server.Value as JObject, null);
                                }
                            }
                        }
                        // Found the project but no UnityMCP - don't continue walking up
                        return (null, null);
                    }

                    // Move up one directory
                    int lastSlash = currentDir.LastIndexOf('/');
                    if (lastSlash <= 0)
                        break;
                    currentDir = currentDir.Substring(0, lastSlash);
                }

                return (null, null);
            }
            catch (Exception ex)
            {
                return (null, $"Error reading user Claude config: {ex.Message}");
            }
        }

        /// <summary>
        /// Normalizes a file path for comparison (handles forward/back slashes, trailing slashes).
        /// </summary>
        private static string NormalizePath(string path)
        {
            if (string.IsNullOrEmpty(path))
                return path;

            // Replace backslashes with forward slashes and remove trailing slashes
            return path.Replace('\\', '/').TrimEnd('/');
        }

        /// <summary>
        /// Extracts the package source from Claude Code JSON config.
        /// For stdio servers, this is in the args array after "--from".
        /// </summary>
        private static string ExtractPackageSourceFromConfig(JObject serverConfig)
        {
            if (serverConfig == null)
                return null;

            var args = serverConfig["args"] as JArray;
            if (args == null)
                return null;

            // Look for --from argument (either "--from VALUE" or "--from=VALUE" format)
            bool foundFrom = false;
            foreach (var arg in args)
            {
                string argStr = arg?.ToString();
                if (argStr == null)
                    continue;

                if (foundFrom)
                {
                    // This is the package source following --from
                    return argStr;
                }

                if (argStr == "--from")
                {
                    foundFrom = true;
                }
                else if (argStr.StartsWith("--from=", StringComparison.OrdinalIgnoreCase))
                {
                    // Handle --from=VALUE format
                    return argStr.Substring(7).Trim('"', '\'');
                }
            }

            return null;
        }
    }
}
