free-code-dotnet/docs/基础设施设计/基础设施设计-MCP协议集成.md
应文浩wenhao.ying@xiaobao100.com e25ac591a7 init easy-code
2026-04-06 07:24:24 +08:00

29 KiB
Raw Blame History

基础设施设计 — MCP 协议集成

文档元数据

  • 项目名称: free-code
  • 文档类型: 基础设施设计
  • 原始代码来源: ../../src/services/mcp/22个文件
  • 原始设计意图: 将 MCP 服务器、工具、资源与认证统一抽象为可管理的 .NET 协议层,并支持多传输、多作用域与自动适配
  • 交叉引用: 基础设施设计总览 | 核心模块设计-工具系统 | 原始代码映射

设计目标

MCP 协议层负责把外部工具、远程服务、认证流程与本地执行环境统一起来,向上提供稳定的 .NET 抽象。该层不仅要兼容多种 transport还要把 MCP 工具/命令/资源适配为上层工具系统可消费的接口。

10.1 IMcpClientManager 接口定义

/// <summary>
/// MCP 客户端管理器 — 管理多个 MCP 服务器连接
/// 对应原始 useManageMCPConnections.ts
/// </summary>
public interface IMcpClientManager
{
    /// <summary>连接所有配置的 MCP 服务器</summary>
    Task ConnectServersAsync(CancellationToken ct = default);

    /// <summary>获取所有已连接服务器的工具(适配为 ITool</summary>
    Task<IReadOnlyList<ITool>> GetToolsAsync();

    /// <summary>获取所有已连接服务器的命令(适配为 ICommand</summary>
    Task<IReadOnlyList<ICommand>> GetCommandsAsync();

    /// <summary>列出指定服务器的资源</summary>
    Task<IReadOnlyList<ServerResource>> ListResourcesAsync(
        string? serverName = null, CancellationToken ct = default);

    /// <summary>读取指定资源</summary>
    Task<ResourceContent> ReadResourceAsync(
        string serverName, string resourceUri, CancellationToken ct = default);

    /// <summary>断开指定服务器</summary>
    Task DisconnectServerAsync(string serverName);

    /// <summary>重连指定服务器(用于断线恢复)</summary>
    Task ReconnectServerAsync(string serverName);

    /// <summary>获取所有服务器连接状态</summary>
    IReadOnlyList<MCPServerConnection> GetConnections();

    /// <summary>触发认证流程OAuth</summary>
    Task AuthenticateServerAsync(string serverName);

    /// <summary>重新加载所有配置并重连</summary>
    Task ReloadAsync();
}

10.2 MCPServerConnection 抽象 record

对应原始 types.ts 中的 union type。

/// <summary>
/// MCP 服务器连接状态 — 替代原始 TypeScript 联合类型
/// 原始: ConnectedMCPServer | FailedMCPServer | NeedsAuthMCPServer | PendingMCPServer | DisabledMCPServer
/// </summary>
public abstract record MCPServerConnection
{
    public string Name { get; init; }
    public string ConnectionType { get; init; }
    public ScopedMcpServerConfig Config { get; init; }

    // 类型判别(替代 TypeScript 可辨识联合)
    public bool IsConnected => this is Connected;
    public bool IsFailed => this is Failed;
    public bool NeedsAuth => this is NeedsAuthentication;
    public bool IsPending => this is Pending;
    public bool IsDisabled => this is Disabled;

    public sealed record Connected(
        string Name,
        ScopedMcpServerConfig Config,
        McpClient Client,
        ServerCapabilities Capabilities,
        ServerInfo? ServerInfo,
        string? Instructions,
        Func<Task> Cleanup
    ) : MCPServerConnection { ConnectionType = "connected"; }

    public sealed record Failed(
        string Name,
        ScopedMcpServerConfig Config,
        string? Error
    ) : MCPServerConnection { ConnectionType = "failed"; }

    public sealed record NeedsAuthentication(
        string Name,
        ScopedMcpServerConfig Config
    ) : MCPServerConnection { ConnectionType = "needs-auth"; }

