# MCP SDK 自研实现方案 > 关联文档:[总体概述](../总体概述与技术选型.md) | [MCP协议集成](../../基础设施设计/基础设施设计-MCP协议集成.md) --- ## 1. 为什么自研 社区目前没有成熟的 .NET MCP SDK。检索 NuGet.org 和相关 GitHub 仓库后,现状如下: - 微软官方尚未发布 `Microsoft.Extensions.Mcp` 正式包(截至 2026 年初仍在 preview 阶段,API 不稳定) - 社区的几个实验性实现覆盖场景有限,停留在 MCP 协议的早期版本,不支持 Streamable HTTP Transport - 没有任何包支持原生 AOT 编译,大量依赖运行时反射 自研的核心收益: 1. **协议版本精确对齐**,与 TypeScript SDK 的行为保持一致,减少互操作问题 2. **传输层可插拔**,在测试环境使用 InProcessTransport,生产环境使用 StdioTransport 或 SseTransport 3. **AOT 友好**,所有序列化走 `JsonSerializerContext`,无运行时反射 4. **依赖最小化**,不引入不受控的第三方包,安全审查更简单 --- ## 2. JSON-RPC 2.0 协议基础 MCP 基于 JSON-RPC 2.0 协议。三种消息类型构成所有交互: **请求 (Request)** 客户端发起,期待服务端响应。必须携带 `id`,方法名决定语义: ```json { "jsonrpc": "2.0", "id": "req-001", "method": "tools/call", "params": { "name": "read_file", "arguments": { "path": "/tmp/foo.txt" } } } ``` **响应 (Response)** 服务端对请求的回复,`id` 必须与请求一致。成功用 `result`,失败用 `error`: ```json { "jsonrpc": "2.0", "id": "req-001", "result": { "content": [{ "type": "text", "text": "file contents here" }] } } ``` **通知 (Notification)** 单向消息,不携带 `id`,无需响应: ```json { "jsonrpc": "2.0", "method": "notifications/tools/list_changed" } ``` 对应的 C# 模型: ```csharp public record JsonRpcRequest( string JsonRpc, string Id, string Method, JsonElement? Params ); public record JsonRpcResponse( string JsonRpc, string Id, JsonElement? Result, JsonRpcError? Error ); public record JsonRpcNotification( string JsonRpc, string Method, JsonElement? Params ); public record JsonRpcError(int Code, string Message, JsonElement? Data); ``` --- ## 3. 传输层实现 传输层负责消息的发送和接收,与协议逻辑解耦。所有传输层实现同一接口: ```csharp public interface ITransport : IAsyncDisposable { Task ReadMessageAsync(CancellationToken ct); Task SendMessageAsync(JsonRpcMessage message, CancellationToken ct); Task ConnectAsync(CancellationToken ct); Task CloseAsync(CancellationToken ct); } ``` ### StdioTransport 通过子进程的 stdin/stdout 通信,是 MCP 最常见的本地工具接入方式: ```csharp public sealed class StdioTransport : ITransport { private readonly Process _process; private readonly Channel _inbound = Channel.CreateUnbounded(); public async Task ConnectAsync(CancellationToken ct) { _process.Start(); // 启动后台读取循环,将 stdout 行解析为 JsonRpcMessage 写入 Channel _ = Task.Run(() => ReadLoopAsync(ct), ct); } public async Task ReadMessageAsync(CancellationToken ct) => await _inbound.Reader.ReadAsync(ct); public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken ct) { var json = JsonSerializer.Serialize(message, FreeCodeJsonContext.Default.JsonRpcMessage); await _process.StandardInput.WriteLineAsync(json.AsMemory(), ct); await _process.StandardInput.FlushAsync(ct); } } ``` 每行 JSON 对应一条消息,严格遵循 MCP 规范的 newline-delimited JSON 格式。 ### SseTransport 服务端通过 HTTP Server-Sent Events 推送消息,客户端通过 HTTP POST 发送: ```csharp public sealed class SseTransport : ITransport { private readonly HttpClient _http; private readonly Uri _sseEndpoint; private readonly Uri _postEndpoint; public async Task ConnectAsync(CancellationToken ct) { var response = await _http.GetAsync( _sseEndpoint, HttpCompletionOption.ResponseHeadersRead, ct); // 以流方式持续读取 SSE 事件行 _ = Task.Run(() => ReadSseLoopAsync(response, ct), ct); } } ``` ### StreamableHttpTransport MCP 协议 2025-03-26 版本引入的新传输方式,支持单次 HTTP 请求内的双向流式通信: ```csharp public sealed class StreamableHttpTransport : ITransport { // 使用 HttpClient 发送请求,响应体作为 NDJSON 流持续读取 // 请求体也可以是 NDJSON 流(客户端到服务端) // 通过 Content-Type: application/jsonl 区分 } ``` ### WebSocketTransport 适用于需要长连接和低延迟的场景: ```csharp public sealed class WebSocketTransport : ITransport { private readonly ClientWebSocket _ws = new(); public async Task ConnectAsync(CancellationToken ct) { await _ws.ConnectAsync(_uri, ct); _ = Task.Run(() => ReceiveLoopAsync(ct), ct); } public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken ct) { var bytes = JsonSerializer.SerializeToUtf8Bytes( message, FreeCodeJsonContext.Default.JsonRpcMessage); await _ws.SendAsync(bytes, WebSocketMessageType.Text, true, ct); } } ``` ### InProcessTransport 测试和嵌入场景专用,通过一对 `Channel` 模拟双向通信,无网络开销: ```csharp public sealed class InProcessTransport : ITransport { private readonly Channel _clientToServer; private readonly Channel _serverToClient; // 客户端和服务端各持有一端的 Reader/Writer // 完全同步,deterministic,适合单元测试 } ``` --- ## 4. 客户端生命周期 MCP 客户端从连接到关闭经历五个阶段: ``` connect → initialize → initialized → [tool calls / resource reads] → shutdown ``` **阶段一:connect** 传输层建立物理连接(进程启动、HTTP 连接、WebSocket 握手)。 **阶段二:initialize** 客户端发送 `initialize` 请求,声明协议版本和能力: ```json { "jsonrpc": "2.0", "id": "init-1", "method": "initialize", "params": { "protocolVersion": "2025-03-26", "clientInfo": { "name": "FreeCode", "version": "0.1.0" }, "capabilities": { "roots": { "listChanged": true } } } } ``` **阶段三:initialized** 服务端响应 `initialize`,返回其支持的能力和协议版本后,客户端发送 `notifications/initialized` 通知,握手完成。 **阶段四:工具调用** 握手完成后,客户端可以发起任意 `tools/call`、`resources/read`、`prompts/get` 等请求。`McpClient` 内部维护一个 pending request 字典,通过 `id` 匹配请求和响应: ```csharp public async Task CallToolAsync( string toolName, IReadOnlyDictionary arguments, CancellationToken ct) { var id = Guid.NewGuid().ToString("N"); var tcs = new TaskCompletionSource(); _pending[id] = tcs; await _transport.SendMessageAsync(new JsonRpcRequest( "2.0", id, "tools/call", BuildParams(toolName, arguments)), ct); var response = await tcs.Task.WaitAsync(ct); return ParseCallToolResult(response); } ``` **阶段五:shutdown** 客户端发送 `shutdown` 请求,等待服务端 `null` 响应后发送 `exit` 通知,最后关闭传输层。 --- ## 5. MCP 工具适配为 ITool 接口 McpToolWrapper 将 MCP 服务端暴露的工具包装为 FreeCode 的 `ITool` 接口,使得 MCP 工具和本地工具在 Engine 层完全透明: ```csharp public sealed class McpToolWrapper : ITool { private readonly McpClient _client; private readonly McpToolDefinition _definition; public string Name => $"mcp_{_client.ServerId}_{_definition.Name}"; public string Description => _definition.Description ?? string.Empty; public JsonElement InputSchema => _definition.InputSchema; public async Task ExecuteAsync( JsonElement input, CancellationToken ct) { var args = ParseArguments(input); var result = await _client.CallToolAsync(_definition.Name, args, ct); return new ToolResult( Success: !result.IsError, Content: result.Content.Select(MapContent).ToList()); } } ``` McpManager 在启动时连接所有配置的 MCP 服务器,拉取工具列表,为每个工具创建 McpToolWrapper 并注册到 ToolRegistry。Engine 层通过 ToolRegistry 统一调度,无需感知来源。 --- ## 6. 与 TypeScript SDK 对比 | 特性 | @modelcontextprotocol/sdk (TS) | FreeCode.Mcp (C# 自研) | |------|-------------------------------|----------------------| | 传输层 | Stdio、SSE、StreamableHttp | Stdio、SSE、StreamableHttp、WebSocket、InProcess | | 序列化 | Zod 运行时验证 | System.Text.Json + Source Generator | | AOT 支持 | 不适用 | 完全支持 | | 异步模型 | Promise / async-await | ValueTask / Channel\ | | 协议版本 | 跟随官方 | 对齐 2025-03-26 | | 测试支持 | Jest mock | InProcessTransport | | 服务端实现 | 完整 | 计划中(v0.3) | TypeScript SDK 使用 Zod schema 自动验证所有传入和传出消息。C# 自研版本通过 `JsonSerializerContext` 反序列化时的类型约束和 FluentValidation 检查实现等效的安全性,且无运行时反射开销。 --- ## 7. 关键设计决策 **Channel\ 用于异步消息传递** 传输层的读取循环与业务调用层之间通过 `Channel` 解耦。读取循环是一个长期运行的后台任务,不阻塞调用线程。`Channel` 支持背压(有界通道)和取消,比 `BlockingCollection` 更适合全异步场景。 **不可变 Record 表示协议状态** 所有 JSON-RPC 消息和 MCP 协议模型都定义为 C# `record`,天然不可变。消息一旦构建就不会被修改,消除了多线程读写竞争的可能,也使测试断言更简单。 **id 用字符串而不是整数** JSON-RPC 2.0 允许 id 为字符串或整数。FreeCode.Mcp 统一使用 `Guid.NewGuid().ToString("N")` 生成字符串 id,避免高并发下的整数碰撞,也方便日志追踪。 **传输层不感知协议** `ITransport` 只负责原始 `JsonRpcMessage` 的收发,不理解 `initialize`、`tools/call` 等语义。协议逻辑完全在 `McpClient` 和 `McpServer` 中,传输层可以随时替换,不影响上层逻辑。这个边界在集成测试中尤其有价值:用 InProcessTransport 替换 StdioTransport,测试速度从秒级降到毫秒级。