
1. 从一次断链说起OpenAI Agent 调用 MCP Server 到底卡在哪如果你正在用 OpenAI Agent 接 MCP Server大概率遇到过这种场景Agent 明明启动了日志里也打印了Running input但工具就是不被调用或者调用到一半直接卡死最后抛一个local proxy failed或者reading choices之类的报错。你盯着代码看半天Agent 侧没问题MCP Server 侧也没问题问题就出在中间那条 SSE 链路上。这篇内容聚焦的就是这条链路OpenAI Agent 通过 SSE 连接 MCP Server 的端到端调用过程。我会把整条链路拆成四个阶段——握手、工具发现、参数回传、结果渲染每个阶段给出可复制的配置片段和抓包验证方法。适合已经跑通过 stdio 方式、想进一步把 MCP Server 搬到远程 SSE 场景的开发者也适合正在排查 Agent 调用 MCP 时链路中断点的同学。先说清楚 SSE 和 stdio 的本质差异。stdio 是本地进程通信Agent 直接拉起 MCP Server 子进程通过标准输入输出收发 JSON-RPC 消息延迟低、无网络依赖适合本地调试。SSE 则是基于 HTTP 的单向事件流Agent 作为客户端连到远程 MCP Server 的/sse端点服务端通过这条长连接主动推送事件。它支持断线重连兼容 HTTP 生态适合云端服务与客户端分离的部署形态。两者都遵循 JSON-RPC 2.0 消息格式请求、响应、通知三种消息类型一致所以业务代码层面差异不大真正的坑都在传输层。我试过把同一个 Weather Server 从 stdio 切到 SSE代码几乎没改但调试成本翻了好几倍。原因很简单stdio 下你能直接在终端看到子进程的输出SSE 下所有信息都藏在 HTTP 事件流里不抓包基本靠猜。所以下面每个阶段我都会配上验证手段让你能定位到具体是哪一步断了。在动手之前你需要准备一个可用的模型接入端点。Agent 侧要调用模型来决策是否触发工具这一步需要一个兼容 OpenAI 接口的 Base URL 和 API Key。我这边用的是 TaoToken 的接入方式它的 API 地址是https://taotoken.net/api兼容 OpenAI 的 Chat Completions 格式Agent 侧只要把base_url指过去就能用。模型对话调试可以在 模型对话 页面先验证连通性确认模型能正常返回再往下走避免把模型问题和链路问题混在一起排查。2. 前置准备TaoToken 接入与 MCP Server 环境搭建这一节把两边的环境都准备好MCP Server 侧跑起来一个 SSE 服务Agent 侧配好模型接入。很多人卡在第一步不是因为代码难而是依赖版本和端点路径对不上。先装依赖。MCP 的 Python SDK 和 OpenAI Agents SDK 都要装注意用虚拟环境隔离python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install mcp openai-agents requestsmcp包提供FastMCP类openai-agents提供Agent、Runner、MCPServerSse这些核心类。版本上建议mcp1.2.0早期版本对 SSE 传输的支持不完整容易出现握手后立即断开的情况。接着写 MCP Server。这里用 wttr.in 这个开源天气服务做数据源它支持终端、HTML、PNG 多种输出格式直接 GET 就能拿到文本天气非常适合做演示import requests from mcp.server.fastmcp import FastMCP mcp FastMCP(Weather Server) mcp.tool() def get_current_weather(city: str) - str: print(f[debug-server] get_current_weather({city})) endpoint https://wttr.in response requests.get(f{endpoint}/{city}, timeout10) return response.text if __name__ __main__: mcp.run(transportsse)注意mcp.run(transportsse)这一行它决定了服务端以 SSE 模式启动默认监听8000端口SSE 端点是/sse。启动后你会看到类似Uvicorn running on http://0.0.0.0:8000的输出。这里有个细节FastMCP的 SSE 实现底层用的是 ASGI如果你在容器里跑记得把端口映射出来否则 Agent 侧连不上。Agent 侧的模型接入配置。TaoToken 的 API 地址是https://taotoken.net/api在 Agent 里通过AsyncOpenAI指定base_url和api_keyfrom openai import AsyncOpenAI external_client AsyncOpenAI( api_key你的 TaoToken API Key, base_urlhttps://taotoken.net/api, )API Key 在 API Keys 页面生成生成后先别急着写进 Agent用一条 curl 验证一下模型端点是否通curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer 你的Key \ -H Content-Type: application/json \ -d {model:gpt-4o,messages:[{role:user,content:ping}]}返回里有choices字段就说明模型侧没问题。这一步很关键因为后面 Agent 调用 MCP 时模型要先决策“要不要调工具”如果模型端点本身不通你会看到 Agent 直接报错误以为是 MCP 链路的问题。完整的接入参数和端点说明可以参考 接入文档里面有 Base URL、鉴权方式和模型 ID 的对照表。环境准备好之后两个服务分别跑在两个终端一个跑 MCP Serverpython weather_server.py一个准备跑 Agent。接下来进入链路拆解。3. 可复制配置SSE 握手、工具发现与 Agent 登记这一节给出完整的 Agent 侧代码并逐段解释握手和工具发现阶段发生了什么。先看完整可运行的版本import asyncio from agents import Agent, Runner, AsyncOpenAI, OpenAIChatCompletionsModel from agents.mcp import MCPServer from agents.mcp.server import MCPServerSse async def run(mcp_server: MCPServer): external_client AsyncOpenAI( api_key你的 TaoToken API Key, base_urlhttps://taotoken.net/api, ) agent Agent( nameAssistant, instructionsUse the tools to answer the questions., mcp_servers[mcp_server], modelOpenAIChatCompletionsModel( modelgpt-4o, openai_clientexternal_client, ), ) message 泉州今天的天气怎么样 print(fRunning input: {message}) result await Runner.run(starting_agentagent, inputmessage) print(result.final_output) async def main(): async with MCPServerSse( nameSSE Python Server, params{ url: http://localhost:8000/sse, }, ) as server: await run(server) if __name__ __main__: asyncio.run(main())这段代码里MCPServerSse的params字典是关键配置。url指向 MCP Server 的 SSE 端点默认是http://localhost:8000/sse。如果你把 Server 部署在远程换成对应域名即可但要注意协议必须是http或https路径必须是/sse少一个字符都会握手失败。握手阶段发生在async with MCPServerSse(...)进入上下文管理器的那一刻。Agent 会向/sse发起一个 GET 请求服务端返回Content-Type: text/event-stream然后推送第一个事件里面包含一个session_id和后续消息的 POST 端点。这个 POST 端点通常是/messages/?session_idxxxAgent 后续所有 JSON-RPC 请求都往这里发。握手成功的标志是 Agent 侧不报错且 Server 日志里出现连接建立记录。工具发现紧接着握手。Agent 通过 POST 端点发送tools/list请求Server 返回工具清单包含工具名、描述、参数 schema。对于我们的 Weather Server返回的就是get_current_weather及其参数city。这一步如果断了你会看到 Agent 报“no tools available”或者工具列表为空。验证方法是手动发一次tools/listcurl -X POST http://localhost:8000/messages/?session_id你的session_id \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:tools/list,params:{}}返回里能看到get_current_weather就说明工具发现正常。session_id从握手时的事件流里拿抓包方法下一节讲。Agent 登记阶段是把 MCP Server 挂到 Agent 实例上。mcp_servers[mcp_server]这一行让 Agent 知道有哪些工具可用但真正触发调用是在Runner.run执行时。模型收到用户问题后会判断是否需要调工具如果需要就生成一个工具调用请求Agent 框架把它转成 JSON-RPC 的tools/call发给 Server。这里有个容易忽略的点模型 ID 必须支持 function calling。gpt-4o是支持的但如果你换成某些不支持工具调用的模型Agent 会直接把问题当普通对话回答不会触发 MCP 调用。所以模型选择上要确认它具备工具调用能力。如果你在 Coding Plan 里配置了长期编码用的模型也可以直接复用同一套 Base URL 和 Key只是模型 ID 换成对应的编码模型即可。配置写完后先别急着跑完整流程。建议分两步验证第一步只跑握手和工具发现把Runner.run换成手动发tools/list确认链路通第二步再跑完整 Agent 调用。这样出问题时能快速定位是传输层还是业务层。4. 验证请求抓包看 SSE 事件流与工具回调结果这一节是整篇的核心怎么确认链路真的通了以及每个阶段的事件长什么样。SSE 的调试难点在于它是长连接事件流普通 curl 只能看到连接建立看不到后续推送。有两个办法一是用curl -N保持连接不缓冲二是用抓包工具看完整事件流。先用curl -N看握手事件curl -N http://localhost:8000/sse-N关闭缓冲你会看到类似这样的输出event: endpoint data: /messages/?session_idabc123def456这就是握手事件session_id是abc123def456后续所有 POST 请求都要带上它。拿到 session_id 后另开一个终端发tools/listcurl -X POST http://localhost:8000/messages/?session_idabc123def456 \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:tools/list,params:{}}同时回到curl -N那个终端你会看到服务端推送回来的响应事件event: message data: {jsonrpc:2.0,id:1,result:{tools:[{name:get_current_weather,description:...,inputSchema:{type:object,properties:{city:{type:string}},required:[city]}}]}}这就是工具发现的完整事件。注意event: message和data:两行SSE 协议里每个事件由这两部分组成data里是 JSON-RPC 响应体。接着验证工具调用。手动发一个tools/callcurl -X POST http://localhost:8000/messages/?session_idabc123def456 \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:2,method:tools/call,params:{name:get_current_weather,arguments:{city:泉州}}}curl -N终端会推送回工具执行结果data里包含content数组里面是天气文本。同时 MCP Server 的终端会打印[debug-server] get_current_weather(泉州)说明工具真的被执行了。现在跑完整的 Agent 流程观察端到端结果Running input: 泉州今天的天气怎么样 今天泉州的天气是局部多云气温大约在28℃风速为 10 km/h能见度为 10 km。预计全天无降水。这条输出背后发生了四次关键交互Agent 握手拿到 session_id、Agent 发tools/list发现工具、模型决策触发tools/call、Server 返回结果后模型渲染成自然语言。任何一步断了最终输出都会异常。如果你想看 Agent 侧发出的原始请求可以在AsyncOpenAI初始化时加http_client日志或者用 mitmproxy 这类工具抓本地 HTTP 流量。重点看两个请求一个是发往https://taotoken.net/api/v1/chat/completions的模型请求里面tools字段是否包含get_current_weather另一个是发往localhost:8000/messages/的 JSON-RPC 请求method是否为tools/call。这两个请求都正常链路就是通的。结果渲染阶段是模型把工具返回的原始文本转成自然语言。这一步如果出问题通常表现为工具被调用了但最终输出是空的或者报错。检查点是模型请求的messages里是否包含了工具返回的tool角色消息。如果 Agent 框架没把工具结果回传给模型模型就没法渲染最终输出会是半截的。5. 常见报错排查401、local proxy failed 与 reading choices这一节对照真实报错给出定位路径。这些错误我基本都踩过按出现频率排序。401 Unauthorized。这个最直接模型端点鉴权失败。检查api_key是否填对以及base_url是否指向https://taotoken.net/api。注意有些库会自动在 base_url 后面拼/v1如果你的 base_url 已经带了/v1就会变成/v1/v1导致 401 或 404。TaoToken 的接入地址是https://taotoken.net/apiOpenAI SDK 会自动补/v1所以不要手动加。验证方法就是前面那条 curl能返回choices就说明 Key 和地址都对。local proxy failed。这个报错通常出现在 Agent 尝试连接 MCP Server 时。原因有几个MCP Server 没启动、端口不对、或者/sse路径写错。先确认 Server 在跑curl -N http://localhost:8000/sse能拿到event: endpoint。如果 Server 在容器里确认端口映射正确Agent 侧用localhost还是容器名要对应。还有一种情况是防火墙拦了长连接SSE 是长连接某些网络环境会主动断开空闲连接表现为握手成功后几秒就断。可以在 Server 侧加心跳事件或者检查网络策略。reading choices 报错。这个错误来自模型响应解析阶段通常是模型返回的 JSON 结构不符合预期。常见原因是模型端点返回了非标准格式或者返回了错误信息但 HTTP 状态码是 200。排查方法是把模型请求的原始响应打出来看确认choices[0].message存在。如果模型不支持 function calling返回的message里不会有tool_calls字段Agent 框架解析时就会报这个错。换一个支持工具调用的模型 ID 即可。OAuth 相关报错。如果你接的 MCP Server 需要 OAuth 鉴权Agent 侧要配置对应的 token。SSE 模式下 OAuth 通常通过请求头传递在MCPServerSse的params里加headers字段params{ url: http://localhost:8000/sse, headers: {Authorization: Bearer 你的token}, }如果 Server 侧没配 OAuth 但 Agent 发了鉴权头一般不影响反过来 Server 要求鉴权但 Agent 没发就会返回 401 或 403。工具被调用但结果为空。检查 MCP Server 的工具函数是否真的返回了内容。在函数里加print是最简单的办法看 Server 终端有没有打印。如果打印了但 Agent 侧没收到就是 SSE 推送环节的问题回到抓包步骤看event: message有没有推回来。排查顺序建议从外到内先确认模型端点通curl 验证再确认 MCP Server 通curl -N 验证最后跑 Agent。这样能把问题范围快速缩小到某一层而不是在整条链路上瞎猜。6. 把链路跑稳从调试到长期使用的几个实践链路跑通只是第一步真正用起来还要考虑稳定性。SSE 是长连接网络抖动、服务重启都会导致断连。Agent 框架内置了重连机制但重连后 session_id 会变如果 Agent 侧没处理好工具调用会失败。建议在 MCP Server 侧加日志记录每次连接建立和断开方便观察重连频率。工具描述要写清楚。模型是根据工具名和描述来决定是否调用的描述太模糊会导致模型该调的时候不调。比如get_current_weather的描述里明确写“获取指定城市的当前天气”参数city说明“城市名称如泉州”模型判断起来就准得多。模型选择上工具调用能力比模型大小更重要。有些小模型也支持 function calling响应更快、成本更低适合高频工具调用场景。你可以在 模型对话 里对比几个模型的实际调用表现选一个触发准确率高的。如果你要把这套链路用到长期运行的 Agent 上建议把 MCP Server 和 Agent 分开部署Server 侧做好健康检查和自动重启。Agent 侧的 Key 和 Base URL 统一从环境变量读取不要硬编码在代码里。TaoToken 的接入方式在 接入文档 里有完整说明包括不同语言的 SDK 示例和端点对照配环境变量的时候可以直接参考。最后留一个实用技巧在 Agent 侧把每次工具调用的请求和响应都记到日志里包括工具名、参数、返回内容、耗时。这样出问题时不用重新抓包翻日志就能定位。日志里同时记录模型请求的tools字段确认工具清单有没有正确传给模型。这套日志跑一段时间后你会对链路的健康状态有直观感受哪一步慢、哪一步容易断一目了然。