MCP 通信基础与架构位置
Model Context Protocol (MCP) 作为一种连接 AI 大模型与外部上下文的标准协议,其核心在于定义了双方如何交换信息。协议本身主要关注数据结构与交互流程,比如请求的格式、响应的规范以及错误处理机制。然而,这些数据结构要在客户端(Client)和服务端(Server)之间真正流动起来,必须依赖具体的传输层(Transport)。
协议规范将传输层与协议逻辑进行了分离。这意味着 MCP 的消息内容(基于 JSON-RPC)保持不变,但承载这些消息的底层通道可以灵活选择。目前的 MCP 规范定义了两种标准的传输机制:Streamable HTTP 和 stdio。相比于基于网络的 HTTP 传输,stdio 是一种更为基础且紧密的通信方式,常用于本地环境下的进程间通信。
在 stdio 传输模式下,MCP 客户端并不通过网络端口连接服务端,而是直接将服务端作为一个子进程(Subprocess)启动。这种架构决定了通信的高效性与私密性,因为数据仅在本地内存与标准输入输出流之间传递,不涉及网络协议栈的开销。对于大多数在本地运行的 MCP 服务(如本地文件系统访问、本地数据库查询工具)而言,stdio 是首选且默认的传输方式。
STDIO 传输机制的工作原理
理解 stdio 传输机制的关键,在于掌握操作系统中标准流的概念。当客户端启动服务端进程时,操作系统会为该子进程分配三个默认的数据流通道。MCP 协议巧妙地利用了这些通道来实现全双工通信与日志分离。
客户端通过写入子进程的标准输入(stdin)来发送请求。服务端进程会持续监听这个输入流,一旦检测到完整的消息,便进行解析与处理。处理完成后,服务端将响应数据写入自身的标准输出(stdout),客户端通过读取该输出流来获取执行结果。这两个通道构成了 MCP 协议的主通信回路。

除了数据交换,服务端在运行过程中难免需要输出调试信息或错误日志。如果这些非结构化的日志直接混入标准输出,将会破坏 JSON-RPC 消息的格式,导致客户端解析失败。因此,MCP 规范明确规定,标准错误(stderr)通道用于传输日志与调试信息。客户端可以独立捕获该通道的内容进行记录或展示,而不干扰主通信流程。
下表详细对比了这三个通道在 MCP stdio 传输中的具体职责与行为规范:
| 通道名称 (Channel) | 数据流向 (Direction) | 主要内容 (Content) | 行为约束 (Constraint) |
|---|---|---|---|
| Standard Input (stdin) | 客户端 -> 服务端 | JSON-RPC 请求、通知 | 必须是合法的 JSON-RPC 消息,不得写入无关数据 |
| Standard Output (stdout) | 服务端 -> 客户端 | JSON-RPC 响应、通知 | 严禁输出任何非协议格式的文本(如 print 打印的调试信息) |
| Standard Error (stderr) | 服务端 -> 客户端 | 结构化日志、调试文本 | 允许输出任意 UTF-8 字符串,客户端可选择性捕获或忽略 |
消息编码与格式规范
在建立了传输通道之后,传输的内容必须遵循严格的编码规范。MCP 协议基于 JSON-RPC 2.0 标准,所有通过 stdio 传输的消息必须是 UTF-8 编码的文本。为了确保流式读取的准确性,消息之间采用换行符(Newline)作为分隔符。
这意味着每一个发出的 JSON-RPC 消息必须被压缩为单行文本。在消息体内部,不能包含未转义的换行符,否则接收端会错误地将其识别为消息结束的标志。接收端(无论是客户端还是服务端)通常采用“按行读取”的策略:持续读取输入流,直到遇到换行符,然后将缓冲区中的字符串尝试解析为 JSON 对象。
一个典型的通信过程包含请求与响应。当客户端希望服务端执行操作时,会构造一个包含 jsonrpc 版本号、消息 ID (id)、方法名 (method) 以及参数 (params) 的对象。该对象被序列化为无换行符的字符串后写入标准输入。
服务端接收并处理后,通过标准输出返回响应。响应对象同样包含 jsonrpc 版本号、对应的消息 ID 以及执行结果 (result) 或错误信息 (error)。这种严格的请求-响应对应关系,使得即便在异步处理的情况下,客户端也能准确匹配每一次调用的结果。
协议生命周期:初始化阶段
任何 MCP 连接建立后的首要任务是完成初始化(Initialization)。这是一个强制性的握手过程,用于协商协议版本与功能能力(Capabilities)。在 stdio 模式下,子进程启动后,客户端必须作为发起方,发送第一条 initialize 请求。
初始化请求不仅仅是打个招呼,它承载了至关重要的配置信息。客户端需要声明自己支持的协议版本(例如 2025-06-18),并告知服务端自身具备哪些能力(如是否支持采样 sampling 或根目录列表 roots)。
以下是一个标准的初始化请求示例,展示了客户端如何向服务端发送自身的元数据与能力声明:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {}
},
"clientInfo": {
"name": "MyMCPClient",
"title": "My MCP Client",
"version": "1.0.0"
}
}
}
服务端接收到此请求后,必须进行版本匹配。如果服务端支持客户端请求的协议版本,它将返回相同的版本号;如果不支持,它应返回自己支持的最新版本。同时,服务端也会在响应中列出自己的能力(如是否支持工具调用 tools、资源订阅 resources 或提示词 prompts)。
只有当客户端收到初始化响应,并确认双方能力匹配后,才会发送一条 notifications/initialized 通知。这条通知标志着握手阶段的正式结束,双方随即进入正常操作阶段,开始发送实际的业务请求(如列出工具、读取资源等)。在此之前,除了 Ping 消息外,双方不应进行其他通信。
使用 Python 模拟 MCP STDIO 服务端
为了更直观地理解 stdio 的工作方式,我们可以编写一个极简的 Python 脚本来模拟 MCP 服务端。这个脚本不依赖复杂的 SDK,而是直接操作标准输入输出流,从而揭示底层的通信逻辑。
这个模拟服务端的主要逻辑是:循环读取 sys.stdin 的每一行,将其解析为 JSON,如果是初始化请求,则按照规范格式化输出响应写入 sys.stdout,并强制刷新缓冲区以确保消息即时发送。
创建一个名为 simple_mcp_server.py 的文件,写入以下代码:
import sys
import json
def main():
# 持续监听标准输入
for line in sys.stdin:
try:
# 去除行末换行符并解析 JSON
request = json.loads(line.strip())
# 简单的路由逻辑
if request.get("method") == "initialize":
# 构造符合 MCP 规范的初始化响应
response = {
"jsonrpc": "2.0",
"id": request.get("id"),
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {"listChanged": True},
"logging": {}
},
"serverInfo": {
"name": "SimplePythonServer",
"version": "0.1.0"
}
}
}
# 写入标准输出,必须添加换行符作为分隔
sys.stdout.write(json.dumps(response) + "\n")
# 必须刷新缓冲区,否则客户端可能无法立即收到
sys.stdout.flush()
# 向 stderr 写入日志,模拟调试信息
sys.stderr.write("[INFO] Initialization handled successfully\n")
sys.stderr.flush()
elif request.get("method") == "notifications/initialized":
# 收到初始化完成通知,记录日志
sys.stderr.write("[INFO] Client initialized. Ready for operations.\n")
sys.stderr.flush()
# 这里可以添加更多的 method 处理逻辑
except json.JSONDecodeError:
sys.stderr.write("[ERROR] Invalid JSON received\n")
sys.stderr.flush()
if __name__ == "__main__":
main()
此代码展示了 stdio 传输的核心操作:读取一行、处理逻辑、写入一行、刷新缓冲。sys.stderr.write 的使用演示了如何在不破坏协议流的前提下输出日志。在实际部署中,客户端(如 Claude Desktop)会自动执行此脚本,并接管其输入输出流。
- 打开终端,进入该文件所在的目录。输入命令并回车:
python3 simple_mcp_server.py此时你会发现程序“没反应”,实际上它正在后台静静等待你给它发指令。
2. 复制下面这段 JSON 文本,粘贴到终端里,然后按回车:
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
结果(你会看到): 程序会立刻打印出两行内容:
- JSON 响应(这是发给 AI 看的):
{"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2025-06-18", ...}} - 日志信息(这是发给开发者看的):
[INFO] Initialization handled successfully

