Hotdry.

Article

MCP 服务器极简原型:stdio 传输、JSON-RPC 解析与 tool 注册工程路径

解析 MCP 服务器最小原型的核心工程路径:stdio 子进程管理、JSON-RPC 请求解析、tool/resource 注册机制与响应序列化工程参数。

2026-05-16ai-systems

当我们谈论 Model Context Protocol(MCP)服务器实现时,最容易陷入的陷阱是过早引入框架、过度设计协议层,或者被官方 SDK 的复杂度劝退。实际上,如果从协议最底层出发,剥离所有依赖,一个可用 MCP 服务器的核心代码量可以控制在几百行以内。本文聚焦极简 stdio 实现路径,给出可直接落地的工程参数与关键实现细节。

stdio 传输的协议约定

MCP 的 stdio 传输本质上是标准输入输出流上的文本协议对话。客户端(通常是 AI 客户端)启动 MCP 服务器作为子进程,通过 stdin 写入 JSON-RPC 请求,通过 stdout 读取响应。stderr 保留给日志输出,不混入主消息流 —— 这是一个常被忽视但非常重要的隔离原则。

消息格式本身是逐行 JSON,每行一个完整的 JSON-RPC 2.0 对象。请求需要 id 字段用于匹配响应,通知类消息(如 notifications/initialized)则不需要 id。所有消息必须使用 UTF-8 编码,且每条消息末尾必须有且仅有一个换行符作为分隔符。这一约定看似简单,却是大多数 MCP 服务器实现中的第一个坑:嵌套换行符在 stdio 中是禁止的,如果你的 tool 描述或 resource 内容包含多行文本,必须使用转义或 base64 编码而非直接嵌入换行。

子进程的启动命令通常由客户端通过配置指定,格式类似 npx @some/mcp-serverpython /path/to/server.py。服务器在启动后不应立即向 stdout 写入任何内容,而是等待客户端发送 initialize 请求,完成协议版本协商后再进入正常运行状态。这个握手阶段的行为定义在 MCP 规范中,但很多简化实现会跳过它,导致客户端认为服务器无响应。

JSON-RPC 请求解析的最小实现

一个健壮的 JSON-RPC 解析层需要处理三类消息:请求(带 id)、通知(无 id)、响应(带 id,由服务器发出)。对于请求和通知,关键是解析 method 字段并分发到对应的处理器;对于响应,关键是匹配 id 并将结果或错误传递给等待的调用方。

解析层本身的实现建议使用流式读取而非一次性加载完整输入。伪代码结构如下:

while (true):
    line = read_line_from_stdin()  # 读取一行,阻塞等待
    if line is empty: break  # EOF 或异常退出
    
    try:
        message = json.parse(line)
    except:
        # 解析失败,发送错误响应
        write_to_stdout(json.dumps({
            "jsonrpc": "2.0",
            "id": null,  # 无法获取原始 id 时为 null
            "error": { "code": -32700, "message": "Parse error" }
        }) + "\n")
        continue
    
    # 分发处理
    if "id" in message:
        # 带 id 的请求或响应
        handle_request(message)
    else:
        # 通知类消息
        handle_notification(message)

关键工程参数:缓冲区大小建议 64KB,足以覆盖绝大多数单条 MCP 消息;解析异常时应继续处理后续消息而不崩溃,因为客户端可能会发送多条消息;错误响应必须包含 jsonrpc: "2.0" 字段,错误码遵循 JSON-RPC 规范,-32700 表示解析错误,-32600 表示非法请求,-32601 表示方法未找到。

tool 与 resource 注册机制

MCP 协议的核心价值在于暴露工具和资源给 AI 客户端。tool 和 resource 的注册发生在 initialize 握手之后,服务器通过 listToolslistResources 两个方法响应客户端的查询。注册本身是动态的 —— 服务器可以在运行时修改可用的工具列表,而不是固定在启动时。

一个 tool 的描述结构包含名称、描述和参数 schema。参数 schema 通常遵循 JSON Schema 格式,这使得 AI 客户端能够理解工具的输入约束并在调用前进行验证。一个最小化的 tool 定义示例:

tool = {
    "name": "get_weather",
    "description": "查询指定城市的当前天气",
    "inputSchema": {
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "城市名称,中文或英文均可"
            }
        },
        "required": ["city"]
    }
}

