MCP传输方式详解:stdio、HTTP、WebSocket到底该选哪个?我做了个对比测试
写在前面
前两篇文章讲MCP的时候,例子用的都是stdio传输——也就是客户端通过标准输入输出跟Server进程通信。很多读者跟着跑通了Demo,但也有人问:“我的Server要部署在另一台机器上,stdio还能用吗?”
答案是不能。stdio只能在同一台机器上用,因为它依赖进程间的管道通信。如果你的MCP Server要部署在远程服务器上,或者要被多个客户端同时访问,就需要换一种传输方式。
这篇文章我就把MCP支持的三种传输方式——stdio、HTTP/SSE、WebSocket——全部讲清楚,包括它们的原理、优缺点、适用场景,以及我自己做对比测试时的一些发现。
一、先搞清楚:传输层在MCP里是什么位置
在深入具体传输方式之前,先回顾一下MCP的架构。MCP协议本身是跟传输方式无关的——它定义的是消息格式(JSON-RPC 2.0)和交互流程(握手、工具发现、调用),但不规定这些消息通过什么通道传递。
这就好比HTTP协议:它定义了请求响应的格式,但可以跑在TCP之上,也可以跑在TLS之上,甚至可以跑在QUIC之上。传输方式变了,协议本身不变。
MCP目前官方支持三种传输方式:
- stdio:通过标准输入输出传递消息,适合本地单进程场景
- HTTP + SSE:通过HTTP发送请求,通过SSE(Server-Sent Events)接收推送,适合Web场景
- WebSocket:全双工通信,适合需要双向实时通信的场景
下面我一个一个说。
二、stdio传输:最简单也最常用
stdio是MCP最基础的传输方式,也是官方示例里用得最多的。它的原理很简单:
- 客户端启动Server作为子进程
- 客户端通过Server的stdin发送JSON-RPC请求
- Server通过stdout返回JSON-RPC响应
- stderr用于输出日志和错误信息
代码示例
服务端:
# server_stdio.py
from mcp.server import Server
from mcp.types import Tool, TextContent
import asyncio
server = Server("demo-server")
@server.list_tools()
async def list_tools():
return [Tool(name="hello", description="打招呼", inputSchema={"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]})]
@server.call_tool()
async def call_tool(name, arguments):
return [TextContent(type="text", text=f"你好,{arguments['name']}!")]
if __name__ == "__main__":
asyncio.run(server.run_stdio())
客户端:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(command="python", args=["server_stdio.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("hello", {"name": "张三"})
print(result.content[0].text)
优点
- 配置最简单:不需要开端口、不需要配URL,指定启动命令就行
- 安全性好:Server只跟启动它的客户端通信,不会暴露到网络上
- 性能不错:本地进程间通信,延迟很低
- 生命周期管理方便:客户端退出时,子进程自动终止
缺点
- 只能本地使用:Server必须跟客户端在同一台机器上
- 一对一限制:一个Server进程只能跟一个客户端通信,多个客户端需要启动多个Server实例
- 不适合长驻服务:每次客户端连接都要启动新进程,启动有开销
- 调试不方便:stdio的消息不像HTTP那样可以用抓包工具看
适用场景
- 本地开发和测试
- 桌面应用(比如Claude Desktop、Cursor)
- 工具跟Agent绑定部署、不需要远程访问的场景
我自己的体会是:开发阶段用stdio最省心,不用管网络和端口的问题。但一旦要部署到生产环境,尤其是多客户端共享一个Server的场景,stdio就不够用了。
三、HTTP + SSE传输:Web场景的首选
HTTP + SSE是MCP为Web场景设计的传输方式。它的原理是:
- 客户端通过HTTP POST发送JSON-RPC请求
- 客户端在初始化时建立一个SSE连接,用于接收Server的推送(比如通知、进度更新)
- Server对HTTP请求的响应通过SSE通道返回
这里可能有人会问:为什么不直接用HTTP请求响应?为什么还要SSE?
因为MCP支持Server主动推送消息(比如通知、进度更新),而HTTP是单向的——只能客户端问、Server答,Server不能主动找客户端。SSE正好弥补了这个不足,它让Server可以主动向客户端推送消息。
代码示例
服务端(用FastAPI):
# server_http.py
from mcp.server import Server
from mcp.types import Tool, TextContent
from mcp.server.fastmcp import FastMCP
import uvicorn
mcp = FastMCP("demo-server")
@mcp.tool()
def hello(name: str) -> str:
"""打招呼"""
return f"你好,{name}!"
if __name__ == "__main__":
uvicorn.run(mcp.app(), host="0.0.0.0", port=8000)
客户端:
from mcp import ClientSession
from mcp.client.sse import sse_client
async def main():
async with sse_client("http://localhost:8000/sse") as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("hello", {"name": "李四"})
print(result.content[0].text)
优点
- 支持远程访问:Server可以部署在任何地方,客户端通过URL连接
- 多客户端共享:一个Server可以同时服务多个客户端
- 适合Web应用:浏览器原生支持SSE,跟前端集成方便
- 可扩展性好:可以加负载均衡、限流、认证等中间件
缺点
- 配置稍复杂:需要启动HTTP服务、配置端口和路由
- SSE有一些限制:比如浏览器对同一域名的SSE连接数有限制(通常6个)
- 延迟比stdio高:毕竟走了网络栈
- 需要处理跨域问题:如果前端跟Server不同域,需要配置CORS
适用场景
- Web应用中的Agent工具接入
- Server需要远程部署的场景
- 多客户端共享同一个Server的场景
- 需要跟现有微服务架构集成的场景
我在项目里用HTTP+SSE的时候,最大的感受是"终于可以把Server部署到服务器上了"。以前用stdio的时候,每个Agent实例都要启动一堆Server子进程,资源浪费严重。换成HTTP之后,Server集中部署,Agent通过网络调用,资源利用率高多了。
四、WebSocket传输:实时双向通信
WebSocket是第三种传输方式,它提供全双工的通信通道——客户端和Server可以随时互相发消息,不需要请求响应的配对。
原理
WebSocket的原理大家应该比较熟悉了:
- 客户端通过HTTP握手升级到WebSocket连接
- 连接建立后,双方可以随时发送消息
- MCP的JSON-RPC消息直接通过WebSocket帧传递
代码示例
服务端:
# server_ws.py
from mcp.server import Server
from mcp.types import Tool, TextContent
import asyncio
import websockets
import json
server = Server("demo-server")
@server.list_tools()
async def list_tools():
return [Tool(name="hello", description="打招呼", inputSchema={"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]})]
@server.call_tool()
async def call_tool(name, arguments):
return [TextContent(type="text", text=f"你好,{arguments['name']}!")]
async def handle_connection(websocket):
async def read():
message = await websocket.recv()
return json.loads(message)
async def write(message):
await websocket.send(json.dumps(message))
await server.run(read, write, server.create_initialization_options())
async def main():
async with websockets.serve(handle_connection, "0.0.0.0", 8765):
await asyncio.Future()
if __name__ == "__main__":
asyncio.run(main())
优点
- 全双工通信:双方可以随时发消息,延迟最低
- 连接复用:一个连接上可以同时处理多个请求
- 适合实时场景:比如需要Server持续推送数据的场景
- 比HTTP+SSE更简洁:不需要分开维护HTTP请求和SSE连接
缺点
- 生态相对不成熟:MCP的WebSocket传输支持还在完善中
- 代理和防火墙问题:有些企业网络会拦截WebSocket连接
- 调试不如HTTP方便:没有像浏览器开发者工具那样直观的调试界面
- 需要自己处理连接管理:断线重连、心跳检测等都要自己实现
适用场景
- 需要低延迟双向通信的场景
- 实时数据推送(比如日志流、监控数据)
- 长连接场景(比如Agent跟Server需要持续交互)
说实话,WebSocket传输我用得不多,因为大部分场景下HTTP+SSE已经够用了。WebSocket更适合那种"Server需要持续不断地给客户端推数据"的场景,比如实时日志流、代码执行的实时输出等。
五、三种方式的对比
我做了一张对比表,把三种传输方式的关键维度放在一起:
| 维度 | stdio | HTTP + SSE | WebSocket |
|---|---|---|---|
| 通信范围 | 本地进程间 | 网络 | 网络 |
| 连接模式 | 一对一 | 多对一 | 多对一 |
| 双向通信 | 是(通过stdin/stdout) | 半双工(HTTP请求+SSE推送) | 全双工 |
| 延迟 | 极低 | 中 | 低 |
| 配置复杂度 | 低 | 中 | 中高 |
| 多客户端支持 | 不支持 | 支持 | 支持 |
| 远程部署 | 不支持 | 支持 | 支持 |
| 浏览器兼容 | 不支持 | 原生支持 | 原生支持 |
| 安全性 | 高(不暴露网络) | 需自行配置 | 需自行配置 |
| 适合阶段 | 开发/本地 | 生产/Web | 实时/长连接 |
六、我的选型建议
说了这么多,到底该选哪个?我根据自己的经验给一些建议:
如果你是在本地开发或者做Demo:选stdio。配置最简单,不用管网络,出问题也好排查。
如果你要做Web应用,Server需要远程部署:选HTTP + SSE。这是目前最成熟的方案,生态最好,跟现有架构集成也方便。
如果你需要Server持续推送实时数据:选WebSocket。比如代码执行的实时输出、日志流、监控数据等场景。
如果你不确定:先用stdio开发,部署的时候再考虑换成HTTP+SSE。MCP的传输层是可替换的,你的业务代码不用改,只需要换传输方式就行。
这里有一个很重要的点:MCP的传输层是抽象出来的,你的工具逻辑跟传输方式无关。 你写的Tool函数,不管用stdio还是HTTP还是WebSocket,都是一样的代码。这意味着你可以在开发阶段用stdio,生产环境用HTTP,不需要改业务逻辑。
我自己的项目就是这么做的:开发的时候所有Server都用stdio跑,调试方便;部署到服务器上的时候换成HTTP+SSE,加个Nginx做反向代理和负载均衡。工具代码一行都没改,只改了启动方式。
七、踩坑记录
最后说几个我在切换传输方式时踩的坑:
坑1:HTTP模式下的CORS问题
把Server从stdio换成HTTP之后,前端调用一直报CORS错误。原因是FastMCP默认没有开启CORS,需要手动配置:
from fastapi.middleware.cors import CORSMiddleware
app = mcp.app()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 生产环境要限制域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
坑2:SSE连接被代理服务器截断
部署在Nginx后面的时候,SSE连接经常断开。查了半天才发现是Nginx的缓冲设置问题,需要加这些配置:
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
proxy_read_timeout 86400s;
坑3:stdio模式下Server的日志输出会干扰通信
stdio模式下,Server的stdout是用来传MCP消息的。如果你在Server里用print输出日志,日志会混进stdout里,导致客户端解析失败。解决办法是把日志输出到stderr,或者写到文件里:
import logging
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
这个坑我踩过两次,每次都调试了半天才发现是print搞的鬼。
写在最后
MCP的传输方式设计得很灵活,三种方式各有适用场景。理解它们的区别,能帮你在不同阶段选择最合适的方案。
我的体会是:不要一开始就纠结选哪种传输方式,先把工具逻辑写好,跑通stdio版本。等需要部署、需要多客户端共享的时候,再换成HTTP+SSE也不迟。MCP的传输层抽象让这种切换成本很低。
当然,传输方式只是MCP的一部分。下一篇我们会聊更实际的话题——怎么在智能体中真正用好MCP工具,包括工具选择策略、调用错误处理、多工具协作等。
你们在项目里用的是哪种传输方式?有没有遇到什么奇怪的问题?欢迎在评论区聊聊。
下一篇预告:在智能体中使用MCP工具。我们会把MCP跟Agent真正结合起来,看看大模型是怎么选择工具、调用工具、处理结果的,以及一些让工具调用更稳定的技巧。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2401_89160889/article/details/167495587




