1. 从 v1 到 v2,FastMCP 迁移到底卡在哪
如果你最近在本地写 MCP 服务,大概率会遇到一个很具体的困惑:昨天还能跑的from mcp.server.fastmcp import FastMCP,今天升级依赖之后直接 ImportError。这不是你代码写错了,而是 MCP Python SDK 从 v1.0 走到 v2.0 时,把 FastMCP 这条导入路径彻底拆掉了。FastMCP 本身是一个基于 MCP 协议做上层封装的框架,早期它被整合进官方 SDK,后来又独立维护,于是出现了「同一个名字、两套来源」的局面。到了 v2.0,官方库不再兼容 FastMCP,函数位置、类名、参数命名规则都变了。
这篇文章面向的是在本地开发 MCP 服务、需要把工具暴露给客户端调用的 Python 开发者。核心要解决的问题有三个:第一,搞清楚 v1 和 v2 在导入、工具注册、回调签名上的差异;第二,给出可复制的依赖锁定方式和 server 配置片段;第三,把 endpoint 改到 TaoToken 的统一 Key/API 通道,用 curl 验证工具列表和调用链路是否真的通了。适合谁?适合已经写过一两个 MCP server、但被版本升级打断节奏的人,也适合刚接触 FastMCP、想一步到位用对版本的新手。
我试过在同一个虚拟环境里同时装mcp==1.29.0和mcp==2.0.0,结果就是依赖解析直接打架。所以第一步不是改代码,而是先把版本锁死。下面会按「问题场景 → 前置准备 → 可复制配置 → 验证请求 → 错排查 → 接入收尾」的顺序展开,每一步都尽量给到能直接粘贴的命令和片段。
先明确一个概念:MCP 协议本身在 v1 到 v2 之间没有破坏性变化,变的是 Python SDK 的实现层。也就是说,你的工具逻辑、JSON-RPC 消息格式基本不用动,动的是「怎么把工具注册进去」和「回调函数长什么样」。理解这一点,迁移就不会那么慌。
2. TaoToken 前置:统一 Key 与 API 通道准备
在动手改 server 之前,先把联调要用的通道准备好。本地 MCP 服务最终是要被客户端调用的,而调用链路里如果每个模型、每个工具都各配一套 Key,维护成本会很高。TaoToken 在这里的作用是提供一个统一的 Key/API 通道,把 endpoint 收敛到一处,方便你在迁移过程中专注在代码本身,而不是到处找配置。
你需要先拿到一个可用的 API Key。打开官网https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=注册并登录,然后进入控制台创建 Key。控制台地址是https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite,在 API Keys 页面可以新建和复制 Key,对应页面https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite。复制出来的 Key 形如一串长字符串,先存到环境变量里,别硬编码进代码。
API 的基础地址是https://taotoken.net/api,注意这个地址不带 UTM 参数,是给程序调用的。文档入口在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有各语言接入示例和参数说明,迁移过程中遇到字段不确定的,优先查文档而不是猜。
把 Key 写进环境变量,Linux/macOS 下可以这样:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"Windows PowerShell 用:
$env:TAOTOKEN_API_KEY="sk-你的key" $env:TAOTOKEN_BASE_URL="https://taotoken.net/api"为什么要先做这一步?因为后面验证 MCP 工具调用时,你会需要一个真实的模型 endpoint 来跑通链路。如果 Key 没准备好,验证阶段就会卡在 401 上,分不清是代码问题还是鉴权问题。提前把通道打通,排障时变量就少一个。
另外提醒一点:TaoToken 是统一接入通道,不是让你绕过什么限制,它的价值在于把多个模型的调用收敛到一套 Key 和一套 Base URL 上。你在本地 MCP server 里配置 endpoint 时,指向这个统一地址即可,模型 ID 按文档里支持的填。
3. 可复制配置:依赖锁定与 server 片段
这一节是全文最需要动手的部分。先解决依赖版本,再给 v1 和 v2 两套 server 代码,最后给一份 JSON 配置片段。
3.1 依赖版本锁定
FastMCP 和 MCP SDK 的版本对应关系容易混。按实践中的组合:FastMCP 用fastmcp==3.4.0,MCP SDK v1.0 用mcp==1.29.0,MCP SDK v2.0 用mcp==2.0.0。如果你要用独立维护的 FastMCP,导入路径是from fastmcp import FastMCP,这条路径在 v2 时代依然不变。
推荐用requirements.txt锁死,避免pip install mcp自动拉到最新版:
# requirements.txt mcp==2.0.0 fastmcp==3.4.0如果你还在 v1 阶段,改成:
mcp==1.29.0 fastmcp==3.4.0安装命令:
python -m venv .venv source .venv/bin/activate pip install -r requirements.txt用pip freeze > requirements.lock再固化一次,团队协作时更稳。
3.2 v1 的 server 写法
v1 用装饰器注册工具,风格接近 FastAPI:
from mcp.server import Server from mcp.types import Tool server = Server("my-calculator") @server.list_tools() def list_tools() -> list[Tool]: return [ Tool( name="add", description="Add two numbers", inputSchema={ "type": "object", "properties": { "a": {"type": "number"}, "b": {"type": "number"} }, "required": ["a", "b"] } ) ] @server.call_tool() def call_tool(name: str, arguments: dict) -> list[dict]: if name == "add": result = arguments["a"] + arguments["b"] return [{"type": "text", "text": str(result)}] raise ValueError(f"Unknown tool: {name}") if __name__ == "__main__": server.run(transport="stdio")注意 v1 里字段是驼峰inputSchema,返回值是裸字典列表,SDK 会自动包装。
3.3 v2 的 server 写法
v2 放弃装饰器,改用构造函数传回调,并且回调是 async 的:
from mcp.server import Server from mcp.types import ( Tool, ListToolsResult, CallToolResult, TextContent, CallToolRequestParams ) from mcp.shared.context import RequestContext async def list_tools_callback(ctx: RequestContext, params: dict) -> ListToolsResult: return ListToolsResult( tools=[ Tool( name="add", description="Add two numbers", input_schema={ "type": "object", "properties": { "a": {"type": "number"}, "b": {"type": "number"} }, "required": ["a", "b"] } ) ] ) async def call_tool_callback(ctx: RequestContext, params: CallToolRequestParams) -> CallToolResult: if params.name == "add": result = params.arguments["a"] + params.arguments["b"] return CallToolResult( content=[TextContent(type="text", text=str(result))] ) raise ValueError(f"Unknown tool: {params.name}") server = Server( "my-calculator", on_list_tools=list_tools_callback, on_call_tool=call_tool_callback, ) if __name__ == "__main__": server.run(transport="stdio")关键差异:v2 字段是蛇形input_schema,回调必须返回完整的ListToolsResult/CallToolResult对象,且都是 async。
3.4 客户端 JSON 配置片段
把 endpoint 指向 TaoToken 统一通道,配置片段如下(以常见的 MCP 客户端配置为例):
{ "mcpServers": { "my-calculator": { "command": "python", "args": ["-m", "my_calculator_server"], "env": { "TAOTOKEN_API_KEY": "sk-你的key", "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "MCP_MODEL_ID": "按文档支持的模型ID填写" } } } }如果你用的是 Claude Code 这类工具,配置里同样要写全三件套:Base URL 填https://taotoken.net/api,Key 填你的sk-开头字符串,Model ID 按文档支持的填。三件套缺一个,链路就断。
4. 验证请求:curl 跑通工具列表与调用链路
代码写完不代表通了,必须用 curl 实测。MCP 走 stdio 时不好直接 curl,所以建议先用 HTTP transport 起一个本地服务,或者用 SDK 自带的调试方式。下面给一个通用的验证思路。
先确认服务能启动:
python -m my_calculator_server如果没报错,说明导入和注册没问题。接着用 curl 验证模型 endpoint 是否可达:
curl -s -X POST "https://taotoken.net/api/v1/chat/completions" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "按文档支持的模型ID填写", "messages": [{"role": "user", "content": "ping"}] }'返回里能看到choices字段,说明 Key 和 Base URL 都对。如果返回 401,先查 Key;如果返回local proxy failed,查网络和 Base URL 拼写。
验证工具列表,可以用 MCP 的tools/list请求。假设你的服务暴露在本地某端口:
curl -s -X POST "http://127.0.0.1:8000/mcp" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'期望返回里包含add工具及其input_schema。如果返回里tools为空,说明list_tools_callback没被正确注册,回去检查 v2 的构造函数参数名是不是on_list_tools。
验证工具调用:
curl -s -X POST "http://127.0.0.1:8000/mcp" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc":"2.0","id":2,"method":"tools/call", "params":{"name":"add","arguments":{"a":1,"b":2}} }'期望返回content里是"3"。如果报reading choices相关错误,通常是模型返回结构和你解析的字段对不上,检查是不是把 MCP 的返回和模型 API 的返回混在一起解析了。
实测下来,最容易出问题的是 v2 的 async 回调忘了加await,或者返回了裸字典而不是CallToolResult。这两处一错,curl 就会给你一个含糊的 500。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
迁移过程中报错集中在几类,逐个对照。
401 Unauthorized:Key 没传、传错、或者环境变量没生效。检查echo $TAOTOKEN_API_KEY是否有值,curl 头里Bearer后面有没有多余空格。如果 Key 是从控制台复制的,注意别把前后空白带进去。
local proxy failed:这个报错通常出现在 Base URL 配置不对,或者本地网络到 endpoint 不通。先确认TAOTOKEN_BASE_URL是https://taotoken.net/api,没有多余路径。再用curl -v看握手过程,定位是 DNS 还是连接超时。
reading choices 相关错误:多出现在解析模型响应时。模型 API 返回的是choices数组,而 MCP 工具调用返回的是content数组,两者结构不同。如果你在同一个函数里混着解析,就会读不到choices。分开处理:模型调用走choices,工具调用走content。
OAuth 报错:部分客户端在连接远程 MCP 服务时会走 OAuth 流程。如果你只是本地 stdio 服务,不需要 OAuth,检查客户端配置里是不是误开了远程模式。需要 OAuth 的场景,按文档配置回调地址,别自己拼。
ImportError: cannot import name 'FastMCP' from 'mcp.server.fastmcp':这是 v2 最典型的报错。v2 删掉了fastmcp模块和FastMCP类,改成了MCPServer。要么把导入改成from mcp.server import MCPServer,要么改用独立 FastMCP 的from fastmcp import FastMCP。两条路选一条,别混用。
TypeError: object list can't be used in 'await' expression:v2 回调是 async,但你可能写成了同步函数。给list_tools_callback和call_tool_callback加上async。
排障时建议开 debug 日志:
export MCP_LOG_LEVEL=DEBUG python -m my_calculator_server日志里能看到请求进出的完整 JSON-RPC 消息,比猜快得多。
6. 语义一致收尾:把 endpoint 收敛到统一通道
迁移的最后一步,是把所有 endpoint 统一到 TaoToken 的 Key/API 通道上,这样 v1 和 v2 的 server 都能用同一套鉴权和地址,切换版本时只动代码不动配置。
如果你要长期跑编码类 Agent,或者需要多模型切换,可以了解下 Coding Plan,入口在https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite。需要直接对话验证模型效果的,用模型对话页https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite。接入文档和 API Keys 分别在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite和https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite。
最后给一个实用技巧:把 v1 和 v2 的 server 代码放在同一个仓库的不同分支,用requirements.txt区分版本,切换时只改依赖和导入行,工具逻辑完全复用。这样下次 SDK 再升级,你只需要改一个文件,而不是重写整个服务。