    public sealed record Pending(
        string Name,
        ScopedMcpServerConfig Config,
        int? ReconnectAttempt = null,
        int? MaxReconnectAttempts = null
    ) : MCPServerConnection { ConnectionType = "pending"; }

    public sealed record Disabled(
        string Name,
        ScopedMcpServerConfig Config
    ) : MCPServerConnection { ConnectionType = "disabled"; }
}

10.3 ScopedMcpServerConfig 配置层级

对应原始 types.ts 中的 Zod schema 定义。

/// <summary>
/// MCP 服务器配置 — 替代原始 8 种 Zod schema
/// 使用 FluentValidation 进行校验
/// </summary>
public abstract record ScopedMcpServerConfig
{
    public required ConfigScope Scope { get; init; }
    public string? PluginSource { get; init; }
}

public record StdioServerConfig : ScopedMcpServerConfig
{
    public string Command { get; init; } = "";
    public IReadOnlyList<string> Args { get; init; } = [];
    public IReadOnlyDictionary<string, string>? Env { get; init; }
}

public record SseServerConfig : ScopedMcpServerConfig
{
    public required string Url { get; init; }
    public IReadOnlyDictionary<string, string>? Headers { get; init; }
    public string? HeadersHelper { get; init; }
    public McpOAuthConfig? OAuth { get; init; }
}

public record SseIdeServerConfig : ScopedMcpServerConfig
{
    public required string Url { get; init; }
    public required string IdeName { get; init; }
    public bool IdeRunningInWindows { get; init; }
}

public record WsIdeServerConfig : ScopedMcpServerConfig
{
    public required string Url { get; init; }
    public required string IdeName { get; init; }
    public string? AuthToken { get; init; }
    public bool IdeRunningInWindows { get; init; }
}

public record HttpServerConfig : ScopedMcpServerConfig
{
    public required string Url { get; init; }
    public IReadOnlyDictionary<string, string>? Headers { get; init; }
    public string? HeadersHelper { get; init; }
    public McpOAuthConfig? OAuth { get; init; }
}

public record WebSocketServerConfig : ScopedMcpServerConfig
{
    public required string Url { get; init; }
    public IReadOnlyDictionary<string, string>? Headers { get; init; }
    public string? HeadersHelper { get; init; }
}

public record SdkServerConfig : ScopedMcpServerConfig
{
    public required string ServerName { get; init; }
}

public record ClaudeAiProxyServerConfig : ScopedMcpServerConfig
{
    public required string Url { get; init; }
    public required string Id { get; init; }
}

public enum ConfigScope
{
    Local, User, Project, Dynamic, Enterprise, ClaudeAi, Managed
}

public record McpOAuthConfig
{
    public string? ClientId { get; init; }
    public int? CallbackPort { get; init; }
    public string? AuthServerMetadataUrl { get; init; }
    public bool Xaa { get; init; }
}

10.4 传输层实现

对应原始 @modelcontextprotocol/sdk 中的多种传输 + 自定义 WebSocketTransport。

/// <summary>
/// MCP 传输层抽象 — JSON-RPC 2.0 over various transports
/// </summary>
public interface IMcpTransport : IAsyncDisposable
{
    Task ConnectAsync(CancellationToken ct = default);
    Task SendAsync(JsonRpcMessage message, CancellationToken ct = default);
    IAsyncEnumerable<JsonRpcMessage> ListenAsync(CancellationToken ct = default);
    Task CloseAsync();
    bool IsConnected { get; }
}

/// <summary>
/// Stdio 传输 — 子进程 stdin/stdout
/// 对应原始 StdioClientTransport
/// </summary>
public sealed class StdioTransport : IMcpTransport
{
    private readonly Process _process;
    private readonly Channel<JsonRpcMessage> _incoming = Channel.CreateUnbounded<JsonRpcMessage>();

    public StdioTransport(StdioServerConfig config, string workingDirectory)
    {
        var psi = new ProcessStartInfo
        {
            FileName = config.Command,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
            CreateNoWindow = true,
            WorkingDirectory = workingDirectory,
        };

        foreach (var arg in config.Args)
            psi.ArgumentList.Add(arg);

        // 合并环境变量
        if (config.Env != null)
            foreach (var (key, value) in config.Env)
                psi.Environment[key] = value;

        _process = new Process { StartInfo = psi };
    }