resource 的定义结构类似,但重点在于 URI 模板和 MIME 类型。AI 客户端通过 readResource 方法配合 URI 来获取具体资源内容。注册时需要提供每个 resource 的 URI pattern,例如 file:///data/{user_id}/configdb://orders/{order_id},pattern 中的变量对应客户端调用时的实际参数。

注册信息的变更通知通过 notifications/tools_changednotifications/resources_changed 两个特殊通知实现。当工具列表发生变化时,服务器主动向客户端推送通知,客户端随后会重新调用 listTools 获取最新列表。这个机制是 MCP 动态性的核心支撑,也是与静态 API 设计的关键区别。

响应序列化与错误处理

响应序列化看似简单,但有几个工程细节需要注意。首先,每个响应必须携带原始请求中的 id,这是 JSON-RPC 的基本要求。其次,成功响应的格式是 { "jsonrpc": "2.0", "id": xxx, "result": {...} },错误响应的格式是 { "jsonrpc": "2.0", "id": xxx, "error": {...} }。两者不能混淆,即使方法执行过程中出现异常,也要返回错误响应而非崩溃退出。

对于 tool 调用的响应,result 字段通常包含调用结果的内容。MCP 规范对 tool 调用的返回格式有一定约定,但核心原则是返回的内容应该足够让 AI 客户端理解执行结果。常见的返回结构是一个包含 content 数组的对象,其中每个 content 项可以是文本(type: "text")或资源引用(type: "resource")。

错误处理有层级之分。第一层是协议层错误,如解析失败或非法请求格式,这类错误返回固定的错误码和消息。第二层是方法层错误,如 tool 不存在或参数校验失败,需要返回具体的错误信息和代码。第三层是业务层错误,即 tool 执行过程中产生的异常,这类错误通常需要转换为对 AI 客户端有意义的描述,而非直接暴露内部堆栈。

一个实用的设计模式是错误分类映射:业务异常映射为 INVALID_PARAMSTOOL_NOT_FOUND,系统异常映射为 INTERNAL_ERROR,数据异常映射为 NOT_FOUND。每种类型都有对应的 HTTP 类比,便于理解和调试。

生命周期管理与状态机

MCP 服务器存在明确的生命周期:初始化(initialize)、运行(处理各类请求和通知)、关闭。启动阶段必须先等待 initialize 请求,完成协议版本协商和能力交换后再进入运行状态。如果在初始化前收到其他请求,规范要求返回 method not found 错误,但更友好的实现会返回特定的状态错误码以提示客户端尚未完成握手。

优雅关闭需要处理 SIGTERM 信号,清理占用的资源(如数据库连接、文件句柄),必要时向客户端发送 notifications/shutdown 通知。粗暴关闭(如 SIGKILL)会导致客户端可能停留在等待响应的状态,下次连接时需要考虑幂等性和状态恢复问题。

会话状态的管理通常在内存中维护,但需要考虑多实例部署场景。如果 MCP 服务器作为无状态服务部署在容器环境中,状态应外部化到共享存储(如 Redis)或通过数据库持久化,避免重启后状态丢失导致客户端行为异常。

调试与监控的实战参数

开发 MCP 服务器时,stdio 的不可见性是一大挑战。建议在 stderr 输出结构化日志,采用 JSON 格式且包含时间戳、请求 ID(如果能获取到)和操作类型。日志级别至少区分 INFO(正常流程)、WARN(可恢复异常)、ERROR(不可恢复异常)三个档次。

健康检查的最小实现是响应一个固定的 ping 方法调用,返回 { "status": "ok" }。这个方法不在 MCP 规范中,但作为运维约定被广泛使用。建议在服务器启动后的前 5 秒内对所有入站请求做计数统计,如果超过阈值还没有收到 initialize,可能是客户端连接失败或协议不匹配,此时主动在 stderr 输出诊断信息。

性能监控关注两个核心指标:请求响应延迟(P50 应低于 100ms,P99 应低于 500ms)和错误率(建议阈值 1%)。工具执行的耗时应该单独统计,因为 AI 客户端对延迟更敏感,而工具本身的执行时间可能受外部依赖影响。如果发现响应延迟异常增长,首先检查是否是单线程解析导致的阻塞 ——stdin/stdout 是同步流,如果解析逻辑中有阻塞调用,会影响整体吞吐量。

资料来源:MCP Hello Page(https://hybridlogic.co.uk/blog/2026/05/mcp-hello-page)、Model Context Protocol 官方规范(https://modelcontextprotocol.io/specification/2025-06-18/basic/transports)。

ai-systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com