1. 为什么要在本地跑一个 stdio 版 MCP Server
MCP Server 说白了就是给大模型外挂的一双手:模型本身只能吐文字,但通过 MCP 协议,它能调用你本地定义好的函数,去查数据库、读文件、调第三方 API。而 stdio 传输方式是最省事的一种——客户端把 Server 当成一个子进程拉起来,双方通过标准输入输出对话,不需要开端口、不需要配网络、不需要证书。对于想快速验证「我的工具能不能被模型发现并调用」的开发者来说,这是最短路径。
这篇要做的,就是用 FastMCP 加 UV,从空目录开始搭一个最小可用的 MCP Server,跑通「初始化项目 → 写工具函数 → 配客户端 → 本地启动 → 验证一次工具调用」这条完整链路。适合已经装好 Python、想动手跑通第一个 MCP 服务的人。全程本地,不涉及任何网络穿透配置,命令复制粘贴就能用。
我试过把工具函数写得花里胡哨,结果客户端根本发现不了,后来才发现问题出在 docstring 上——这个后面排障章节会细说。
2. 前置准备:UV 与 FastMCP 的分工
UV 是 Rust 写的 Python 包管理和虚拟环境工具,速度比 pip 快一个量级,而且它自带uv run这种「不用手动激活虚拟环境」的执行方式,特别适合 MCP Server 这种「客户端拉起子进程」的场景——客户端只需要执行uv run main.py,UV 自己会把依赖装好、环境切好。
FastMCP 是mcp[cli]包里封装好的高层 API,它把协议里那些 JSON-RPC 的握手、能力声明、工具注册都藏起来了,你只要写普通 Python 函数,加个@mcp.tool()装饰器,它就自动变成一个可被模型调用的工具。两者配合,一个管环境,一个管协议,你只管写业务逻辑。
如果你还没装 UV,先执行全局安装:
pip install uv装完可以用uv --version确认一下。这里不需要额外配置镜像源,UV 默认会走 PyPI。
3. 从零初始化项目并写 Server 骨架
3.1 初始化目录与添加依赖
先建项目:
uv init mcp-server cd mcp-serveruv init会生成pyproject.toml、main.py等基础文件。接着加依赖,这里我用一个公开的第三方库做演示工具,方便你看到真实返回值:
uv add "mcp[cli]" httpxmcp[cli]带上了命令行调试工具,httpx用来发 HTTP 请求。装完后pyproject.toml里会自动记录这两个依赖,UV 也会生成uv.lock锁定版本。
3.2 写一个最小工具函数
打开main.py,替换成下面这段:
from mcp.server.fastmcp import FastMCP import httpx mcp = FastMCP("Demo MCP Server") @mcp.tool() def fetch_weather(city: str) -> dict: """查询指定城市的天气概况。 Args: city: 城市名称,例如 "beijing"。 Returns: 包含城市名和天气描述的字典。 """ # 这里用一个公开的示例接口,实际项目替换成你自己的数据源 url = f"https://wttr.in/{city}?format=j1" resp = httpx.get(url, timeout=10) data = resp.json() current = data["current_condition"][0] return { "city": city, "temp_c": current["temp_C"], "desc": current["weatherDesc"][0]["value"], } if __name__ == "__main__": mcp.run(transport="stdio")几个关键点:FastMCP("Demo MCP Server")里的名字是给客户端看的服务标识;@mcp.tool()装饰器把函数注册成工具;函数签名里的类型注解和 docstring 会被 FastMCP 转成工具的输入 schema 和描述,模型就是靠这些信息判断「什么时候该调这个工具」。transport="stdio"表示走标准输入输出,这是本地场景的默认选择。
3.3 客户端配置片段
以支持 MCP 的客户端为例,配置通常长这样(不同客户端字段名略有差异,核心是 command、args、transport):
{ "mcpServers": { "demo-server": { "command": "uv", "args": [ "--directory", "/绝对路径/mcp-server", "run", "main.py" ], "transportType": "stdio", "timeout": 60 } } }--directory后面必须写你项目的绝对路径,Windows 下类似D:\\workspace\\mcp-server。timeout给 60 秒足够,因为 UV 首次运行可能要解析依赖。保存后客户端左侧会出现这个服务,绿色代表已启动。
4. 本地启动与一次工具调用验证
4.1 先用命令行确认 Server 能起来
在项目目录下直接跑:
uv run main.py如果没有任何报错、进程挂在那里等待输入,说明 Server 已经通过 stdio 待命了。按Ctrl+C退出。这一步能排除掉 90% 的环境问题——如果这里就报ModuleNotFoundError,说明依赖没装好,回到uv add那步。
4.2 用 MCP Inspector 做一次真实调用
mcp[cli]自带调试工具,执行:
uv run mcp dev main.py它会启动一个本地调试界面,在浏览器里打开后,你能看到fetch_weather这个工具被列出来了。点进去,在参数框填beijing,点运行,右侧会返回类似:
{ "city": "beijing", "temp_c": "24", "desc": "Partly cloudy" }看到这个返回,就证明工具被正确发现、参数被正确解析、函数被正确执行。这一步是整个流程里最关键的验证动作,比在客户端里问模型更直接。
4.3 在客户端里让模型调用
回到客户端,确保服务是绿色状态,然后直接问:「帮我查一下北京现在的天气」。模型会先输出一段「我来调用工具」的思考,然后触发fetch_weather,把返回的 JSON 渲染成自然语言。如果模型说「我没有查询天气的能力」,八成是工具没被发现,往下看排障。
5. 本篇常见错误排查
5.1 客户端里服务一直红色或启动失败
最常见的原因是--directory路径写错,或者路径里有空格没转义。先在终端手动执行一遍配置里的完整命令:
uv --directory /你的绝对路径/mcp-server run main.py能跑通再回客户端。另外 Windows 下路径分隔符要用双反斜杠或正斜杠。
5.2 工具列表是空的
FastMCP 靠 docstring 生成工具描述,如果函数没有 docstring,或者 docstring 格式混乱,某些客户端会直接忽略这个工具。确保每个@mcp.tool()函数都有清晰的Args:和Returns:段落。另外装饰器必须写在函数正上方,中间不能插别的语句。
5.3 调用时报参数类型错误
类型注解要和实际使用一致。比如你写city: str,客户端传进来的就是字符串;如果你写count: int但客户端传了"3",FastMCP 会尝试转换,转不了就报错。参数名也要和 docstring 里写的一致,模型是照着 docstring 填参数的。
5.4 首次调用特别慢
UV 第一次run时要解析并下载依赖,可能花十几秒。把客户端timeout调到 60 以上,或者提前在终端跑一次uv run main.py把依赖缓存好。
6. 把工具接进真实工作流
跑通最小示例后,你可以把fetch_weather换成任何真实逻辑:读本地 SQLite、查内部 API、操作文件。只要保持「函数 + 类型注解 + docstring +@mcp.tool()」这个结构,FastMCP 就能把它暴露给客户端。
如果你打算长期在编码或 Agent 场景里用 MCP,建议把常用工具集中管理,避免每个项目重复配置。需要生成和管理调用凭证时,可以到 TaoToken API Keys 创建,接入细节参考 TaoToken 接入文档。想先验证模型对工具的调用效果,用 模型对话 快速试;如果是长期编码或 Agent 工作流,Coding Plan 更合适。官网入口在 taotoken.net。
最后留个实用习惯:每次改完工具函数,先在uv run mcp dev main.py里点一遍,确认返回正常,再去客户端问模型。这样能把「工具本身的问题」和「模型理解的问题」分开,排障效率高很多。