MCP 通信基础与架构位置
Model Context Protocol(MCP)本质上是一套连接 AI 大模型与外部上下文的标准协议,其核心价值在于清晰定义了双方如何交换信息——请求的结构、响应的格式、错误的处理方式。然而,这些数据结构要在客户端和服务端之间真正“流动”,还得依赖具体的传输层来实现。
协议规范在设计上刻意将传输层与协议逻辑分离。这意味着什么呢?MCP 的消息内容(基于 JSON-RPC)保持不变,但承载消息的底层通道可以灵活选择。目前 MCP 规范定义了两套标准传输机制:Streamable HTTP 和 stdio。相比于基于网络的 HTTP 传输,stdio 是更底层、更紧密的通信方式,天然适合在本地环境下进行进程间通信。
在 stdio 模式下,客户端并不通过网络端口连接服务端,而是直接以子进程(Subprocess)方式启动服务端。这种架构决定了通信的高效与私密——数据仅在本地内存和标准输入输出流之间流转,完全不经过网络协议栈。对大多数本地运行的 MCP 服务(例如本地文件系统访问、本地数据库查询工具)来说,stdio 是默认选择,也是最优方案。
STDIO 传输机制的工作原理
要理解 stdio 传输机制,关键在于先了解操作系统中“标准流”的概念。客户端启动服务端进程时,操作系统会为该子进程分配三个默认的数据流通道。MCP 协议巧妙地将这些通道利用起来,既实现了全双工通信,又做到了日志与业务数据的分离。
客户端如何发送请求?只需向子进程的标准输入(stdin)写入数据即可。服务端进程会持续监听这个输入流,一旦发现一条完整的消息便解析并处理。处理完成后,服务端将响应数据写到自己的标准输出(stdout),客户端通过读取该输出流获取结果。这两条通道构成了 MCP 协议的主通信回路。

当然,服务端运行过程中难免会输出调试信息或错误日志。此类非结构化的日志如果直接混入标准输出,会破坏 JSON-RPC 消息的格式,导致客户端解析失败。因此 MCP 规范明确规定:标准错误(stderr)通道专门用于传递日志和调试信息。客户端可以独立捕获该通道的内容,无论是记录还是展示,都不会干扰主通信流程。
下面这张表梳理了三个通道在 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
此时你会看到程序似乎“没反应”——实际上它正在后台安静等待你发送指令。
- 复制下面这段 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 协议的生命周期管理不仅包括如何建立连接,也涉及如何优雅地断开。在 stdio 模式下,关闭连接通常由客户端发起。
正常的关闭流程不依赖特定的 JSON-RPC 消息,而是利用底层流的特性。客户端首先关闭向服务端进程发送的标准输入流(stdin)。服务端进程检测到输入流结束(EOF)后,应认识到会话结束,随即执行清理操作(例如关闭数据库连接、保存状态),然后自行退出。
如果服务端在输入流关闭后迟迟不退出,客户端会采取更强硬的措施。规范建议先发送 SIGTERM 信号,给服务端最后一次优雅退出的机会;若仍无响应,则发送 SIGKILL 强制终止子进程。因此,在实现服务端时,监听输入流关闭事件或系统的终止信号,是确保资源不泄露的关键环节。
