
1. 为什么一个 MCP 会冒出三种传输协议第一次打开 MCP 配置页面的人大概率会被 stdio、SSE、Streamable HTTP 这几个词同时砸中。更让人头大的是网上有的文章说“SSE 已经废弃”有的又说“SSE 还在用”看完反而更糊涂。我试过把三种配置混着抄结果客户端一直卡在初始化日志里只有一行local proxy failed排查了半天才发现是把旧版双端点方案和新版单端点方案搞混了。先把结论摆出来省得你走弯路。MCP 全称 Model Context Protocol是 Anthropic 推出的开放标准用来定义 AI 模型和外部工具、数据源之间怎么通信。它本身是传输无关的也就是说协议层只规定消息长什么样具体怎么把消息送出去交给传输层决定。目前官方认可的传输方式就两种stdio 和 Streamable HTTP。SSE 不是被废弃了被废弃的是旧版“HTTPSSE 双端点”那套传输方案在新版 Streamable HTTP 里SSE 仍然可以作为流式响应的载体继续用。所以选型逻辑其实很清晰本地单机跑工具用 stdio简单稳定延迟低远程服务化、要给多个客户端共享用 Streamable HTTP单端点设计对网关友好只有在做历史系统迁移、兼容老客户端时才需要了解旧版 HTTPSSE 的工作方式。这篇文章会按“概念厘清 → 三种传输逐一拆解 → 横向对比 → 可复制配置 → 验证请求 → 报错排查”的顺序走每一步都给能直接粘贴的片段。同时我会用 TaoToken 的统一 Key 通道来演示多协议接入这样你不管切到哪种传输鉴权部分都不用反复改。2. 先厘清概念运行器和传输层不是一回事很多人混乱的根源是把“这个服务怎么跑起来”和“跑起来以后双方怎么通信”混成了一层。这两件事本来就不在一个维度上。你看到的词本质上分两类。uv、npm、npx 这些是运行器或包管理工具解决的是“MCP Server 怎么启动、依赖怎么装”。stdio、SSE、Streamable HTTP 解决的是“进程跑起来之后客户端和服务器之间怎么传消息”。把 npx 和 stdio 并列去比较就像把“怎么把车打着火”和“走哪条路”放一起比没有意义。MCP 的消息编码统一用 JSON-RPC 2.0所有消息必须 UTF-8。一个典型的工具调用请求长这样{ jsonrpc: 2.0, method: tools/call, id: 1, params: { name: read_file, arguments: { path: /src/main.py } } }响应则是{ jsonrpc: 2.0, id: 1, result: { content: [{ type: text, text: print(Hello World) }] } }传输层的职责就是把这些 JSON-RPC 消息从一端搬到另一端。协议层不关心你是用管道搬还是用 HTTP 搬只要消息格式对就行。这个解耦设计是理解后面所有差异的基础。版本演进也得记一下不然看文档容易串版本。2024-11-05 是第一个正式规范定义了 stdio 和 HTTPSSE 两种机制其中 HTTPSSE 需要两个独立端点。2025-03-26 通过 PR #206 引入 Streamable HTTP替代旧版双端点方案改成单一 MCP 端点。到 2025-11-25Streamable HTTP 成为当前主方案旧 HTTPSSE 进入兼容语义。官方文档现在明确写的是当前标准传输机制是 stdio 和 Streamable HTTP。注意如果你在文档里看到 “SSE transport”先确认它指的是哪个版本的规范。旧版双端点方案已被替代但 SSE 技术本身在 Streamable HTTP 里仍然活跃。3. stdio 传输本地进程间通信的默认选择stdio 是 MCP 最基础的通信方式靠进程间通信实现。工作机制很简洁客户端把 MCP Server 作为子进程启动Server 从 stdin 读 JSON-RPC 消息通过 stdout 返回响应日志走 stderr。类比一下就是两个人面对面说话一个说一个听没有中间环节直接即时。核心规则必须记牢这几条踩错任何一条都会导致解析失败。消息通过换行符\n分隔消息内容里禁止包含嵌入式换行。服务器不得向 stdout 写入任何非有效 MCP 消息的内容。客户端不得向服务器的 stdin 写入任何非有效 MCP 消息的内容。服务器可以把 UTF-8 字符串写到 stderr 做日志客户端可以捕获、转发或忽略。最常见的坑就是 Server 把日志打到了 stdout。一旦 stdout 里混进一行INFO: server started客户端解析 JSON 就会失败报错通常是Unexpected token或者直接卡住。日志务必走 stderr这是 stdio 模式的头号纪律。Claude Desktop 配置 stdio 类型的 MCP Server是最典型的场景{ mcpServers: { filesystem: { command: npx, args: [-y, modelcontextprotocol/server-filesystem, /path/to/allowed/files] }, sqlite: { command: uvx, args: [mcp-server-sqlite, --db-path, /tmp/test.db] } } }如果你想自己写一个最简 stdio MCP ServerPython 版本大概是这样import sys import json def handle_request(request): method request.get(method, ) req_id request.get(id) if method tools/list: return { jsonrpc: 2.0, id: req_id, result: { tools: [{name: echo, description: Echo back the input}] } } elif method tools/call: return { jsonrpc: 2.0, id: req_id, result: { content: [{type: text, text: Echo: str(request.get(params, {}))}] } } return {jsonrpc: 2.0, id: req_id, error: {code: -32601, message: Method not found}} def main(): for line in sys.stdin: line line.strip() if not line: continue try: request json.loads(line) response handle_request(request) sys.stdout.write(json.dumps(response) \n) sys.stdout.flush() except json.JSONDecodeError: sys.stderr.write(fInvalid JSON: {line}\n) sys.stderr.flush() if __name__ __main__: main()这里sys.stdout.flush()是必须的。少了它响应会滞留在缓冲区客户端一直等不到结果表现就是“请求发出去了但没反应”。stdio 最适合本地工具型 Server比如文件系统、Git、Shell 命令也适合开发调试阶段终端直接交互所见即所得。桌面客户端如 Claude Desktop、Cursor、VS Code 插件基本都用它。局限也很明显天然偏单机本地进程不能远程多客户端共享进程生命周期由客户端托管客户端一退出 Server 就没了也没法天然横向扩展。4. Streamable HTTP远程生产环境的当前推荐Streamable HTTP 是官方从 2025-03-26 起推荐的远程传输方案彻底改了旧版双端点架构。核心特征有三个使用单一 MCP 端点比如/mcp该端点必须支持 POST可选支持 GETPOST 发送 JSON-RPC 请求响应可以是application/json普通返回也可以是text/event-stream流式返回服务器作为独立进程运行能处理多个客户端连接。请求要求也得注意。客户端 POST 必须包含 Accept 头列出application/json和text/event-stream。Body 是单个 JSON-RPC 请求、通知或响应。如果输入是带 id 的请求服务器必须返回 JSON 或 SSE 流如果输入是无 id 的响应或通知服务器接受则返回 202 Accepted 无响应体。会话管理靠 HTTP 头。Mcp-Session-Id在初始化后由服务端分配用来标识会话级状态Mcp-Protocol-Version由客户端携带协商版本。标准交互流程是这样的# 1. 初始化 POST /mcp → {method: initialize, id: 1, ...} ← Mcp-Session-Id: abc123 ← {result: {protocolVersion: 2025-11-25, ...}} # 2. 通知初始化完成 POST /mcp Mcp-Session-Id: abc123 → {method: notifications/initialized} # 3. 列出工具 POST /mcp Mcp-Session-Id: abc123 → {method: tools/list, id: 2} ← {result: {tools: [...]}} # 4. 调用工具 POST /mcp Mcp-Session-Id: abc123 → {method: tools/call, id: 3, params: {...}} ← {result: {...}}如果携带的 Session ID 失效服务端返回 404客户端应重新执行 initialize。这个机制比旧版清晰很多。用 curl 测试 Streamable HTTP 端点初始化请求长这样curl -X POST http://localhost:3001/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -d { jsonrpc: 2.0, method: initialize, params: { protocolVersion: 2025-11-25, capabilities: {}, clientInfo: {name: test-client, version: 1.0.0} }, id: 1 }拿到 Session ID 后调用工具curl -X POST http://localhost:3001/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -H Mcp-Session-Id: abc123 \ -d { jsonrpc: 2.0, method: tools/call, params: {name: mockfunc, arguments: {desc: 测试参数}}, id: 2 }Python 服务端用 FastAPI 实现能同时支持 JSON 和 SSE 两种响应from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse, JSONResponse import json import asyncio app FastAPI() app.post(/mcp) async def mcp_endpoint(request: Request): body await request.json() method body.get(method, ) req_id body.get(id) accept request.headers.get(accept, ) if method initialize: return JSONResponse({ jsonrpc: 2.0, id: req_id, result: { protocolVersion: 2025-11-25, capabilities: {tools: {}}, serverInfo: {name: demo-server, version: 1.0.0} } }, headers{Mcp-Session-Id: demo-session-123}) if method tools/call: if text/event-stream in accept: async def generate(): for token in [Hello, , World, !]: yield fdata: {json.dumps({token: token})}\n\n await asyncio.sleep(0.1) return StreamingResponse(generate(), media_typetext/event-stream) return JSONResponse({ jsonrpc: 2.0, id: req_id, result: {content: [{type: text, text: Hello World!}]} }) return JSONResponse({ jsonrpc: 2.0, id: req_id, error: {code: -32601, message: Method not found} })这里有个容易混淆的点必须说清楚。SSE 有两种语境旧传输 HTTPSSE 是一套独立的双端点协议模型已被替代仅向后兼容新传输里的 SSE 流是 Streamable HTTP 框架下的流式响应方式活跃使用中。SSE 技术本身没被废弃废弃的是“HTTPSSE 作为独立传输协议”这个方案。5. 用 TaoToken 统一 Key 通道接入多协议前面讲的都是传输层本身但实际接入时还有个绕不开的问题鉴权。stdio 靠环境变量传 KeyStreamable HTTP 靠 Authorization 头传 Key如果每种传输都单独配一套切换时很容易漏改。TaoToken 的统一 Key 通道就是来解决这个的一个 Key 覆盖多种接入方式切传输时只改传输配置鉴权部分不动。TaoToken 的 API 地址是https://taotoken.net/api官网在https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。它提供统一的模型对话、Coding Plan、控制台和 API Keys 管理入口MCP 客户端接入时把 Base URL 指向它就行。先看 stdio 模式下怎么配。stdio 的 Key 通过 env 字段传给子进程配置片段如下{ mcpServers: { taotoken-bridge: { command: npx, args: [-y, your-scope/mcp-bridge], env: { TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_MODEL: claude-sonnet-4-5 } } } }三件套要写全Base URL 是https://taotoken.net/apiKey 从控制台的 API Keys 页面拿Model ID 按你实际用的填。少任何一个客户端初始化时都会报鉴权失败。再看 Streamable HTTP 模式。远程接入时 Key 走 Authorization 头配置片段{ mcpServers: { taotoken-remote: { url: https://taotoken.net/api/mcp, transport: streamable-http, headers: { Authorization: Bearer sk-你的Key, Accept: application/json, text/event-stream } } } }如果你用的是 Cline 或 Claude Code 这类工具配置位置不太一样。Cline 的 MCP 配置在cline_mcp_settings.jsonClaude Code 走~/.claude/settings.json或项目级.mcp.json。Codex 的话看auth.json里面填 Base URL 和 Key。不管哪个工具三件套都是 Base URL、Key、Model ID缺一不可。提示Key 不要硬编码进会提交到 Git 的文件。stdio 模式用 env 传HTTP 模式用环境变量注入 Authorization 头配置文件权限设成 600。TaoToken 的模型对话入口可以用来快速验证 Key 是否有效接入文档里有各客户端的详细配置示例。如果你要长期跑编码或 Agent 任务Coding Plan 会更划算但那是后话先把传输层跑通再说。6. 验证请求从初始化到工具调用的完整链路配置写完不代表能跑通得一步步验证。我习惯按“初始化 → 列工具 → 调工具”三段来测每段都有明确的成功标志。第一段初始化握手。用 curl 打 Streamable HTTP 端点curl -i -X POST https://taotoken.net/api/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -H Authorization: Bearer sk-你的Key \ -d { jsonrpc: 2.0, method: initialize, params: { protocolVersion: 2025-11-25, capabilities: {}, clientInfo: {name: verify-client, version: 1.0.0} }, id: 1 }成功标志是 HTTP 200响应头里有Mcp-Session-Id响应体里result.protocolVersion是2025-11-25。如果返回 401说明 Key 或 Authorization 头有问题如果返回 404说明端点路径不对。第二段列出工具。带上刚才拿到的 Session IDcurl -X POST https://taotoken.net/api/mcp \ -H Content-Type: application/json \ -H Accept: application/json \ -H Authorization: Bearer sk-你的Key \ -H Mcp-Session-Id: 你的SessionID \ -d {jsonrpc: 2.0, method: tools/list, id: 2}成功标志是result.tools是个非空数组每个工具都有 name 和 description。如果 tools 是空的说明服务端没注册工具不是传输层的问题。第三段调用工具。挑一个工具实际调一次curl -X POST https://taotoken.net/api/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -H Authorization: Bearer sk-你的Key \ -H Mcp-Session-Id: 你的SessionID \ -d { jsonrpc: 2.0, method: tools/call, params: {name: 你的工具名, arguments: {}}, id: 3 }成功标志是result.content里有实际返回。如果 Accept 里带了text/event-stream响应会变成 SSE 流你会看到一行行data: {...}。stdio 模式的验证更简单直接用 MCP Inspectornpx modelcontextprotocol/inspector node path/to/your-server.jsInspector 会自动把 Server 作为子进程启动通过 stdin/stdout 通信在 Web UI 里展示所有 Tools、Resources 和 Prompts。如果 Inspector 里能看到工具列表说明 stdio 链路通了。自动化测试的话可以写个脚本把三段串起来import requests BASE_URL https://taotoken.net/api/mcp HEADERS { Authorization: Bearer sk-你的Key, Accept: application/json, text/event-stream } def test_initialize(): resp requests.post(BASE_URL, json{ jsonrpc: 2.0, method: initialize, params: { protocolVersion: 2025-11-25, capabilities: {}, clientInfo: {name: test, version: 1.0} }, id: 1 }, headersHEADERS) assert resp.status_code 200 assert Mcp-Session-Id in resp.headers return resp.headers[Mcp-Session-Id] def test_tools_list(session_id): resp requests.post(BASE_URL, json{ jsonrpc: 2.0, method: tools/list, id: 2 }, headers{**HEADERS, Mcp-Session-Id: session_id}) assert resp.status_code 200 return resp.json()[result][tools] if __name__ __main__: sid test_initialize() print(f初始化成功Session ID: {sid}) tools test_tools_list(sid) print(f发现 {len(tools)} 个工具: {[t[name] for t in tools]})跑通这个脚本说明传输层和鉴权都没问题剩下的就是业务逻辑了。7. 常见报错排查401、local proxy failed 与 reading choices实际接入时踩的坑基本集中在几个固定报错上。我把最常见的几个列出来对照着查能省不少时间。401 Unauthorized。这个最直接Key 不对或没传。先确认 Authorization 头格式是Bearer sk-xxx中间有空格。再确认 Key 没过期去控制台的 API Keys 页面看一眼。stdio 模式下如果报 401检查 env 里的TAOTOKEN_API_KEY有没有被引号包错或者子进程有没有继承到环境变量。local proxy failed。这个报错通常出现在客户端配置了远程 MCP 但本地代理没起来的情况。常见原因是 Base URL 写成了https://taotoken.net少了/api或者 transport 字段写成了sse但服务端只支持streamable-http。检查配置里的 url 和 transport 是否匹配url 必须是完整的https://taotoken.net/api/mcp。reading choices 相关报错。这个一般出现在流式响应解析阶段客户端读 SSE 流时字段对不上。检查 Accept 头有没有同时包含application/json和text/event-stream少了任何一个服务端可能返回的格式和客户端预期不一致。另外确认Mcp-Session-Id在每次请求都带上了会话丢了也会导致解析异常。OAuth 相关报错。如果你用的是需要 OAuth 的远程服务报错可能是invalid_grant或token expired。这类问题不在传输层在认证层重新走一遍授权流程即可。TaoToken 的 Key 通道不走 OAuth用 Bearer Token 就行所以配 TaoToken 时不会遇到这类报错。stdio 模式下的 JSON 解析失败。报错通常是Unexpected token或JSONDecodeError。九成是 Server 把日志打到了 stdout。检查你的 Server 代码所有print()都要改成sys.stderr.write()。另外确认每条消息以\n结尾且消息内部没有裸换行。连接超时。Streamable HTTP 模式下如果请求一直挂着检查反向代理有没有开proxy_buffering off。Nginx 默认会缓冲 SSE 流导致数据卡在代理层不往下发。配置里加上proxy_buffering off;和proxy_read_timeout 3600s;就能解决。排查时有个通用思路先确认传输层通不通curl 能不能拿到 200再确认鉴权过不过401 还是 200最后确认业务逻辑对不对tools 列表和调用结果。按这个顺序查基本不会绕远路。8. 三种传输横向对比与选型建议把三种传输放一起对比差异一目了然。对比维度stdioHTTPSSE旧版Streamable HTTP推荐端点数量无管道通信2 个1 个如 /mcp连接模型父子进程双连接按需连接多客户端支持否需维护多 SSE 连接无状态/有状态均可流式响应否原生 SSE按需 SSE网关友好度N/A双端点需特殊配置单端点标准 HTTP远程访问否是是规范状态活跃标准已废弃仅兼容当前推荐选型决策其实就一句话需要远程访问吗不需要就用 stdio本地工具、IDE 插件、调试开发都选它。需要远程就用 Streamable HTTP简单请求响应走 POST 返回 JSON流式输出需求走 POST 返回 text/event-stream。只有在兼容老客户端时才考虑 HTTPSSE而且只作过渡。部署场景对应关系也整理一下。本地开发用 stdio配置最简单。小团队内部服务用 Streamable HTTP 加反向代理记得关 buffering。Serverless 用 Streamable HTTP 纯 JSON 模式别用 SSE 长连接。大规模集群用 Streamable HTTP 加 Redis 会话存储配合负载均衡。历史系统兼容才用旧版 HTTPSSE。最后提醒几个容易忽略的点。stdio 模式下永远不要把日志打到 stdout这是头号陷阱。Streamable HTTP 一定要验证 Origin 头防 DNS 重绑定攻击。上线前用 curl 或 MCP Inspector 把每个端点都测一遍。如果你在维护工具库考虑同时提供 stdio 和 Streamable HTTP 两种接入方式让使用者自己选。传输层这东西平时不出问题感觉不到它的存在一旦出问题就是“消息没发出去”还是“发出去没收到回应”的区别。搞清楚这三种传输的原理和差异排查 MCP 通信问题时能少走很多弯路。