1. 握手成功却拿不到工具结果,问题往往不在 get_weather
你大概率遇到过这种场景:终端里 MCP 的 initialize 握手日志一切正常,list_tools 也返回了 get_weather 的描述,DeepSeek 那边甚至已经吐出了 tool_calls,可最后模型就是收不到天气数据,回复停在“正在为你查询”或者干脆答非所问。这时候很多人第一反应是去翻 get_weather 的逻辑,怀疑字典写错、城市名对不上、async 没加 await。
我试过把排查顺序倒过来,先看 stdout 干不干净,再看 base_url 填得对不对,八成问题当场现形。MCP 的客户端与服务器之间走的是标准输入输出上的 JSON-RPC,stdout 是协议数据的专用通道,任何一句调试用的 print 混进去,都会让客户端解析失败。weather_server.py 里专门把 print 重定向到 stderr,就是在守这条底线。而 main.py 里 OpenAI 客户端的 base_url 如果填成官网落地页或者多带了 /v1,模型对话补全的请求根本发不出去,工具结果自然回不来。这篇就按排障视角,把这两处最容易踩的坑一次讲清。
2. 先把 TaoToken 通道准备好,Key 和地址别混用
MCP 本身只负责模型与工具之间的通信,真正决定“模型能不能收到工具结果”的,是模型对话补全那一段请求有没有成功往返。所以你需要一个稳定的模型通道,把 main.py 里的 OpenAI 客户端指向它。到 TaoToken 官网创建 Key 的入口在这里:
官网落地页:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
登录后在控制台生成 API Key,形如 sk- 开头的一串字符。这里要区分两个地址,很多排障卡住就是混用了它们:
| 用途 | 地址 | 说明 |
|---|---|---|
| 浏览器访问、注册、看文档 | https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= | 官网落地页,带查询参数,给人看的 |
| 代码里 base_url | https://taotoken.net/api | 接口地址,给 OpenAI 客户端用的 |
注意接口地址后面不要带 /v1。OpenAI SDK 会自己在 base_url 后面拼 /chat/completions 这类路径,你多写一层 /v1 就变成 /api/v1/chat/completions,请求直接 404,表现就是模型侧毫无响应,工具调用链断在第一步。另外也别把官网落地页那一长串带 utm 的地址填进 base_url,那是页面地址不是接口地址,客户端会把它当成非法 URL 处理。
Key 的管理和查看在控制台完成,需要轮换或补额度时从这里进:
API Keys 管理:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
如果你后面要长期跑编码类 Agent、反复调试 MCP 工具链,可以了解下 Coding Plan,按套餐走比单次调用更省心:
Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
3. 可复制配置:main.py 的 base_url 与 weather_server.py 的 stdout 双确认
排障的核心就两处确认,先把 main.py 里 OpenAI 客户端的初始化改对。下面这段是修正后的写法,重点看 base_url 和 api_key 两行:
import os import json import asyncio from openai import OpenAI from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client # 从环境变量读取 Key,避免硬编码进仓库 client = OpenAI( api_key=os.environ.get("TAOTOKEN_API_KEY", "sk-你的Key"), base_url="https://taotoken.net/api" # 注意:不带 /v1,也不是官网落地页 ) async def main(): server_params = StdioServerParameters( command="py", args=["weather_server.py"] ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools_response = await session.list_tools() tools = [ { "type": "function", "function": { "name": tool.name, "description": tool.description, "parameters": tool.inputSchema } } for tool in tools_response.tools ] messages = [ {"role": "system", "content": "你是一个贴心的天气助手,请根据工具提供的数据回答用户。"}, {"role": "user", "content": "今天杭州天气如何?"} ] response = client.chat.completions.create( model="deepseek-chat", messages=messages, tools=tools, tool_choice="auto" ) msg = response.choices[0].message if msg.tool_calls: messages.append(msg) for tool_call in msg.tool_calls: result = await session.call_tool( tool_call.function.name, json.loads(tool_call.function.arguments) ) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result.content[0].text }) final_response = client.chat.completions.create( model="deepseek-chat", messages=messages ) print("AI助手最终回复:", final_response.choices[0].message.content) if __name__ == "__main__": asyncio.run(main())第二处确认在 weather_server.py,print 重定向必须放在所有业务代码之前,且不能被后续 import 覆盖:
import sys import builtins # 核心修复:把 print 全部导向 stderr,保证 stdout 只跑 JSON-RPC _orig_print = builtins.print builtins.print = lambda *a, **k: _orig_print(*a, file=sys.stderr, **k) import asyncio from fastmcp import FastMCP mcp = FastMCP("Weather-Server") @mcp.tool() async def get_weather(city: str) -> str: """ 获取指定城市的当前天气情况。 参数: city - 城市名称,例如:杭州、北京 """ fake_weather_data = { "杭州": "晴天,气温 28℃,微风,非常适合散步。", "北京": "多云,气温 25℃,空气质量良。", "上海": "小雨,气温 22℃,出门记得带伞。" } return fake_weather_data.get(city, f"抱歉,暂时无法获取 {city} 的天气信息。") if __name__ == "__main__": mcp.run()这两处改完,stdout 里只剩协议帧,模型侧请求也指向了正确接口,工具结果才有机会回到对话里。消耗 Token 的正是最后那段模型对话补全,前面握手和工具调用本身不产生补全消耗。
4. 验证请求:从握手日志到最终回复的完整链路
配置改好后,按顺序跑一遍验证。先装依赖:
pip install openai mcp fastmcp设置环境变量再运行,避免 Key 写死在代码里:
# Windows PowerShell $env:TAOTOKEN_API_KEY="sk-你的Key" python main.py # macOS / Linux export TAOTOKEN_API_KEY="sk-你的Key" python main.py正常的话你会看到类似输出:
AI助手最终回复: 今天杭州是晴天,气温 28℃,微风,非常适合散步。如果只看到握手成功、list_tools 也返回了工具,但最终回复里没有天气数据,可以加一段临时日志确认工具结果有没有被塞回 messages。在messages.append那段后面加一行打印到 stderr 的调试:
import sys print("工具返回:", result.content[0].text, file=sys.stderr)注意这里显式指定 file=sys.stderr,不要用裸 print,否则又污染 stdout。看到这行说明工具执行成功,问题就落在模型补全请求上,回去检查 base_url 和 Key。看不到这行说明 tool_calls 为空,模型没决定调用工具,可以检查 tools 描述是否完整传给了请求。
想单独验证模型通道是否通,可以到模型对话页面发一条普通消息,确认 Key 和接口地址没问题:
模型对话:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
5. 本篇常见错排查
报错一:握手正常,模型回复“我无法获取实时天气”。这是最典型的症状。先确认 weather_server.py 的 print 重定向是否真的生效,有没有在重定向之前就 import 了会打印日志的库。再确认 main.py 的 base_url 是不是写成了官网落地页,或者多带了 /v1。两者任一出错,工具结果都回不到模型。
报错二:客户端解析 JSON 失败,日志里出现 Unexpected token。说明 stdout 被污染了。检查 weather_server.py 里有没有漏网的 print,包括第三方库在 import 时打的日志。重定向必须放在最顶部,晚于任何可能输出的代码都不行。
报错三:请求返回 404 或连接被拒。九成是 base_url 写错。正确值是 https://taotoken.net/api,不带 /v1,不带查询参数。把官网落地页那串带 utm 的地址填进去也会报错,那是页面地址不是接口地址。
报错四:tool_calls 为空,模型直接回答。检查传给请求的 tools 列表是否为空,list_tools 是否真的返回了 get_weather。另外 system 提示词里可以明确要求“涉及天气必须调用工具”,提高模型调用意愿。
报错五:Key 无效或额度不足。到控制台确认 Key 状态和余额,必要时重新生成。接入细节和参数说明可以对照接入文档:
接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
6. 排障收尾:把两处确认固化成习惯
MCP 工具链的排障,顺序比努力重要。先看 stdout 干不干净,再看 base_url 填得对不对,最后才去怀疑 get_weather 的业务逻辑。weather_server.py 里那行 print 重定向不是可有可无的装饰,它是保证 JSON-RPC 通道纯净的关键;main.py 里 base_url 填 https://taotoken.net/api、不带 /v1、不填官网落地页,是保证模型对话补全能往返的前提。两处都确认过,模型才能拿到 get_weather 的返回并继续回答。
如果你在跑更复杂的多工具 MCP 场景,或者要把这套链路接进长期运行的编码 Agent,建议把 Key 管理、接口地址、stdout 规范写成一份项目内的 checklist,每次新增工具时过一遍。需要长期编码或 Agent 场景的,可以从 Coding Plan 入手;只是临时验证模型通道,模型对话页面足够。排障时优先查 API Keys 和接入文档,比盲目改业务代码快得多。