1. 从一次“工具发现失败”说起:FastMCP 到底解决了什么问题
如果你正在用 Python 写 Agent,大概率遇到过这种场景:本地写了一个查天气的函数,想让大模型调用它,结果要么是手写 JSON Schema 写到吐,要么是客户端tools/list返回空列表,要么是tools/call报Method not found。这些问题的根源,往往不是模型不行,而是工具暴露层没打通。
FastMCP 就是干这件事的:它用装饰器把普通 Python 函数变成符合 MCP 协议的 JSON-RPC 工具,服务端自动生成inputSchema,客户端通过tools/list发现、tools/call调用,整条链路一次跑通。适合谁?适合想快速把已有 Python 函数暴露成 MCP 工具、又不想手写协议解析的开发者;也适合已经在用 Claude Code、Cline 这类客户端,想把本地能力接进去的人。
我试过最省事的路径是:本地起一个 FastMCP 服务端,用fastmcp dev打开 Inspector 验证工具发现,再用一个最小 Python 客户端发tools/call,确认返回结构。等本地通了,再把 endpoint 换到统一 Key/API 通道做联调,避免每个工具都去配一遍鉴权。下面按“原问题 → 前置 → 配置 → 验证 → 排障 → 收尾”的顺序走一遍,每一步都能复制。
核心检索词先记住三个:FastMCP 装饰器写法、MCP JSON-RPC 调用链路、Python 暴露 MCP 工具。这三个词贯穿全文,后面每个章节都会落到具体代码上。
2. TaoToken 前置:统一 Key 与 API 通道怎么准备
在把 endpoint 切到统一通道之前,先把本地环境跑通。FastMCP 的安装推荐用 uv,比 pip 快且依赖隔离干净:
uv add fastmcp python -c "import fastmcp; print(fastmcp.__version__)"如果输出类似2.x.x就说明装好了。接着准备统一通道的凭证。访问 TaoToken 官网 注册后,在控制台创建 API Key,路径是 Console。Key 创建完去 API Keys 页面 复制,注意只显示一次。
这里要区分两个地址:官网带 UTM 用于归因,API 基址是https://taotoken.net/api,不带任何参数。客户端配置里填的是后者。如果你用的是 Claude Code 这类工具,接入文档在 doc,里面有 Base URL、Key、Model ID 三件套的完整说明。
为什么要先做这一步?因为 FastMCP 服务端本身不负责模型鉴权,它只暴露工具。真正需要统一 Key 的是调用模型的客户端。所以联调时的顺序是:FastMCP 服务端本地跑通 → 客户端能发现工具 → 客户端把模型请求指向统一通道 → 端到端验证。这样出问题时能快速定位是工具层还是模型层。
一个容易踩的坑:有人把 API Key 直接写进 FastMCP 服务端代码里,这是错的。服务端只管工具注册,Key 属于客户端配置。分开之后,换 Key 不用动服务端代码。
3. 可复制配置:装饰器注册 + 客户端 settings 片段
先写服务端。新建server.py,用装饰器注册三个工具,覆盖无参、必填参、可选参三种情况:
from fastmcp import FastMCP mcp = FastMCP( name="demo-tools", version="1.0.0", instructions="演示 FastMCP 装饰器注册与 JSON-RPC 调用" ) @mcp.tool() def add(a: float, b: float) -> float: """两数相加""" return a + b @mcp.tool() def greet(name: str, excited: bool = False) -> str: """打招呼,excited 为 True 时加感叹号""" suffix = "!" if excited else "." return f"Hello, {name}{suffix}" @mcp.tool() def list_items(limit: int = 5) -> list: """返回示例列表""" return [{"id": i, "name": f"item-{i}"} for i in range(limit)]装饰器@mcp.tool()会自动读取函数签名和 docstring,生成 JSON Schema。a: float变成{"type": "number"},excited: bool = False变成可选布尔字段。这就是 FastMCP 相比手写 SDK 最省事的地方。
启动服务端,用 SSE 传输方便客户端连:
fastmcp run server.py --transport sse --host 127.0.0.1 --port 8000客户端配置。如果你用 Cline 或类似支持 MCP 的编辑器,settings 片段长这样:
{ "mcpServers": { "demo-tools": { "url": "http://127.0.0.1:8000/sse", "transport": "sse" } } }如果客户端需要走统一通道调模型,模型侧的配置单独放。以 Claude Code 为例,~/.claude/settings.json里加:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "你的Key", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" } }三件套齐了:Base URL 指向https://taotoken.net/api,Key 用控制台创建的,Model ID 按文档填。注意 Base URL 不带 UTM,UTM 只用于官网跳转归因。
Codex 用户如果用的是auth.json,结构类似:
{ "base_url": "https://taotoken.net/api", "api_key": "你的Key", "model": "gpt-4o" }这里的关键是:MCP 服务端配置和模型通道配置是两套东西,不要混在一个文件里。混了之后排障会很痛苦,因为分不清是工具没注册还是模型没连上。
4. 验证请求:tools/list 与 tools/call 一次跑通
服务端起好后,先别急着接客户端,用 Python 写个最小客户端验证 JSON-RPC 链路。这样能排除编辑器配置的干扰:
import asyncio from fastmcp import Client async def main(): async with Client("http://127.0.0.1:8000/sse") as client: tools = await client.list_tools() print("发现工具:", [t.name for t in tools]) result = await client.call_tool("add", {"a": 3, "b": 4}) print("add 结果:", result) result = await client.call_tool("greet", {"name": "MCP", "excited": True}) print("greet 结果:", result) asyncio.run(main())预期输出:
发现工具: ['add', 'greet', 'list_items'] add 结果: 7.0 greet 结果: Hello, MCP!如果list_tools返回空,说明装饰器没生效或服务端没重启。如果call_tool报Tool not found,检查工具名是否和注册时一致。FastMCP 默认用函数名作为工具名,除非你在装饰器里传了name=。
想直接看 JSON-RPC 原始报文,可以用 curl 发一个tools/list:
curl -X POST http://127.0.0.1:8000/messages \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'返回结构里result.tools是数组,每个元素有name、description、inputSchema。inputSchema就是 FastMCP 从函数签名自动生成的,你可以对照add的 schema 看a和b是不是number类型。
验证通过后,把客户端里的模型请求指向统一通道,再问一句“帮我算 3 加 4”,如果模型能正确调用add工具并返回 7,说明工具发现、调用、模型通道三层全通了。这一步是整个链路的关键验证点,过了就说明配置没问题。
5. 本篇常见错排查:401、local proxy failed、reading choices
排障按报错信息对号入座,下面几个是我实际遇到过的。
401 Unauthorized:出现在客户端调模型时。原因通常是 Key 没填、填错,或者 Base URL 写成了带 UTM 的官网地址。检查ANTHROPIC_BASE_URL或base_url是不是https://taotoken.net/api,Key 是不是从 API Keys 页面 复制的完整字符串。注意 Key 只显示一次,复制时别带空格。
local proxy failed:这个报错通常出现在客户端尝试连接 MCP 服务端时。检查fastmcp run的 host 和 port 是否和客户端配置一致。如果服务端跑在127.0.0.1:8000,客户端却写localhost:8000,某些环境下会解析失败。统一用127.0.0.1更稳。另外确认 SSE 路径是/sse,不是根路径。
reading choices 报错:这个一般出现在模型返回结构解析阶段,常见于客户端把非标准响应当成 OpenAI 格式解析。检查 Model ID 是否填对,以及 Base URL 是否指向了正确的 API 路径。如果用的是 Claude Code,确认ANTHROPIC_MODEL是文档里列出的可用模型。
OAuth 相关报错:如果客户端提示 OAuth 失败,说明它尝试走 OAuth 流程但配置里没提供。MCP 服务端本地调试不需要 OAuth,把客户端里相关的 auth 字段去掉,只保留 url 和 transport。
tools/call 返回 Method not found:检查方法名拼写。MCP 标准方法是tools/list、tools/call、resources/list、prompts/list。大小写敏感,Tools/List会失败。
装饰器没生效:确认@mcp.tool()写在函数定义正上方,中间没有空行或其他装饰器干扰。如果函数是 async 的,FastMCP 也支持,但调用时要确保客户端用 await。
排障时建议开 DEBUG 日志:
fastmcp run server.py --transport sse --log-level DEBUG日志里能看到每个 JSON-RPC 请求的 method 和 params,对照客户端发的报文,很快能定位是哪一层的问题。
6. 收尾:把工具接进长期编码流
本地跑通之后,下一步是把这套东西接进日常编码流。如果你只是偶尔验证模型能力,用 模型对话 手动测一下工具调用就行。如果是要长期跑 Agent、做自动化编码,建议走 Coding Plan,把统一 Key 和 MCP 工具一起管起来,省得每次换项目都重新配。
一个实用技巧:把 FastMCP 服务端写成可复用的模块,工具函数按业务拆文件,用mcp.mount()挂载子服务。这样新增工具不用改主文件,也不会因为一个工具报错影响其他工具。另外,工具函数的 docstring 一定要写清楚,FastMCP 会把它作为description传给模型,描述越准确,模型选对工具的概率越高。
最后提醒一句:MCP 服务端不要直连生产数据库。本地调试用 mock 数据或只读副本,等工具逻辑稳定了再考虑接真实数据源,并且加好权限校验。工具暴露出去之后,模型能调用的能力就是真实能力,安全边界要在服务端守住。