错误处理与日志的最佳实践
在 stdio 传输模式下,错误处理分为协议层面的错误和传输层面的异常。协议层面的错误(如方法未找到、参数无效)应通过 JSON-RPC 的 error 响应对象返回给客户端。这属于正常的通信流程,数据依然通过 stdout 传输。
然而,对于服务端内部的运行时异常、调试堆栈或性能监控数据,绝不能直接打印到控制台。在许多编程语言中,print() 函数默认输出到 stdout,这在开发普通命令行工具时很方便,但在开发 MCP 服务时是致命的。一旦一段纯文本(如 Value is 10)被写入 stdout,客户端在解析 JSON 时会抛出语法错误,导致连接中断。
因此,开发 MCP 服务端时,必须严格配置日志系统,将其重定向到 stderr。大多数成熟的日志库都支持配置输出流。如果不确定所用库的行为,最安全的做法是显式捕获所有未处理的异常,并将其格式化后写入 stderr,同时确保主进程在发生非致命错误时不会意外退出。
连接关闭与资源清理
MCP 协议定义了清晰的生命周期,这也包括连接的终止(Shutdown)。在 stdio 模式下,关闭连接通常由客户端发起。
正常的关闭流程并不依赖特定的 JSON-RPC 消息,而是利用底层流的特性。客户端会首先关闭发送给服务端进程的标准输入流(stdin)。服务端进程在检测到输入流结束(End-of-File / EOF)后,应当理解为会话结束,随即执行清理工作(如关闭数据库连接、保存状态),然后自行终止进程。
如果服务端在输入流关闭后长时间未退出,客户端会采取更强制的措施。规范建议客户端先发送 SIGTERM 信号,给予服务端最后一次优雅退出的机会。若服务端仍无响应,客户端将发送 SIGKILL 信号强制杀掉子进程。因此,在实现服务端时,监听输入流的关闭事件或系统的终止信号,是保证资源不泄露的关键环节。
开发必备:API 全流程管理神器 Apifox
介绍完上文的内容,我想额外介绍一个对开发者同样重要的效率工具 —— Apifox。作为一个集 API 文档、API 调试、API 设计、API 测试、API Mock、自动化测试等功能于一体的 API 管理工具,Apifox 可以说是开发者提升效率的必备工具之一。
如果你正在开发项目需要进行接口调试,不妨试试 Apifox。注册过程非常简单,你可以直接在这里注册使用。

注册成功后可以先看看官方提供的示例项目,这些案例都是经过精心设计的,能帮助你快速了解 Apifox 的主要功能。
使用 Apifox 的一大优势是它完全兼容 Postman 和 Swagger 数据格式,如果你之前使用过这些工具,数据导入会非常方便。而且它的界面设计非常友好,即使是第一次接触的新手也能很快上手,快去试试吧!
