1. 为什么我要用 FastMCP 写一个 stdio 服务端
MCP 协议服务端创建这件事,说穿了就是让大模型能调用你自己写的函数。你写一个 Python 函数,用@mcp.tool()装饰一下,大模型就能在对话里"看到"这个工具,并在需要时调用它。听起来很玄,实际跑一遍你会发现核心代码不到 50 行。
MCP(Model Context Protocol)是给大模型提供上下文和能力的标准协议。一个 MCP 服务端可以暴露三类东西:Resources(资源,类似文件数据供客户端读取)、Tools(工具,LLM 可调用的函数)、Prompts(提示模板)。对初次接触的开发者来说,Tools 是最直观的切入点——你写一个查天气的函数,模型就能帮你查天气。
stdio 通信是 MCP 最基础的传输方式:服务端通过标准输入接收 JSON-RPC 消息,通过标准输出返回结果。不需要开端口、不需要网络配置,客户端启动一个子进程就能通信。这也是为什么我建议从 stdio 入手——它把"协议"和"网络"两件事拆开了,你只需要关心协议本身。
这篇文章面向的是第一次接触 MCP 协议的开发者。我会带你从零搭一个可被大模型调用的 stdio 服务端,给出完整可复制的代码、依赖安装命令、本地联调验证步骤,最后说明如何通过 TaoToken 统一 Key/API 通道接入模型完成端到端调用测试。你不需要事先了解 JSON-RPC,也不需要懂异步编程的细节,跟着敲一遍就能跑通。
我试过用最朴素的方式(手写 JSON-RPC 消息)去理解 MCP,结果被消息格式和握手流程绕晕了。FastMCP 把这个过程封装成了装饰器,你只需要关注业务逻辑。实测下来,从装环境到看到工具被调用,大概 20 分钟。
2. 环境准备与 TaoToken 前置配置
2.1 依赖安装:Python 3.10+ 与 uv
MCP Python SDK 要求 Python 3.10 或更高版本。先确认版本:
python3 --version # 期望输出:Python 3.10.x 或更高如果版本不够,建议用 uv 管理 Python 版本,它比手动装省事。安装 uv:
curl -LsSf https://astral.sh/uv/install.sh | sh安装完成后重新加载 shell 配置,然后验证:
uv --version # 期望输出:uv 0.x.x2.2 创建项目并安装 MCP SDK
uv init weather-mcp cd weather-mcp uv venv source .venv/bin/activate uv add "mcp[cli]" httpx touch weather.pymcp[cli]会装上 MCP SDK 和命令行工具,httpx用来发异步 HTTP 请求。装完后可以用uv run mcp version确认 SDK 版本,建议 1.2.0 以上。
2.3 通过 TaoToken 统一 Key/API 通道接入模型
服务端写完后,你需要一个能调用 MCP 工具的模型客户端来做端到端测试。这里用 TaoToken 的统一 API 通道,好处是一个 Key 能对接多种模型,不用为每个模型单独配环境。
先在 TaoToken 控制台创建 API Key:访问 API Keys 管理页,新建一个 Key 并复制保存。然后记下两个地址:
- Base URL:
https://taotoken.net/api - API Key:你刚创建的那串
如果你打算长期做编码类 Agent 开发,可以了解下 Coding Plan,它针对高频调用场景做了额度优化。只是想先验证模型能不能调通工具,用按量计费的 Key 就够了。
模型 ID 方面,TaoToken 支持多种主流模型,具体可用列表在 模型对话页 能看到。选一个支持 function calling 的模型即可,比如claude-sonnet-4-20250514或gpt-4o。
3. 可复制的 FastMCP 服务端配置与代码
3.1 服务端完整代码
把下面内容写入weather.py。这段代码定义了两个工具:get_alerts查美国某州的天气预警,get_forecast查某经纬度的天气预报。
from typing import Any import httpx from mcp.server.fastmcp import FastMCP # 初始化 FastMCP 服务端,名字叫 weather mcp = FastMCP("weather") NWS_API_BASE = "https://api.weather.gov" USER_AGENT = "weather-app/1.0" async def make_nws_request(url: str) -> dict[str, Any] | None: """请求 NWS API,带错误处理。""" headers = { "User-Agent": USER_AGENT, "Accept": "application/geo+json", } async with httpx.AsyncClient() as client: try: response = await client.get(url, headers=headers, timeout=30.0) response.raise_for_status() return response.json() except Exception: return None def format_alert(feature: dict) -> str: """把预警数据格式化成可读字符串。""" props = feature["properties"] return f""" Event: {props.get('event', 'Unknown')} Area: {props.get('areaDesc', 'Unknown')} Severity: {props.get('severity', 'Unknown')} Description: {props.get('description', 'No description available')} Instructions: {props.get('instruction', 'No specific instructions provided')} """ @mcp.tool() async def get_alerts(state: str) -> str: """获取美国某州的天气预警。 Args: state: 两个字母的州代码,例如 CA、NY """ url = f"{NWS_API_BASE}/alerts/active/area/{state}" data = await make_nws_request(url) if not data or "features" not in data: return "无法获取预警数据。" if not data["features"]: return "该州当前没有活跃预警。" alerts = [format_alert(feature) for feature in data["features"]] return "\n---\n".join(alerts) @mcp.tool() async def get_forecast(latitude: float, longitude: float) -> str: """获取某经纬度的天气预报。 Args: latitude: 纬度 longitude: 经度 """ points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}" points_data = await make_nws_request(points_url) if not points_data: return "无法获取该位置的预报数据。" forecast_url = points_data["properties"]["forecast"] forecast_data = await make_nws_request(forecast_url) if not forecast_data: return "无法获取详细预报。" periods = forecast_data["properties"]["periods"] forecasts = [] for period in periods[:5]: forecast = f""" {period['name']}: Temperature: {period['temperature']}°{period['temperatureUnit']} Wind: {period['windSpeed']} {period['windDirection']} Forecast: {period['detailedForecast']} """ forecasts.append(forecast) return "\n---\n".join(forecasts) if __name__ == "__main__": mcp.run(transport="stdio")3.2 关键点解析:@mcp.tool() 装饰器做了什么
@mcp.tool()是 FastMCP 的核心。它把普通 Python 函数注册成 MCP 工具,并自动从类型提示和 docstring 生成工具的输入 schema。也就是说,你写的state: str和那段 docstring,会被转成模型能理解的参数说明。
装饰器内部逻辑大致是:接收函数,调用self.add_tool(fn, name=name, description=description)注册,然后返回原函数。注意它必须写成@mcp.tool()带括号的形式,直接写@mcp.tool会抛TypeError,因为装饰器需要先被调用一次。
3.3 客户端配置:让模型客户端启动这个服务端
stdio 服务端不是常驻进程,它由客户端按需启动。以 Claude Desktop 为例,配置文件路径:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
写入以下 JSON(把路径换成你自己的绝对路径):
{ "mcpServers": { "weather": { "command": "uv", "args": [ "--directory", "/ABSOLUTE/PATH/TO/weather-mcp", "run", "weather.py" ] } } }这段配置告诉客户端:有一个叫weather的 MCP 服务端,通过uv --directory ... run weather.py启动它。改完配置要重启客户端。
如果你用的是支持 OpenAI 兼容接口的客户端(比如 Cline、Continue 等),配置里需要同时填三件套:
{ "mcpServers": { "weather": { "command": "uv", "args": ["--directory", "/ABSOLUTE/PATH/TO/weather-mcp", "run", "weather.py"] } }, "llm": { "baseUrl": "https://taotoken.net/api", "apiKey": "你的 TaoToken Key", "modelId": "claude-sonnet-4-20250514" } }Base URL、Key、Model ID 三者缺一不可。Base URL 固定为https://taotoken.net/api,不要加 UTM 参数。
4. 验证请求与成功结果
4.1 本地 stdio 联调:用 mcp dev 检查工具注册
在启动客户端之前,先用 SDK 自带的调试工具确认服务端能正常握手:
uv run mcp dev weather.py这会启动一个本地调试界面,列出服务端注册的所有工具。如果看到get_alerts和get_forecast两个工具,说明服务端代码没问题。这一步能提前排掉大部分语法和注册错误。
4.2 直接跑服务端看 stdio 输出
也可以直接运行,观察它是否在等待 stdin:
uv run weather.py正常情况下进程会挂起,等待标准输入。此时它已经准备好接收 JSON-RPC 消息了。按 Ctrl+C 退出。
4.3 端到端调用:让模型调用工具
重启客户端后,在对话里输入:
帮我查一下加州现在的天气预警
模型会分析可用工具,选择get_alerts,传入state="CA"。客户端通过 stdio 把调用请求发给服务端,服务端请求 NWS API 拿到数据,格式化后返回。整个链路是:
- 客户端把问题发给模型
- 模型决定调用
get_alerts,生成参数 - 客户端通过 stdio 把工具调用发给服务端
- 服务端执行函数,返回结果
- 客户端把结果回传给模型
- 模型组织自然语言回复
如果模型回复里包含具体的预警事件和区域描述,说明端到端跑通了。第一次调用时客户端通常会弹窗询问是否授权,点允许即可。
5. 本篇常见错误排查
5.1 401 Unauthorized:Key 或 Base URL 配错
报错长这样:
Error: 401 Unauthorized - invalid api key原因通常是三件套里某一项写错。检查顺序:Base URL 必须是https://taotoken.net/api,结尾不要多斜杠;API Key 复制时别带空格;Model ID 要和 TaoToken 支持的模型名完全一致。如果用的是 Claude Desktop 这类不直接暴露 LLM 配置的客户端,401 一般来自它自己的账号体系,和 MCP 服务端无关。
5.2 local proxy failed / connection refused
MCP error: local proxy failed to connect这通常是客户端启动服务端子进程失败。检查 JSON 配置里的--directory路径是不是绝对路径,Windows 下要用双反斜杠或正斜杠。另外确认uv在 PATH 里,客户端可能读不到你 shell 里的环境变量,必要时把command写成uv的绝对路径。
5.3 reading 'choices' 报错:响应结构不符
TypeError: Cannot read properties of undefined (reading 'choices')这个报错说明客户端拿到的响应不是标准 OpenAI 格式。常见原因是 Base URL 填成了网页地址而不是 API 地址。确认填的是https://taotoken.net/api,不是带/chat的页面地址。如果客户端要求填完整 endpoint,用https://taotoken.net/api/v1/chat/completions。
5.4 OAuth 相关报错
OAuth error: invalid_client部分客户端默认走 OAuth 流程,但 TaoToken 用的是 API Key 鉴权。在客户端设置里把认证方式从 OAuth 改成 API Key,填入你的 Key 即可。如果客户端没有这个选项,检查是不是选错了接入模式。
5.5 工具注册了但模型不调用
服务端日志显示工具已注册,但模型就是不调用。先确认模型本身支持 function calling,部分轻量模型不支持。其次检查 docstring 是否清晰——模型靠 docstring 判断什么时候用这个工具。把"""获取美国某州的天气预警。"""写得更具体,比如加上"当用户询问某地天气状况或预警时使用",命中率会明显提升。
6. 继续往下走:从 stdio 到更完整的接入
跑通 stdio 之后,你已经掌握了 MCP 服务端的核心:用 FastMCP 注册工具、用 stdio 传输、用客户端联调。接下来可以做的事:
把服务端换成你自己的业务逻辑,比如查数据库、调内部 API、读本地文件。工具函数的写法完全一样,只是把make_nws_request换成你的请求逻辑。
如果你想让服务端被更多客户端复用,可以了解 streamable HTTP 传输方式,它适合部署成常驻服务。但 stdio 依然是本地开发调试最快的路径。
端到端测试时如果遇到模型侧的问题,可以到 模型对话页 直接验证 Key 和模型是否正常,排除掉 MCP 服务端的干扰。接入细节和参数说明在 接入文档 里有完整对照表。
最后一个实用技巧:调试 stdio 服务端时,不要用print()输出日志,因为 stdout 被 JSON-RPC 占用了,任何多余输出都会破坏协议。要打日志就写到 stderr,或者用logging模块配置到文件。这个坑我第一次写的时候踩过,服务端看起来在跑,但客户端一直握手失败,排查了半天才发现是一行 print。