☰
OpenAI Agent调用MCP Server案例分析:从SSE握手到工具回调的完整链路
2026/10/1 6:51:28 网站建设 项目流程

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 requests

mcp包提供FastMCP类,openai-agents提供Agent、Runner、MCPServerSse这些核心类。版本上建议mcp>=1.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}", timeout=10) return response.text if __name__ == "__main__": mcp.run(transport="sse")

注意mcp.run(transport="sse")这一行,它决定了服务端以 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_key:

from openai import AsyncOpenAI external_client = AsyncOpenAI( api_key="你的 TaoToken API Key", base_url="https://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 Server(python 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_url="https://taotoken.net/api", ) agent = Agent( name="Assistant", instructions="Use the tools to answer the questions.", mcp_servers=[mcp_server], model=OpenAIChatCompletionsModel( model="gpt-4o", openai_client=external_client, ), ) message = "泉州今天的天气怎么样?" print(f"Running input: {message}") result = await Runner.run(starting_agent=agent, input=message) print(result.final_output) async def main(): async with MCPServerSse( name="SSE 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_id=xxx,Agent 后续所有 JSON-RPC 请求都往这里发。握手成功的标志是 Agent 侧不报错,且 Server 日志里出现连接建立记录。

工具发现紧接着握手。Agent 通过 POST 端点发送tools/list请求,Server 返回工具清单,包含工具名、描述、参数 schema。对于我们的 Weather Server,返回的就是get_current_weather及其参数city。这一步如果断了,你会看到 Agent 报“no tools available”或者工具列表为空。验证方法是手动发一次tools/list:

curl -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_id=abc123def456

这就是握手事件,session_id是abc123def456,后续所有 POST 请求都要带上它。拿到 session_id 后,另开一个终端发tools/list:

curl -X POST "http://localhost:8000/messages/?session_id=abc123def456" \ -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/call:

curl -X POST "http://localhost:8000/messages/?session_id=abc123def456" \ -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/api,OpenAI 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字段,确认工具清单有没有正确传给模型。这套日志跑一段时间后,你会对链路的健康状态有直观感受,哪一步慢、哪一步容易断,一目了然。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询