    public async Task ConnectAsync(CancellationToken ct = default)
    {
        _process.Start();

        // 后台读取 stdout → 解析 JSON-RPC 消息 → 写入 channel
        _ = Task.Run(async () =>
        {
            using var reader = new StreamReader(_process.StandardOutput.BaseStream,
                System.Text.Encoding.UTF8);
            while (!reader.EndOfStream && !ct.IsCancellationRequested)
            {
                var line = await reader.ReadLineAsync(ct);
                if (string.IsNullOrEmpty(line)) continue;
                var message = JsonSerializer.Deserialize<JsonRpcMessage>(line);
                if (message != null)
                    await _incoming.Writer.WriteAsync(message, ct);
            }
        }, ct);
    }

    public async Task SendAsync(JsonRpcMessage message, CancellationToken ct = default)
    {
        var json = JsonSerializer.Serialize(message);
        await _process.StandardInput.WriteLineAsync(json.AsMemory(), ct);
        await _process.StandardInput.FlushAsync();
    }

    public IAsyncEnumerable<JsonRpcMessage> ListenAsync(CancellationToken ct = default)
        => _incoming.Reader.ReadAllAsync(ct);

    public bool IsConnected => !_process.HasExited;

    public async ValueTask DisposeAsync()
    {
        if (!_process.HasExited)
        {
            _process.StandardInput.Close();
            if (!_process.WaitForExit(5000))
                _process.Kill(entireProcessTree: true);
        }
        _incoming.Writer.TryComplete();
    }
}

/// <summary>
/// SSE 传输 — HTTP Server-Sent Events
/// 对应原始 SSEClientTransport
/// </summary>
public sealed class SseTransport : IMcpTransport
{
    private readonly HttpClient _httpClient;
    private readonly string _url;
    private readonly Channel<JsonRpcMessage> _incoming = Channel.CreateUnbounded<JsonRpcMessage>();
    private string? _messageEndpoint; // 服务器提供的 POST 端点

    public async Task ConnectAsync(CancellationToken ct = default)
    {
        // GET SSE 流 → 解析 endpoint 事件 → 开始监听
        var response = await _httpClient.GetAsync(_url, HttpCompletionOption.ResponseHeadersRead, ct);
        using var stream = await response.Content.ReadAsStreamAsync(ct);
        using var reader = new StreamReader(stream);

        while (!reader.EndOfStream && !ct.IsCancellationRequested)
        {
            var line = await reader.ReadLineAsync(ct);
            if (string.IsNullOrEmpty(line)) continue;

            if (line.StartsWith("event: endpoint"))
            {
                var dataLine = await reader.ReadLineAsync(ct);
                if (dataLine?.StartsWith("data: ") == true)
                    _messageEndpoint = dataLine[6..];
            }
            else if (line.StartsWith("data: "))
            {
                var json = line[6..];
                var message = JsonSerializer.Deserialize<JsonRpcMessage>(json);
                if (message != null)
                    await _incoming.Writer.WriteAsync(message, ct);
            }
        }
    }

    public async Task SendAsync(JsonRpcMessage message, CancellationToken ct = default)
    {
        if (_messageEndpoint == null)
            throw new InvalidOperationException("SSE transport not connected");

        var content = JsonContent.Create(message);
        await _httpClient.PostAsync(_messageEndpoint, content, ct);
    }

    public IAsyncEnumerable<JsonRpcMessage> ListenAsync(CancellationToken ct = default)
        => _incoming.Reader.ReadAllAsync(ct);

    public bool IsConnected => _messageEndpoint != null;
}

/// <summary>
/// Streamable HTTP 传输 — MCP 2025-03-26 规范
/// 对应原始 StreamableHTTPClientTransport
/// POST 发送消息,响应可能是 JSON 或 SSE
/// </summary>
public sealed class StreamableHttpTransport : IMcpTransport
{
    private readonly HttpClient _httpClient;
    private readonly string _url;
    private readonly Channel<JsonRpcMessage> _incoming = Channel.CreateUnbounded<JsonRpcMessage>();
    private string? _sessionId; // 服务器分配的 Mcp-Session-Id

