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

11 KiB
Raw Blame History

MCP SDK 自研实现方案

关联文档:总体概述 | MCP协议集成


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,方法名决定语义:

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "method": "tools/call",
  "params": {
    "name": "read_file",
    "arguments": { "path": "/tmp/foo.txt" }
  }
}

响应 (Response)

服务端对请求的回复,id 必须与请求一致。成功用 result,失败用 error

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "result": {
    "content": [{ "type": "text", "text": "file contents here" }]
  }
}

通知 (Notification)

单向消息,不携带 id,无需响应:

{
  "jsonrpc": "2.0",
  "method": "notifications/tools/list_changed"
}

对应的 C# 模型:

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. 传输层实现

传输层负责消息的发送和接收,与协议逻辑解耦。所有传输层实现同一接口:

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 最常见的本地工具接入方式:

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 发送:

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 请求内的双向流式通信:

public sealed class StreamableHttpTransport : ITransport
{
    // 使用 HttpClient 发送请求,响应体作为 NDJSON 流持续读取
    // 请求体也可以是 NDJSON 流(客户端到服务端)
    // 通过 Content-Type: application/jsonl 区分
}

WebSocketTransport

适用于需要长连接和低延迟的场景:

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> 模拟双向通信,无网络开销:

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 请求,声明协议版本和能力:

{
  "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/callresources/readprompts/get 等请求。McpClient 内部维护一个 pending request 字典,通过 id 匹配请求和响应:

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 层完全透明:

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 的收发,不理解 initializetools/call 等语义。协议逻辑完全在 McpClientMcpServer 中,传输层可以随时替换,不影响上层逻辑。这个边界在集成测试中尤其有价值:用 InProcessTransport 替换 StdioTransport测试速度从秒级降到毫秒级。