1. 为什么你的 Python 工具写完了,AI 却还是“看不见”
很多人第一次接触 MCP Server 时,都会经历一个心理落差:代码写完了,python server.py也能跑起来,终端里安安静静没有报错,可一打开 Cursor 或 Claude Desktop,问它“帮我查一下上海天气”,它还是自顾自地编一段回答,完全不碰你写的函数。
问题不在你的 Python 代码,而在于客户端根本不知道这个 Server 存在。MCP(Model Context Protocol)的本质,是给 AI 客户端和本地工具之间定一套“发现—描述—调用”的协议。你的 Server 只是把工具“挂”了出来,但 Cursor 和 Claude Desktop 需要被明确告知:去哪里启动它、用什么命令启动、启动后能拿到哪些工具。
我试过把整个链路拆开看,它其实是这样的:
用户提问 → Claude Desktop / Cursor → MCP Client → MCP Server → Python Tool → 数据库/API/文件 → 返回结果 → AI 组织自然语言
真正执行 Python 代码的是 MCP Server,AI 只负责“决定什么时候调用哪个工具”。所以接入的核心动作只有两个:把 Server 注册进客户端配置,以及验证 AI 确实触发了你的函数。这篇就围绕 FastMCP 写的 Python 工具,把 Cursor 和 Claude Desktop 两条接入路径都走一遍,每一步都给可复制的配置和验证方法。
适合谁看:已经用 FastMCP 写过至少一个@app.tool()的 Python 开发者;想让 Cursor 里的 AI 真正调用本地脚本的人;以及准备把内部系统封装成 MCP 工具、但卡在“接不上客户端”这一步的工程同学。
需要提前说明一点:MCP Server 跑在本地,客户端通过标准输入输出或本地命令与它通信,不涉及任何网络穿透类操作。你只需要保证 Python 环境可用、脚本路径正确即可。
2. 用 FastMCP 把 Python 工具封装成 MCP Server 的完整写法
在接入客户端之前,先把 Server 本身写扎实。FastMCP 的好处是把协议细节都藏起来了,你只需要关心“工具函数长什么样”。但要让 AI 正确选择工具,函数命名、类型标注、docstring 这三样一个都不能省。
先装依赖。FastMCP 现在有独立包,也可以直接用官方mcp包里的 FastMCP:
pip install fastmcp # 或者 pip install mcp下面是一个可以直接跑的server.py,我放了两个工具:一个做加法,一个查天气(先用假数据,方便验证调用链路):
from mcp.server.fastmcp import FastMCP app = FastMCP("DemoTools") @app.tool() def add(a: int, b: int) -> int: """计算两个整数之和,用于数学运算类请求。""" return a + b @app.tool() def get_weather(city: str) -> str: """查询指定城市的当前天气,输入为城市中文名。""" fake = {"上海": "晴,30℃", "北京": "多云,26℃"} return fake.get(city, f"{city} 暂无数据") if __name__ == "__main__": app.run()启动它:
python server.py如果终端没有报错、进程保持运行,说明 Server 已经就绪。这里有个容易被忽略的点:@app.tool()装饰器是工具被发现的唯一入口。如果你写了函数却忘了加装饰器,AI 那边永远看不到它,后面配置再正确也没用。
工具暴露给 AI 的其实是这样一段结构化描述:
{ "name": "get_weather", "description": "查询指定城市的当前天气,输入为城市中文名。", "inputSchema": { "city": "string" } }AI 就是靠name和description判断“什么时候该调用它”。所以def test():这种命名和空 docstring 是灾难,模型根本不知道它能干什么。反过来,get_weather加上一句清晰描述,命中率会高很多。
再补一个稍微贴近真实业务的工具,把数据库查询封装成固定用途函数,而不是让模型直接执行任意 SQL:
@app.tool() def query_customer(customer_id: int) -> dict: """根据客户 ID 查询客户基本信息,返回姓名与等级。""" # 实际项目里替换为你的 DB 查询 return {"id": customer_id, "name": "张三", "level": "VIP"}这种写法的好处是权限边界清晰:模型只能调用你允许的固定查询,不能拼 SQL。企业项目里这一点比“功能强大”更重要。
Server 写完后,建议先在命令行确认它能正常启动、不依赖任何客户端。因为后面 90% 的接入失败,根源都在“Server 本身没跑起来”或“路径写错”。
3. Cursor 与 Claude Desktop 的 MCP 配置片段(可直接复制)
这一节是重点,两个客户端的配置文件格式不同,但核心三要素一致:启动命令、脚本路径、Server 名称。任何一处写错,客户端都会静默失败或报 “No MCP Server”。
3.1 Claude Desktop 配置
Claude Desktop 通过一个 JSON 配置文件注册本地 MCP Server。不同系统路径不同:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
打开(没有就新建)后写入:
{ "mcpServers": { "demo-tools": { "command": "python", "args": [ "D:/mcp/demo/server.py" ] } } }三个字段的含义:
| 字段 | 作用 | 常见错误 |
|---|---|---|
demo-tools | Server 名称,客户端内显示用 | 重名会互相覆盖 |
command | 启动命令 | 写成python3但系统只有python |
args | 脚本绝对路径 | 用相对路径导致找不到文件 |
保存后完全退出并重启 Claude Desktop(不是关窗口,是退出进程)。重启时它会自动拉起这个 Server,并发现add、get_weather、query_customer三个工具。
如果 Python 不在系统 PATH 里,command要写绝对路径,比如 Windows 下"C:/Python311/python.exe"。这是新手最常踩的坑之一。
3.2 Cursor 配置
Cursor 的 MCP 配置入口在设置里,不同版本位置略有差异,通常在Settings → MCP或Features → MCP Servers,选择 “Add Server”。它同样支持 JSON 配置,格式与 Claude Desktop 接近:
{ "mcpServers": { "demo-tools": { "command": "python", "args": [ "D:/mcp/demo/server.py" ] } } }如果你用的是较新版本,Cursor 也支持在项目根目录放.cursor/mcp.json,这样配置可以跟着项目走,团队协作时更方便。写入后重启 Cursor,在 MCP 面板里应该能看到demo-tools处于已连接状态,展开后列出三个工具。
这里要强调一个完整配置的“三件套”概念:无论 Claude Desktop 还是 Cursor,一个可用的 MCP 接入都必须同时具备Base URL(本地场景即启动命令)、Key(本地场景通常不需要,远程 Server 才涉及)、Model ID(客户端里选用的模型)。本地 stdio 模式下,Key 一般留空,但如果你接的是远程 MCP 服务,就需要在配置里补上鉴权信息。很多教程只贴一半配置,导致读者接不上,问题就出在这。
配置完成后,两个客户端的行为是一致的:启动时自动拉起 Server,读取工具列表,之后在对话中按需调用。你不需要手动“连接”,客户端会管理生命周期。
4. 验证 AI 真的调用了你的 Python 函数
配置写完不代表成功,必须做一次可观测的调用验证。否则你无法区分“AI 调用了工具”和“AI 自己编了答案”。
最直接的验证方式,是在工具函数里加一行打印,让调用留下痕迹:
@app.tool() def add(a: int, b: int) -> int: """计算两个整数之和,用于数学运算类请求。""" print(f"[TOOL CALLED] add({a}, {b})") return a + b然后在 Claude Desktop 或 Cursor 里输入:
帮我计算 12345 加 67890
如果接入成功,你会看到两件事同时发生:客户端返回80235,同时运行 Server 的终端里打印出[TOOL CALLED] add(12345, 67890)。这行打印就是“AI 真正触发了 Python 函数”的铁证。
再验证天气工具:
上海今天多少度?
预期终端打印[TOOL CALLED] get_weather(上海),客户端回答里出现“晴,30℃”。如果 AI 回答的是“我无法获取实时天气”,说明工具没被发现;如果它编了一个温度,说明它没调用工具而是自己生成。
这里有个细节值得注意:AI 内部流程是“理解需求 → 发现可用工具 → 选择get_weather→ 传入city="上海"→ 拿到返回值 → 组织自然语言”。整个过程用户无感知,但后台函数确实执行了。这也是 MCP 相比“让模型直接写代码”的核心价值——执行发生在你可控的 Python 环境里,而不是模型的黑盒里。
验证通过后,你可以把假数据换成真实 API 或数据库查询,链路不用改。工具层、业务层、数据访问层分离,后续维护会轻松很多。
5. 接入失败排查:401、local proxy failed、reading choices、OAuth 对照表
接入阶段最常见的不是代码错,而是配置和环境的错。下面按真实报错逐条对照。
报错一:No MCP Server或工具列表为空
这是最高频的问题。原因通常是三类:Python 路径错误、脚本路径错误、环境变量未生效。排查顺序是先在命令行手动执行python D:/mcp/demo/server.py,确认能启动;再把配置里的command换成 Python 绝对路径;最后确认 JSON 没有多余逗号。JSON 语法错误会导致整个配置被忽略,且客户端往往不报明确错误。
报错二:401 Unauthorized
本地 stdio 模式一般不会出现 401。如果你接的是远程 MCP 服务,401 说明鉴权信息缺失或过期。检查配置里是否带了正确的 Key,以及 Key 是否已失效。远程场景下 Base URL、Key、Model ID 三件套缺一不可。
报错三:local proxy failed
这个报错通常出现在客户端尝试通过本地代理连接 Server 时。检查是否有其他进程占用了同一端口,或配置里误加了代理相关字段。本地 stdio 模式不需要任何代理设置,把多余字段删掉即可。
报错四:reading choices相关错误
这类错误多出现在模型返回结构解析阶段,常见于客户端版本与模型接口不匹配。先升级 Cursor / Claude Desktop 到最新版,再确认所选模型 ID 正确。如果配置里 Model ID 写错,客户端可能拿到非预期响应。
报错五:OAuth 相关报错
部分远程 MCP 服务要求 OAuth 授权。如果报 OAuth 失败,检查回调地址是否与注册时一致,以及授权是否已过期。本地工具不需要 OAuth,遇到这个报错说明你接的是远程服务,按服务方文档重新授权即可。
报错六:AI 始终不调用工具
配置没问题、工具也发现了,但 AI 就是不用。这几乎总是描述问题。把def test():改成def get_weather(city: str):,并补上 docstring。函数名要能自解释,参数类型要标注,描述要说清“什么时候用”。模型选择工具靠的就是这些元信息。
排查时建议养成一个习惯:每改一次配置就重启客户端,并观察 Server 终端输出。有打印就说明链路通了,没打印就往配置和路径上找。这套方法能覆盖绝大多数接入问题。
6. 把工具接上之后,下一步怎么走
走到这里,你的 FastMCP Server 应该已经能在 Cursor 和 Claude Desktop 里被真实调用了。回头看,真正卡住大多数人的从来不是 Python 代码,而是“客户端不知道 Server 在哪”这一层配置。把配置写对、把验证做扎实,AI 就从“会聊天”变成了“能干活”。
如果你还想继续扩展,几个方向比较实用:把数据库查询、文件操作、内部 API 都封装成固定用途的工具,让模型在权限边界内调用;给工具加统一的异常处理和超时,避免一个慢查询拖垮整个对话;敏感信息走环境变量,不要写进代码或配置。
需要提醒的是,MCP 工具应该封装成明确的业务动作,而不是把生产库的任意 SQL 执行权交给模型。工具层做窄、做清晰,权限和审计才好落地。
接入过程中如果卡在配置或鉴权上,可以直接对照官方文档排查,也可以到控制台里管理你的 Key 和接入信息。把本地工具接上 AI 客户端只是第一步,后面把更多业务能力以 MCP 工具的形式暴露出来,AI Agent 能承担的事情会越来越多。