    public async Task ConnectAsync(CancellationToken ct = default)
    {
        // 发送 initialize JSON-RPC 请求
        var initRequest = new JsonRpcRequest
        {
            Id = "init-1",
            Method = "initialize",
            Params = new { protocolVersion = "2025-03-26", capabilities = new { }, clientInfo = new { name = "free-code", version = "1.0" } }
        };

        var response = await SendInternalAsync(initRequest, ct);
        // 从响应头提取 session ID
        if (response.Headers.Contains("Mcp-Session-Id"))
            _sessionId = response.Headers.GetValues("Mcp-Session-Id").First();
    }

    private async Task<HttpResponseMessage> SendInternalAsync(
        JsonRpcRequest request, CancellationToken ct)
    {
        using var content = JsonContent.Create(request);
        var httpReq = new HttpRequestMessage(HttpMethod.Post, _url) { Content = content };

        // MCP Streamable HTTP 规范: Accept 必须包含 application/json 和 text/event-stream
        httpReq.Headers.Accept.Add(new("application/json"));
        httpReq.Headers.Accept.Add(new("text/event-stream"));

        if (_sessionId != null)
            httpReq.Headers.Add("Mcp-Session-Id", _sessionId);

        return await _httpClient.SendAsync(httpReq, HttpCompletionOption.ResponseHeadersRead, ct);
    }

    // ... SendAsync 和 ListenAsync 实现类似 SSE 但处理 JSON/SSE 双模式响应
}

/// <summary>
/// WebSocket 传输 — 对应原始 WebSocketTransport (自定义实现)
/// </summary>
public sealed class WebSocketTransport : IMcpTransport
{
    private readonly ClientWebSocket _webSocket;
    private readonly string _url;
    private readonly Channel<JsonRpcMessage> _incoming = Channel.CreateUnbounded<JsonRpcMessage>();

    public async Task ConnectAsync(CancellationToken ct = default)
    {
        await _webSocket.ConnectAsync(new Uri(_url), ct);
        _ = ReceiveLoopAsync(ct);
    }

    private async Task ReceiveLoopAsync(CancellationToken ct)
    {
        var buffer = new byte[8192];
        while (_webSocket.State == WebSocketState.Open && !ct.IsCancellationRequested)
        {
            using var ms = new MemoryStream();
            WebSocketReceiveResult result;
            do
            {
                result = await _webSocket.ReceiveAsync(buffer, ct);
                await ms.WriteAsync(buffer, 0, result.Count, ct);
            } while (!result.EndOfMessage);

            var json = System.Text.Encoding.UTF8.GetString(ms.ToArray());
            var message = JsonSerializer.Deserialize<JsonRpcMessage>(json);
            if (message != null)
                await _incoming.Writer.WriteAsync(message, ct);
        }
    }

    public async Task SendAsync(JsonRpcMessage message, CancellationToken ct = default)
    {
        var json = JsonSerializer.Serialize(message);
        var bytes = System.Text.Encoding.UTF8.GetBytes(json);
        await _webSocket.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
    }

    public IAsyncEnumerable<JsonRpcMessage> ListenAsync(CancellationToken ct = default)
        => _incoming.Reader.ReadAllAsync(ct);

    public bool IsConnected => _webSocket.State == WebSocketState.Open;
}

/// <summary>
/// 进程内传输 — 对应原始 InProcessTransport (linked transport pair)
/// 用于 Chrome/Computer Use MCP 服务器在同一进程运行
/// </summary>
public sealed class InProcessTransport : IMcpTransport
{
    private readonly Channel<JsonRpcMessage> _serverToClient = Channel.CreateUnbounded<JsonRpcMessage>();
    private readonly Channel<JsonRpcMessage> _clientToServer = Channel.CreateUnbounded<JsonRpcMessage>();

