1. 为什么 MCP 天气 demo 总是卡在模型通道这一行
MCP(Model Context Protocol)这两年被聊得很多,但真正动手跑通一个最小闭环的人,往往不是卡在协议本身,而是卡在「执行工具那一端的模型通道」上。我见过太多照着教程抄weather_server.py和client.py的朋友,Server 端mcp.run(transport="stdio")跑得好好的,session.list_tools()也能列出query_weather,结果一到client.chat.completions.create()就报 401、404 或者Connection error。
问题几乎都出在同一个地方:OpenAI(api_key=..., base_url=...)这两行。教程里写的是base_url="compatible-mode/v1",很多人直接复制,结果 SDK 拼出来的请求地址缺了域名,或者多带了一层/v1,请求根本发不出去。更麻烦的是,当你想换一个模型通道(比如从默认的 qwen-max 换成别的),又不知道该动哪一行——是改model字段,还是改base_url,还是两个都要改?
这篇就专门解决这个痛点。MCP 协议、stdio 管道、weather_server.py里的query_weather逻辑,全部保持原样不动,我只替换 client 里取 Key 和写base_url的两行。改完之后,asyncio.run(main())能在「AI 最终回复」里打印出城市天气文本,就说明 tool_call 链路和模型请求都通了。适合已经会写 FastMCP Server、但被 client 模型通道卡住的人。
2. 先理清 MCP 的两条传输通道和天气 Server 的角色
在动 client 之前,得先明确一件事:MCP 的连接和模型请求是两条完全独立的链路,别把它们混在一起。
2.1 Stdio 与 Streamable HTTP 的分工
Stdio 是本地通信方式。客户端把 MCP Server 当成一个子进程启动,两者通过操作系统的 stdin/stdout 管道「递纸条」,完全绕开网络栈,延迟低、数据不出本机。天气 demo 用的就是这条通道,mcp.run(transport="stdio")就是让 Server 挂在这条管道上等消息。
Streamable HTTP 是远程通信方式,客户端发 POST 请求,服务端通过长连接推送流式响应,适合云端服务、SaaS 集成、企业多租户。早期的 SSE 因为只支持单向推送,正在被它取代。
关键点在于:这两条通道负责的是「客户端 ↔ MCP Server」之间的工具调用,跟「客户端 ↔ 大模型」之间的请求没有任何关系。天气 Server 只干一件事——收到query_weather(city)就返回天气文本。至于这个工具调用请求是谁发起的、模型是哪家的,Server 完全不关心。
2.2 weather_server.py 里 query_weather 到底做了什么
Server 端逻辑很简单,fetch_weather用 httpx 异步请求天气接口,format_weather把 JSON 拼成自然语言,@mcp.tool()装饰的query_weather把两者串起来。它对外暴露的接口就是「给我一个城市名,我还你一段天气描述」。
所以 client 里真正需要模型参与的部分只有两处:第一次create()让模型决定要不要调query_weather,第二次create()把工具结果回灌给模型生成最终回复。这两次请求走的是 OpenAI SDK,跟 MCP 的 stdio 管道是并行的两条线。
3. TaoToken 前置:只提供 Key 和 Base URL 两样东西
这里要特别说清楚 TaoToken 在这个 demo 里的定位,避免误解。
TaoToken 在这里只提供两样东西:一把 API Key,和一个 Base URL。它不参与 MCP 的连接,不参与 stdio 管道的建立,也不参与session.call_tool()的执行。工具调用那一端仍然是你的本地 Server 通过管道完成的,TaoToken 完全不碰。
换句话说,你只是把 client 里「向大模型发请求」这一段的地址和凭证换掉,其余逻辑一行不动。
操作上分两步:
第一步,打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册账号。注册流程很常规,邮箱验证完就能进控制台。
第二步,在控制台里创建一把 API Key。创建入口在 https://taotoken.net/api-keys ,点新建,复制出来的那串就是你要填进 client 的api_key。
拿到 Key 之后,Base URL 固定填https://taotoken.net/api。注意这里有两个坑:不要带/v1,也不要带任何 utm 参数。SDK 会自己在后面拼/chat/completions,你多写一层/v1就会变成/api/v1/chat/completions,直接 404。
4. 可复制配置:只改 client 里的两行
现在进入正题。假设你的weather_server.py已经按教程写好,uv venv、uv add mcp、uv add openai httpx python-dotenv都装完了,我们只动 client。
4.1 改 api_key 和 base_url
原来的写法是从环境变量取DASHSCOPE_API_KEY,base_url写compatible-mode/v1。现在替换成 TaoToken 的 Key 和地址:
import asyncio import os import json from contextlib import AsyncExitStack from openai import OpenAI from dotenv import load_dotenv from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client load_dotenv() async def main(): # 只改这两行:Key 换成 TaoToken 创建的 Key,base_url 换成 TaoToken 地址 client = OpenAI( api_key=os.getenv("TAOTOKEN_API_KEY"), base_url="https://taotoken.net/api" ) server_params = StdioServerParameters( command="D:\\python.exe", args=["e:\\mcp-servers-weather.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() openai_tools = [ { "type": "function", "function": { "name": tool.name, "description": tool.description, "parameters": tool.inputSchema } } for tool in tools_response.tools ] messages = [{"role": "user", "content": "Shanghai weather today"}] response = client.chat.completions.create( model="qwen-max", messages=messages, tools=openai_tools, tool_choice="auto" ) message = response.choices[0].message if message.tool_calls: for tool_call in message.tool_calls: if tool_call.function.name == "query_weather": args = json.loads(tool_call.function.arguments) result = await session.call_tool("query_weather", arguments=args) messages.append(message) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result.content[0].text }) final_response = client.chat.completions.create( model="qwen-max", messages=messages ) print("AI 最终回复:", final_response.choices[0].message.content) else: print("AI 回复:", message.content) if __name__ == "__main__": asyncio.run(main())4.2 把 Key 放进 .env
别把 Key 硬编码在代码里。在项目根目录建一个.env:
TAOTOKEN_API_KEY=你从控制台复制的那串Keyload_dotenv()会自动把它读进环境变量,os.getenv("TAOTOKEN_API_KEY")就能取到。如果你之前用的是DASHSCOPE_API_KEY,把变量名换掉即可,代码里对应的os.getenv也要跟着改。
4.3 想换模型时改哪一行
这是很多人问的问题。答案很明确:只改model字段。
base_url指向的是 TaoToken 的入口,它背后能路由到哪些模型是服务端的事,你不需要为每个模型换一个地址。所以从qwen-max换成别的,只动model="..."这一处,两处create()都要改(第一次决定调工具、第二次生成最终回复)。
| 改动项 | 改哪里 | 说明 |
|---|---|---|
| 换 Key | .env里的TAOTOKEN_API_KEY | 控制台重新创建即可 |
| 换模型 | 两处create()的model字段 | base_url 不动 |
| 换 Base URL | OpenAI(base_url=...) | 固定https://taotoken.net/api,不带 /v1 |
5. 验证请求:看到天气文本就算通了
配置改完,直接跑:
python client.py如果一切正常,终端会打印类似这样的内容:
AI 最终回复: 上海当前天气:多云,气温 22°C,湿度 68%看到「AI 最终回复」后面跟着城市天气文本,就说明整条链路通了。这里其实验证了两件事:一是session.call_tool("query_weather", ...)通过 stdio 管道成功拿到了工具结果,二是两次client.chat.completions.create()都成功打到了 TaoToken 的地址并返回了内容。
如果第一次create()返回的message.tool_calls是空的,说明模型没决定调工具,可以检查tools参数有没有正确传入、tool_choice是不是"auto"。如果tool_calls有值但最终回复为空,多半是第二次create()的messages里 tool 结果没拼对。
想单独验证模型通道是否通,可以先用模型对话页面发一条普通消息,确认 Key 和地址没问题,再回来跑完整 demo。
6. 本篇常见错排查
6.1 base_url 少写域名或多带 /v1
这是最高频的错。base_url="compatible-mode/v1"这种写法是相对路径,SDK 拼出来会缺域名,直接报连接错误。正确写法是完整的https://taotoken.net/api。反过来,写成https://taotoken.net/api/v1也不行,SDK 会再拼一层,变成/api/v1/chat/completions,返回 404。
记住一个判断方法:base_url 填到/api为止,后面什么都不要加。
6.2 401 报错:Key 没读到或填错
openai.AuthenticationError一般是 Key 的问题。先确认.env文件在项目根目录、变量名和os.getenv里写的一致。如果.env里有多余空格或引号,也会导致读取失败。最稳妥的办法是临时print(os.getenv("TAOTOKEN_API_KEY"))看一眼,确认取到的是完整字符串。
6.3 stdio 管道报错:路径写错
StdioServerParameters里的command和args必须是绝对路径。Windows 下反斜杠要转义,写成"D:\\python.exe"。如果command指向的 python 不是你装 mcp 依赖的那个虚拟环境,会报ModuleNotFoundError: No module named 'mcp'。建议直接用虚拟环境里的 python 解释器路径。
6.4 工具调用成功但最终回复为空
检查messages.append(message)有没有执行。第一次create()返回的message对象必须原样 append 进messages,否则第二次请求时模型看不到自己发起的 tool_call,会不知道该怎么接。同时tool_call_id要和message.tool_calls里的 id 对应上。
6.5 换模型后报模型不存在
如果model字段填了一个 TaoToken 不支持的名称,会返回模型不存在的错误。换模型前先在模型对话页面确认目标模型可用,再填进代码。base_url 不用动,只改model。
7. 后续怎么接
跑通这个 demo 之后,你手里其实有了一套可复用的模板:MCP Server 负责工具,client 负责把工具结果回灌给模型,两者通过 stdio 管道解耦。要加新工具,就在 Server 里再写一个@mcp.tool();要换模型通道,就改 client 里的model字段。
如果你打算把这个模式用到长期编码或 Agent 场景,比如让模型反复调用多个工具、维护多轮上下文,可以了解一下 Coding Plan,它更适合这种持续性的调用需求。接入文档在 https://taotoken.net/doc ,里面有完整的参数说明和示例。API Key 管理还是回到 https://taotoken.net/api-keys ,随时可以新建或吊销。
最后提醒一句:TaoToken 在这个链路里只负责模型请求的 Key 和 Base URL,MCP 的连接、工具的注册与执行,全部在你本地完成。把这两条链路分清楚,后面再遇到报错,你就能一眼判断是模型通道的问题还是 MCP 管道的问题。