应文浩wenhao.ying@xiaobao100.com e25ac591a7 init easy-code
2026-04-06 07:24:24 +08:00

347 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<JsonRpcMessage> 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<JsonRpcMessage> _inbound =
Channel.CreateUnbounded<JsonRpcMessage>();
public async Task ConnectAsync(CancellationToken ct)
{
_process.Start();
// 启动后台读取循环,将 stdout 行解析为 JsonRpcMessage 写入 Channel
_ = Task.Run(() => ReadLoopAsync(ct), ct);
}
public async Task<JsonRpcMessage> 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<JsonRpcMessage>` 模拟双向通信,无网络开销:
```csharp
public sealed class InProcessTransport : ITransport
{
private readonly Channel<JsonRpcMessage> _clientToServer;
private readonly Channel<JsonRpcMessage> _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<CallToolResult> CallToolAsync(
string toolName,
IReadOnlyDictionary<string, JsonElement> arguments,
CancellationToken ct)
{
var id = Guid.NewGuid().ToString("N");
var tcs = new TaskCompletionSource<JsonRpcResponse>();
_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<ToolResult> 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\<T\> |
| 协议版本 | 跟随官方 | 对齐 2025-03-26 |
| 测试支持 | Jest mock | InProcessTransport |
| 服务端实现 | 完整 | 计划中v0.3 |
TypeScript SDK 使用 Zod schema 自动验证所有传入和传出消息。C# 自研版本通过 `JsonSerializerContext` 反序列化时的类型约束和 FluentValidation 检查实现等效的安全性,且无运行时反射开销。
---
## 7. 关键设计决策
**Channel\<T\> 用于异步消息传递**
传输层的读取循环与业务调用层之间通过 `Channel<JsonRpcMessage>` 解耦。读取循环是一个长期运行的后台任务,不阻塞调用线程。`Channel<T>` 支持背压(有界通道)和取消,比 `BlockingCollection<T>` 更适合全异步场景。
**不可变 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测试速度从秒级降到毫秒级。