    // 创建一对连接的传输(客户端 + 服务器各一个)
    public static (InProcessTransport client, InProcessTransport server) CreateLinkedPair()
    {
        var client = new InProcessTransport();
        var server = new InProcessTransport();
        // 交叉连接 channel: client writes → server reads, vice versa
        client._outgoing = server._serverToClient.Writer;
        server._outgoing = client._clientToServer.Writer;
        return (client, server);
    }

    private ChannelWriter<JsonRpcMessage>? _outgoing;
    public IAsyncEnumerable<JsonRpcMessage> ListenAsync(CancellationToken ct = default)
        => _serverToClient.Reader.ReadAllAsync(ct);
    // ...
}

10.5 McpClient 核心

/// <summary>
/// JSON-RPC 2.0 MCP 客户端
/// 对应原始 @modelcontextprotocol/sdk Client
/// </summary>
public sealed class McpClient
{
    private readonly IMcpTransport _transport;
    private int _requestId;
    private readonly ConcurrentDictionary<string, TaskCompletionSource<JsonRpcResponse>> _pending = new();

    public ServerCapabilities? Capabilities { get; private set; }
    public ServerInfo? ServerInfo { get; private set; }
    public string? Instructions { get; private set; }

    public async Task ConnectAsync(CancellationToken ct = default)
    {
        await _transport.ConnectAsync(ct);

        // 发送 initialize → 接收 response
        var response = await SendRequestAsync("initialize", new
        {
            protocolVersion = "2025-03-26",
            capabilities = new { roots = new { }, elicitation = new { } },
            clientInfo = new { name = "free-code", version = "1.0" }
        }, ct);

        Capabilities = DeserializeCapabilities(response);
        ServerInfo = DeserializeServerInfo(response);
        Instructions = DeserializeInstructions(response);

        // 发送 initialized 通知
        await SendNotificationAsync("notifications/initialized", null, ct);
    }

    public async Task<ListToolsResult> ListToolsAsync(CancellationToken ct = default)
    {
        var response = await SendRequestAsync("tools/list", new { }, ct);
        return Deserialize<ListToolsResult>(response);
    }

    public async Task<CallToolResult> CallToolAsync(
        string toolName, JsonElement? arguments = null, CancellationToken ct = default)
    {
        var response = await SendRequestAsync("tools/call", new
        {
            name = toolName,
            arguments
        }, ct);
        return Deserialize<CallToolResult>(response);
    }

    public async Task<ListResourcesResult> ListResourcesAsync(CancellationToken ct = default)
    {
        var response = await SendRequestAsync("resources/list", new { }, ct);
        return Deserialize<ListResourcesResult>(response);
    }

    public async Task<ReadResourceResult> ReadResourceAsync(
        string uri, CancellationToken ct = default)
    {
        var response = await SendRequestAsync("resources/read", new { uri }, ct);
        return Deserialize<ReadResourceResult>(response);
    }

    public async Task<ListPromptsResult> ListPromptsAsync(CancellationToken ct = default)
    {
        var response = await SendRequestAsync("prompts/list", new { }, ct);
        return Deserialize<ListPromptsResult>(response);
    }

    private async Task<JsonElement> SendRequestAsync(
        string method, object? @params, CancellationToken ct)
    {
        var id = Interlocked.Increment(ref _requestId).ToString();
        var tcs = new TaskCompletionSource<JsonRpcResponse>();
        _pending[id] = tcs;

        var message = new JsonRpcRequest { Id = id, Method = method, Params = @params };
        await _transport.SendAsync(message, ct);

        using var reg = ct.Register(() => tcs.TrySetCanceled(ct));
        var response = await tcs.Task;
        return response.Result;
    }

    private async Task SendNotificationAsync(
        string method, object? @params, CancellationToken ct)
    {
        var message = new JsonRpcNotification { Method = method, Params = @params };
        await _transport.SendAsync(message, ct);
    }
}

10.6 McpClientManager 实现

public class McpClientManager : IMcpClientManager
{
    private readonly IServiceProvider _services;
    private readonly IConfiguration _config;
    private readonly IFeatureFlagService _features;
    private readonly IAppStateStore _stateStore;
    private readonly ILogger<McpClientManager> _logger;
    private readonly ConcurrentDictionary<string, MCPServerConnection> _connections = new();

