MCP踩坑实录
我花了 3 天,踩了 12 个坑,才真正搞懂 MCP 是什么
不是教程搬运,是一份「从似懂非懂到能讲清楚」的踩坑实录。
如果你也看过 MCP 的介绍、跟着跑过代码、但合上电脑还是说不清它到底是个啥——这篇是写给你的。
一、先说那个让我破防的瞬间
三天前,我坐在电脑前,屏幕上是一个孤零零的提示符:
>
我知道这是 MCP 客户端的交互界面,我刚刚跑通了「连接服务器、发现工具、调用工具」的全流程。教程说,输入问题就能调用工具。
于是我敲了:
add 3 5
回车。
收到输入: add 3 5
它把我的话原样复读了一遍。
没有 8,没有报错,什么都没有。
那一刻我突然意识到:我跑通了所有代码,但我根本不知道这条链路上到底是谁在干活。
二、MCP 到底是什么?一句话版本
如果你只想知道定义,记住这个类比就够了:
MCP 是 AI 世界的 USB-C。
在 USB-C 之前,每个手机一个充电口,出差要带一包线。
在 MCP 之前,每个 AI 工具一套接口,OpenAI 一套、Claude 一套、通义一套。
有了 USB-C,线不用管插的是手机还是显示器。
有了 MCP,工具不用管对面是 Claude 还是 Cursor。
所以:
- MCP 不是框架,它不帮你写代码
- MCP 不是服务,它不是一个要部署的东西
- MCP 是一份约定:约定「AI 应用」和「工具」之间的话怎么说
底层用的是 JSON-RPC 2.0,一条消息长这样:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "add", "arguments": { "a": 3, "b": 5 } }
}
就这么朴素。没有任何黑魔法。
三、三个角色,别搞混
这是最容易糊的地方:
| 角色 | 是什么 | 例子 |
|---|---|---|
| Host | 你打开的软件 | Claude Desktop、Cursor、VS Code |
| Client | 软件里的一个组件,管一条连接 | 代码里的 ClientSession |
| Server | 提供工具的一方 | 你自己写的 server.py |
Host 是人用的软件,Client 是软件里的一根线,Server 是线另一头的插座。
你在 Cursor 里装了 3 个 MCP 插件 = 1 个 Host + 3 个 Client + 3 个 Server。
四、Server 能提供三种东西,但 90% 的情况你只需要一种
| 类型 | 谁决定用它 | 类比 |
|---|---|---|
| Tools | 模型自己决定 | AI 的手 |
| Resources | 应用决定要不要塞给模型 | AI 的眼睛 |
| Prompts | 用户主动选 | 话术模板 |
实际项目里,Tools 占了 90%。先把 Tool 吃透,另外两个知道概念就行。
五、20 行代码,跑通第一个 Server
别急着上框架,先看最小可运行版本:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Hello MCP Server")
@mcp.tool()
def add(a: int, b: int) -> int:
"""将两个数字相加,返回结果"""
return a + b
@mcp.tool()
def get_weather(city: str, unit: str = "celsius") -> str:
"""查询指定城市的天气"""
return f"{city} 的天气:22°C"
if __name__ == "__main__":
mcp.run()
然后:
uv run mcp dev server.py
浏览器打开 localhost:6274,在 Tools 里点 add,填 a=3, b=5,Run。
返回 8。
注意:你一行 JSON 都没写,一行参数解析都没写。
@mcp.tool() 这行装饰器干了三件事:
- 把函数注册进工具表
- 读取
a: int, b: int类型注解,自动生成 JSON Schema - 把 docstring 当成工具描述
这就是 MCP 最爽的地方:你写普通函数,得到 AI 能调用的工具。
六、一个关键认知:AI 从没见过你的代码
这是我想了很久才想明白的一点。
模型看到的,只有这段 JSON:
{
"name": "add",
"description": "将两个数字相加,返回结果",
"inputSchema": {
"properties": {
"a": { "type": "integer" },
"b": { "type": "integer" }
},
"required": ["a", "b"]
}
}
它只靠这段文字判断:用户这句话该不该调这个工具、参数怎么填。
所以——
docstring 不是给人看的注释,是给模型看的说明书。
工具描述写得好不好,直接决定 AI 聪不聪明。这也是为什么很多教程反复强调「description 要清晰」,但很少有人说清为什么。
七、踩坑实录:12 个坑,一个都没少踩
这部分才是本文的价值。以下全是真实报错,不是编的。
坑 1:python: Python was not found
明明装了 Python,CMD 里就是找不到。
原因:PATH 里第一个命中的是 WindowsApps\python.exe——微软应用商店的假别名,不是真解释器。
解法:删掉别名,或者直接用 py。
Remove-Item "$env:LOCALAPPDATA\Microsoft\WindowsApps\python.exe" -Force
坑 2:'mcp' 不是内部或外部命令
pip install mcp 明明成功了。
原因:包装在 conda 环境里,但 CMD 没走 conda 初始化,Scripts 目录不在 PATH。
解法:用 PowerShell,或者直接走项目 .venv。
坑 3:Error: typer is required
mcp --help
Error: typer is required. Install with 'pip install mcp[cli]'
原因:mcp 只装了核心库,CLI 是可选依赖。
解法:
pip install "mcp[cli]"
引号必须加,不然 PowerShell 会把 [cli] 当通配符。
坑 4:No module named 'mcp.server.fastmcp'
这个坑最阴。
原因:项目里装的是 mcp 2.3.0,而代码是 1.x 写法。2.x 把 FastMCP 改名成了 MCPServer,路径也变了。
This is mcp 2.x, where FastMCP was renamed to MCPServer
解法:锁版本。
uv add "mcp[cli]<2"
教训:看到
ModuleNotFoundError但包明明装了,第一反应应该是查版本,不是查拼写。
坑 5:IndentationError: expected an indented block
从 PDF 复制代码,缩进全没了,每行还带着一个空格。
解法:粘贴完先跑一遍语法检查。
uv run python -m py_compile server.py
没有输出 = 语法正确。 这个习惯能省你一半的调试时间。
坑 6:跑 server.py 一片空白,以为卡死了
$ uv run python server.py
(光标停着,什么都不输出)
真相:stdio 模式本来就是没有输出的,它在等你(或客户端)发消息。
这不是卡死,是正常工作。
坑 7:Invalid JSON: input_value='\n'
ERROR Received exception from stream:
Invalid JSON: EOF while parsing a value at line 2 column 0
input_value='\n'
原因:我在服务端窗口按了一下回车。
stdio 模式下,终端输入就是客户端的输入。一个空行会被当成 JSON 解析——当然失败。
解法:什么都别输。要退出按 Ctrl+C。
坑 8:Inspector 连不上,提示 proxy token is correct
原因:打开的是上次的旧页面,token 早就失效了。
解法:用终端打印的完整地址(带 ?MCP_PROXY_AUTH_TOKEN=...),或者直接关掉认证:
set DANGEROUSLY_OMIT_AUTH=true
坑 9:Proxy Server PORT IS IN USE AT 6277
上次的 Inspector 没退干净。
解法:
netstat -ano | findstr :6277
taskkill /F /PID <那个PID>
坑 10:Inspector 启动崩在 GlobalHeaders
TypeError: Cannot read properties of undefined (reading 'prototype')
原因:PATH 里第一个 node 是 16,新版 Inspector(基于 @hono/node-server)要 18+。
解法:
set PATH=D:\tools\nodejs;%PATH%
我机器上装了三个 node:16、22,还有一个藏在 JDK 目录里的。
where node一看便知。
坑 11:'coroutine' object does not support async with
教程写的是:
async with get_db() as db:
但 get_db() 是个 async def,返回的是协程对象,协程不能用 async with。
解法:加 @asynccontextmanager。
from contextlib import asynccontextmanager
@asynccontextmanager
async def get_db():
db = await aiosqlite.connect(DATABASE_PATH)
db.row_factory = aiosqlite.Row
try:
yield db
finally:
await db.close()
记住这个模式:凡是「拿到资源 → 用一下 → 必须释放」的东西,都该这么写。
坑 12:LangGraph KeyError,路由不到节点
if "天气" in user_input:
return {"next_action": "call_weather"} # 返回这个
但节点叫 "weather",不叫 "call_weather"。而且 add_conditional_edges 少传了映射表。
解法:
workflow.add_conditional_edges(
"intent",
lambda s: s["next_action"],
{
"weather": "weather",
"routing": "routing",
"summary": "summary",
},
)
八、一次调用,到底发生了什么
这是全篇最重要的图。记住它,90% 的报错你能自己定位。
客户端 服务端
| ① initialize ────────────> |
| ② 响应(协议版本/能力)<──── |
| ③ notifications/initialized >|
| ④ tools/list ─────────────> |
| ⑤ 工具清单 <──────────────── |
| ⑥ tools/call(add,3,5) ────> |
| ⑦ result: "8" <──────────── |
你在终端看到的那两行日志:
Processing request of type ListToolsRequest
Processing request of type CallToolRequest
就是第 ④ 步和第 ⑥ 步。
你当时以为是噪音,其实是协议在跟你说话。
九、为什么「输入 add 3 5 没反应」
回到开头那个破防瞬间。
因为教程里的 chat_loop 是个占位函数:
async def chat_loop(self):
while True:
user_input = input("\n> ").strip()
if user_input.lower() == "quit":
break
print("收到输入:", user_input) # ← 就只有这一句
它只会复读。真正决定「该调哪个工具」的大脑还没接上。
那个大脑,就是 LLM。
所以完整的链路其实是:
用户说话
↓
LLM 判断:「这句话该调 add,参数是 3 和 5」
↓
Client 发 tools/call
↓
Server 执行,返回 8
↓
LLM 组织成自然语言:「3 加 5 等于 8」
MCP 只负责中间两段。 它不负责理解人话,也不负责组织回答。
这就是为什么「MCP」和「LLM」是两个东西,不能混为一谈。
十、Agent 是什么?祛魅版
很多人一听 Agent 就觉得高大上。其实它朴素得让人失望:
while step < max_iterations:
thought = llm.decide(context) # 想
if thought.action == "FINISH":
return thought.answer
result = call_tool(thought.tool) # 做
context.append(result) # 看
step += 1
想 → 做 → 看 → 再想。
所有 Agent 框架(LangGraph、AutoGPT、CrewAI)都是这个循环的变体。框架帮你做的是:状态管理、分支控制、失败重试。
理解了这一点,你看任何 Agent 框架都不会再懵。
十一、最后送你一个心智模型
学完这些,我把它压缩成了三层:
┌─────────────────────────────────────┐
│ 第三层 业务层 │
│ 你的数据、你的逻辑(SQL、API、权限) │
│ ↑ 这层跟 MCP 无关,是普通后端开发 │
├─────────────────────────────────────┤
│ 第二层 协议层 │
│ initialize / tools/list / tools/call │
│ JSON-RPC 2.0 │
│ ↑ 这层是「说什么」,所有语言都必须遵守 │
├─────────────────────────────────────┤
│ 第一层 传输层 │
│ stdio(管道) / Streamable HTTP(网络)│
│ ↑ 这层是「怎么送」,决定能不能跨机 │
└─────────────────────────────────────┘
遇到报错时,先问自己:这是哪一层的问题?
| 现象 | 哪一层 |
|---|---|
命令找不到 / python not found | 环境(层外) |
Connection refused / 端口不通 | 传输层 |
Invalid JSON / 版本不匹配 | 协议层 |
| 数据不对 / 逻辑错 | 业务层 |
定位范围一下从「整个项目」缩到「某一层」。
这个思维习惯,比记住任何 API 都值钱。
十二、写在最后
三天前我以为 MCP 是个复杂的新技术。
三天后我发现它只是一个约定——约定 AI 和工具之间怎么说话。
复杂的是生态:版本在演进、传输在换代、认证在收紧。
但核心从来没变:让工具写一次,所有 AI 都能用。
如果你也在学 MCP,别再只看教程了。跑一遍,踩一次坑,比看十篇文章管用。
如果这篇对你有帮助:
- 👍 点个赞,让更多人少踩几个坑
- ⭐ 收藏起来,下次报错可以直接查
- 💬 评论区聊聊你踩过最离谱的坑
- ➕ 关注我,后续会写「用 Python 给自己的工作流做一套 MCP 工具」
我们下篇见。
更多推荐
所有评论(0)