1. 从一次本地联调翻车说起:FastMCP 服务到底难在哪
FastMCP 是一个用 Python 快速构建 MCP 服务的框架,你可以把它理解成「MCP 版的 FastAPI」——用装饰器把普通函数注册成工具、资源和提示模板,几行代码就能跑起一个符合 MCP 规范的服务端。它适合谁?适合想把内部脚本、数据库查询、文件读取能力暴露给 AI 客户端的后端开发者,也适合正在做 Agent 工具链、需要统一管理模型调用通道的团队。
但真正动手时,卡人的往往不是@mcp.tool()怎么写,而是三件事:第一,服务跑起来了,客户端却连不上,SSE 端点握手失败;第二,工具函数里要调模型,Key 散落在环境变量、配置文件、代码里,换一个模型就要改一遍;第三,本地调试和线上部署的配置不一致,config.toml和settings.json各写一套,改到怀疑人生。
这篇就按「项目初始化 → 统一 Key 通道 → 可复制配置 → 启动验证 → 排障」的顺序走一遍,重点演示怎么用 TaoToken 把模型调用的 Key 和 API 通道统一管起来,让 FastMCP 服务里的工具函数不再关心「这次调的是哪个模型、走哪条通道」。全程可复制,跟着敲就能跑通。
2. 前置准备:TaoToken 统一 Key 与 API 通道
在写 MCP 服务之前,先把模型调用的「出口」定下来。MCP 服务里的工具函数经常需要调用大模型,比如代码审查工具、摘要工具、翻译工具。如果每个工具各自读环境变量、各自拼 base_url,后期维护会很痛苦。
TaoToken 在这里扮演的角色是统一的 API 通道:你只需要在控制台创建一个 Key,拿到一个统一的 base_url,之后所有模型调用都走这个入口。FastMCP 服务里不管有多少个工具要调模型,都复用同一份配置。
操作路径很直接:进控制台创建 API Key,然后确认接入文档里的 base_url 格式。Key 建议放在.env里,不要硬编码进mcp_server.py。
- 控制台创建 Key:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
- API Key 管理页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite
- 接入文档(确认 base_url 与请求格式):https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
注意:API 入口是
https://taotoken.net/api,不要在后面拼多余的路径,具体端点以接入文档为准。Key 只存在服务端环境变量里,别写进会提交到 Git 的文件。
如果你后面要做长期编码类 Agent,或者需要频繁切换模型做对比,可以顺带了解 Coding Plan,它更适合持续性的编码场景:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite
3. 可复制配置:config.toml 与 settings.json 骨架
先把项目结构定下来,避免后面文件乱放:
fastmcp-demo/ ├── mcp_server.py ├── config.toml ├── settings.json ├── .env └── data/ └── test.txt3.1 config.toml:服务与模型通道配置
config.toml放服务本身的参数和模型通道参数。模型部分统一指向 TaoToken 的 API 入口,Key 从环境变量注入,不写死在文件里。
[server] title = "FastMCP 统一通道服务" description = "基于 FastMCP 构建,模型调用走统一 API 通道" version = "1.0.0" host = "127.0.0.1" port = 8000 reload = true [model] # 统一 API 入口,具体端点以接入文档为准 base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" default_model = "claude-sonnet" timeout = 60 max_retries = 2 [logging] level = "INFO" file = "mcp_server.log"3.2 settings.json:客户端侧连接配置
settings.json给 MCP 客户端用,声明要连哪个服务、走什么传输方式。FastMCP 默认通过 SSE 暴露,路径是/sse。
{ "mcpServers": { "fastmcp-demo": { "url": "http://127.0.0.1:8000/sse", "transport": "sse", "timeout": 30000, "env": { "TAOTOKEN_API_KEY": "${TAOTOKEN_API_KEY}" } } } }3.3 .env:只放 Key
TAOTOKEN_API_KEY=你的Key.env必须进.gitignore。这一步别偷懒,Key 泄露的代价比多写一行忽略规则大得多。
3.4 CC Switch 切换步骤
如果你本地同时维护多个 MCP 服务或模型通道,用 CC Switch 做配置切换比手动改文件稳。步骤是:先把上面的settings.json存成一个 profile,命名比如fastmcp-demo;再为其他通道各存一个 profile;切换时选中目标 profile 生效,客户端重连即可。切换后建议重启一次 MCP 客户端,避免旧连接缓存了上一个服务的 SSE 会话。
4. 服务代码:把统一 Key 接进 FastMCP 工具
下面这份mcp_server.py把配置读取、模型调用、工具注册串起来。模型调用部分统一从config.toml读 base_url,从环境变量读 Key,工具函数本身不关心通道细节。
import os import tomllib from pathlib import Path import httpx from dotenv import load_dotenv from fastmcp import FastMCP load_dotenv() # 读取配置 cfg = tomllib.loads(Path("config.toml").read_text(encoding="utf-8")) BASE_URL = cfg["model"]["base_url"] API_KEY = os.environ.get(cfg["model"]["api_key_env"], "") DEFAULT_MODEL = cfg["model"]["default_model"] mcp = FastMCP( title=cfg["server"]["title"], description=cfg["server"]["description"], version=cfg["server"]["version"], ) def call_model(prompt: str, model: str = DEFAULT_MODEL) -> str: """统一模型调用入口,所有工具复用这一条通道""" if not API_KEY: raise RuntimeError("未读取到 API Key,请检查 .env") headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } payload = { "model": model, "messages": [{"role": "user", "content": prompt}], } with httpx.Client(timeout=cfg["model"]["timeout"]) as client: resp = client.post(f"{BASE_URL}/v1/chat/completions", headers=headers, json=payload) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] @mcp.tool() def summarize(text: str) -> str: """对输入文本做摘要,模型调用走统一通道""" return call_model(f"请用三句话总结以下内容:\n{text}") @mcp.tool() def review_code(code: str, language: str = "python") -> str: """代码审查工具,复用同一个模型通道""" prompt = f"你是{language}代码审查专家,指出问题并给优化建议:\n{code}" return call_model(prompt) @mcp.resource("data://{filename}") def read_data(filename: str) -> str: """暴露 data 目录下的文件给 AI 读取""" path = Path("data") / filename if not path.exists(): raise FileNotFoundError(f"{filename} 不存在") return path.read_text(encoding="utf-8") if __name__ == "__main__": mcp.run(host=cfg["server"]["host"], port=cfg["server"]["port"], reload=cfg["server"]["reload"])这里的关键设计是call_model只写一次。以后要换模型,改config.toml里的default_model就行;要换通道,改base_url;要换 Key,改.env。工具函数完全不用动。
5. 启动与请求验证:一次完整闭环
先装依赖:
pip install fastmcp httpx python-dotenv启动服务:
python mcp_server.py控制台出现MCP server running on http://127.0.0.1:8000就说明起来了。接着验证 SSE 端点是否可达:
curl -N http://127.0.0.1:8000/sse-N关闭缓冲,能持续看到事件流。如果只想确认握手,看到event: endpoint之类的输出即可,按 Ctrl+C 退出。
再用客户端调一次工具,验证模型通道是否真的通了:
pip install mcp-client mcp-client call --server http://127.0.0.1:8000 \ --tool summarize \ '{"text": "FastMCP 让 MCP 服务开发像写 FastAPI 一样简单。"}'成功时返回的是模型生成的摘要文本。如果这一步返回了内容,说明「FastMCP 服务 → 统一 Key → 模型通道」整条链路是通的。你也可以在模型对话页手动发一条同样的 prompt 做对照,确认通道行为一致:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite
6. 本篇常见错排查
报错一:ModuleNotFoundError: No module named 'tomllib'tomllib是 Python 3.11 才进标准库的。低于这个版本要么升级 Python,要么pip install tomli,然后把导入改成import tomli as tomllib。
报错二:RuntimeError: 未读取到 API Key说明.env没被读到。检查三点:.env是否和mcp_server.py同目录;load_dotenv()是否在读取环境变量之前调用;变量名是否和config.toml里的api_key_env完全一致,大小写敏感。
报错三:httpx.ConnectError或连接超时先确认base_url没写错,API 入口是https://taotoken.net/api,不要自己拼多余路径。再确认本机网络能正常访问该地址。如果服务本身能启动但工具调用超时,把config.toml里的timeout调大试试。
报错四:客户端连不上/sse常见原因是端口被占用或 host 写成了0.0.0.0但客户端用127.0.0.1连。本地开发统一用127.0.0.1。另外确认settings.json里的 url 结尾是/sse,少写这段路径会 404。
报错五:改了配置但行为没变reload=True只监听 Python 文件变化,不会监听config.toml。改完配置文件要手动重启服务。CC Switch 切换 profile 后也要重启客户端,否则旧 SSE 会话还在。
报错六:工具返回 401Key 失效或复制时带了空格。重新在控制台生成一个 Key,粘贴时注意首尾不要有空白字符。
7. 把通道固定下来,再谈扩展
跑通上面这套之后,你会发现 FastMCP 的扩展成本很低:加一个工具就是加一个@mcp.tool()函数,模型调用继续复用call_model。真正需要提前定死的是通道和配置结构——config.toml管服务与模型参数,settings.json管客户端连接,.env只管 Key。这三者分离之后,本地调试、切换通道、部署上线都不会互相打架。
下一步可以做的:把call_model抽成独立模块供多个 MCP 服务复用;给工具加统一的异常处理和日志;用 systemd 把服务做成开机自启。但在此之前,先用一次完整的summarize调用确认链路通了,比什么都重要。