    /// <summary>
    /// 连接所有配置的 MCP 服务器
    /// 对应原始 useManageMCPConnections.ts 的 effect
    /// </summary>
    public async Task ConnectServersAsync(CancellationToken ct = default)
    {
        var configs = await LoadAllServerConfigsAsync();
        var localServers = configs.Where(c => IsLocalServer(c.Value)).ToList();
        var remoteServers = configs.Where(c => !IsLocalServer(c.Value)).ToList();

        // 本地服务器: 批量连接 (默认 concurrency: 3)
        var localBatchSize = GetLocalBatchSize(); // MCP_SERVER_CONNECTION_BATCH_SIZE
        await Parallel.ForEachAsync(localServers,
            new ParallelOptions { MaxDegreeOfParallelism = localBatchSize, CancellationToken = ct },
            async (kvp, _) =>
            {
                var connection = await ConnectToServerAsync(kvp.Key, kvp.Value, ct);
                _connections[kvp.Key] = connection;
                UpdateState();
            });

        // 远程服务器: 批量连接 (默认 concurrency: 20)
        var remoteBatchSize = GetRemoteBatchSize(); // MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE
        await Parallel.ForEachAsync(remoteServers,
            new ParallelOptions { MaxDegreeOfParallelism = remoteBatchSize, CancellationToken = ct },
            async (kvp, _) =>
            {
                var connection = await ConnectToServerAsync(kvp.Key, kvp.Value, ct);
                _connections[kvp.Key] = connection;
                UpdateState();
            });
    }

    private async Task<MCPServerConnection> ConnectToServerAsync(
        string name, ScopedMcpServerConfig config, CancellationToken ct)
    {
        try
        {
            // 1. 创建传输层
            var transport = CreateTransport(name, config);

            // 2. 创建客户端并连接
            var client = new McpClient(transport);
            var timeout = GetConnectionTimeout(); // MCP_TIMEOUT 环境变量, 默认 30s

            using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            timeoutCts.CancelAfter(timeout);

            await client.ConnectAsync(timeoutCts.Token);

            // 3. 获取能力、工具、资源
            var tools = await client.ListToolsAsync(ct);
            var capabilities = client.Capabilities!;

            // 4. 截断过长的 instructions (MAX_MCP_DESCRIPTION_LENGTH = 2048)
            var instructions = client.Instructions;
            if (instructions?.Length > 2048)
                instructions = instructions[..2048] + "… [truncated]";

            return new MCPServerConnection.Connected(name, config, client,
                capabilities, client.ServerInfo, instructions,
                async () => await transport.CloseAsync());
        }
        catch (UnauthorizedException)
        {
            // SSE/HTTP/claudeai-proxy: 需要认证
            return new MCPServerConnection.NeedsAuthentication(name, config);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "MCP server {Name} connection failed", name);
            return new MCPServerConnection.Failed(name, config, ex.Message);
        }
    }

    private IMcpTransport CreateTransport(string name, ScopedMcpServerConfig config)
    {
        return config switch
        {
            StdioServerConfig stdio => new StdioTransport(stdio,
                Directory.GetCurrentDirectory()),
            SseServerConfig sse => new SseTransport(
                CreateHttpClient(sse.Headers), sse.Url),
            SseIdeServerConfig sseIde => new SseTransport(
                new HttpClient(), sseIde.Url),
            HttpServerConfig http => new StreamableHttpTransport(
                CreateHttpClient(http.Headers), http.Url),
            WsIdeServerConfig wsIde => new WebSocketTransport(wsIde.Url, wsIde.AuthToken),
            WebSocketServerConfig ws => new WebSocketTransport(ws.Url, null, ws.Headers),
            ClaudeAiProxyServerConfig proxy => new StreamableHttpTransport(
                CreateProxyHttpClient(), proxy.Url),
            _ => throw new InvalidOperationException($"Unsupported server type: {config}")
        };
    }

    /// <summary>
    /// 获取所有工具并适配为 ITool 接口
    /// 对应原始 MCPTool 适配器
    /// </summary>
    public async Task<IReadOnlyList<ITool>> GetToolsAsync()
    {
        var tools = new List<ITool>();
        foreach (var connection in _connections.Values)
        {
            if (connection is MCPServerConnection.Connected connected)
            {
                var mcpTools = await connected.Client.ListToolsAsync();
                foreach (var tool in mcpTools.Tools)
                {
                    tools.Add(new McpToolWrapper(connected.Name, tool, connected.Client));
                }
            }
        }
        return tools;
    }

    /// <summary>更新 AppState 中的 MCP 状态</summary>
    private void UpdateState()
    {
        _stateStore.Update(state => state with
        {
            Mcp = state.Mcp with
            {
                Clients = _connections.Values.ToList(),
                Tools = GetToolsFromConnections(),
            }
        });
    }

    private static bool IsLocalServer(ScopedMcpServerConfig config) =>
        config is StdioServerConfig or SdkServerConfig;
}

