我花了 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() 这行装饰器干了三件事:

  1. 把函数注册进工具表
  2. 读取 a: int, b: int 类型注解,自动生成 JSON Schema
  3. 把 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 工具」

我们下篇见。


Logo

更多推荐