/// <summary>
/// MCP 工具包装器 — 将 MCP tool 适配为 ITool 接口
/// 对应原始 MCPTool.ts
/// </summary>
public sealed class McpToolWrapper : ITool<JsonElement, JsonElement>
{
    private readonly string _serverName;
    private readonly McpToolDefinition _definition;
    private readonly McpClient _client;

    public string Name => $"mcp__{_serverName}__{_definition.Name}";
    public ToolCategory Category => ToolCategory.Mcp;

    public async Task<ToolResult<JsonElement>> ExecuteAsync(
        JsonElement input, ToolExecutionContext context, CancellationToken ct)
    {
        var timeout = GetMcpToolTimeout(); // MCP_TOOL_TIMEOUT, 默认 ~27.8h
        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        timeoutCts.CancelAfter(timeout);

        try
        {
            var result = await _client.CallToolAsync(_definition.Name, input, timeoutCts.Token);

            // 处理二进制内容 → 持久化到文件
            if (result.HasBinaryContent)
            {
                var persisted = await PersistBinaryContentAsync(result);
                return new ToolResult<JsonElement>(persisted);
            }

            return new ToolResult<JsonElement>(result.Content);
        }
        catch (McpAuthException)
        {
            // 标记服务器需要认证
            throw;
        }
    }

    public JsonElement GetInputSchema() => _definition.InputSchema;
    public bool IsReadOnly(JsonElement input) => !_definition.HasDestructiveBehavior;
    public bool IsConcurrencySafe(JsonElement input) => false;
}

10.7 McpAuthService

/// <summary>
/// MCP OAuth 认证 — 对应原始 auth.ts 中的 ClaudeAuthProvider
/// </summary>
public sealed class McpAuthService
{
    private readonly IdentityModel.OidcClient _oidcClient;
    private readonly ISecureTokenStorage _tokenStorage; // macOS Keychain / credential manager

    /// <summary>
    /// 执行 OAuth 授权流程: 发现 → 浏览器授权 → code 交换 → token 存储
    /// </summary>
    public async Task<McpOAuthTokens> AuthorizeAsync(
        string serverName, McpOAuthConfig config, CancellationToken ct)
    {
        // 1. 检查缓存的 token
        var cached = await _tokenStorage.GetAsync($"mcp-{serverName}");
        if (cached != null && !cached.IsExpired)
            return cached;

        // 2. 发现授权服务器元数据
        var metadata = await DiscoverAuthorizationServerAsync(config.AuthServerMetadataUrl!, ct);

        // 3. 启动本地 HTTP 监听器
        var callbackPort = config.CallbackPort ?? GetAvailablePort();
        using var listener = new HttpListener();
        listener.Prefixes.Add($"http://localhost:{callbackPort}/");
        listener.Start();

        // 4. 构建授权 URL → 打开浏览器
        var authUrl = BuildAuthorizationUrl(metadata, callbackPort);
        OpenBrowser(authUrl);

        // 5. 等待回调 → 提取 code
        var context = await listener.GetContextAsync();
        var code = context.Request.QueryString["code"];

        // 6. 交换 token
        var tokens = await ExchangeCodeAsync(metadata, code, callbackPort, ct);
        await _tokenStorage.SetAsync($"mcp-{serverName}", tokens);

        return tokens;
    }
}