写一个最小的 MCP server,用官方 Python SDK 2.x 只要不到 20 行:装 mcp[cli],用 MCPServer 注册一个 @mcp.tool() 函数,再用 claude mcp add 接进 Claude Code。连不上时先看 claude mcp list,再查路径、Python 环境和 stdout 污染。
最容易踩的坑是版本。2026-07-28 发布 2.0 之后,PyPI 上 mcp 的最新版是 2.3.0(2026-10-02),而大多数教程还在写 v1 的 FastMCP。照抄旧代码会直接报错,下面的示例都按 2.x 写,最后核对日期 2026-10-09。
环境准备:Python 版本和安装命令
SDK 要求 Python 3.10 及以上。推荐用 uv 管理,pip 也行:
uv init mcp-demo && cd mcp-demo
uv add "mcp[cli]"
# 或者
pip install "mcp[cli]"
cli 这个 extra 提供 mcp dev、mcp run、mcp install 三个命令,调试要用,建议装。不需要命令行工具的话装 mcp 就够。
想留在 v1 线的话固定上界:pip install "mcp[cli]<2",v1 最新是 1.30.0,只接收关键 bug 和安全修复。
写一个最小 server
一个工具函数加一个资源,足够验证整个流程:
# server.py
import sys
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("Demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
if __name__ == "__main__":
print("demo server starting", file=sys.stderr) # 日志只能走 stderr
mcp.run(transport="stdio")
几个要点:
- 函数的类型注解和 docstring 会被 SDK 转成工具的 JSON Schema 和描述,模型靠这段描述决定什么时候调用,写清楚比写长重要。
- v1 的写法是
from mcp.server.fastmcp import FastMCP。2.x 改成from mcp.server.mcpserver import MCPServer,旧路径会ModuleNotFoundError。PyPI 首页的示例写的是from mcp.server import MCPServer,我没有逐一验证两种写法是否都可用,迁移指南给的是前一种,我用前一种。 host、port、stateless_http这类传输参数在 2.x 从构造函数挪到了run(),比如mcp.run(transport="streamable-http", host="0.0.0.0", port=9000)。
先用 Inspector 测,别急着接 Claude Code
uv run mcp dev server.py
这条命令会打开 MCP Inspector,能列出工具、手动填参数调用、看原始消息。Inspector 里调不通,接 Claude Code 也一定调不通,先在这里排除代码问题。
想看 HTTP 方式,用 uv run mcp run server.py --transport streamable-http,客户端用 http://localhost:8000/mcp 连接。
接入 Claude Code
本地 stdio server 的格式:
claude mcp add demo -- uv run --directory /绝对路径/mcp-demo python server.py
规则只有三条:
- 所有 Claude 的选项(
--scope、--env、--transport)放在名字demo之前。 --之后全是 server 的启动命令,-y、--port这类参数不会被 Claude 吞掉。- 作用域默认是
local(只对当前项目生效,存在~/.claude.json),-s project写进项目根目录的.mcp.json可以随 git 共享,-s user对所有项目生效。
带环境变量的例子:
claude mcp add --env API_KEY=xxx --transport stdio demo -- uv run --directory /绝对路径/mcp-demo python server.py
--env 后面必须再放一个选项(比如 --transport stdio)再写名字,否则名字会被当成 KEY=value。
远程 server 用 claude mcp add --transport http <名字> <URL>。SSE 已被标为弃用,能用 HTTP 就用 HTTP。
接完检查:
claude mcp list # 看状态
claude mcp get demo # 失败时会多一行 Issue
会话里输入 /mcp 可以查看状态、重新认证或重连。
连不上时的排查顺序
claude mcp list 常见的状态有 ✔ Connected、! Needs authentication、✘ Failed to connect、⏸ Pending approval。按下面的顺序查,命中率最高:
| 现象 | 常见原因 | 处理 |
|---|---|---|
| Failed to connect,启动即退出 | 相对路径、找不到 uv 或 python | 命令和脚本都写绝对路径,用 --directory 固定工作目录 |
| Failed to connect,手动运行正常 | Claude 启动的环境里没有你的虚拟环境和 PATH | 用 uv run 或虚拟环境里 python 的绝对路径 |
| 连上后立刻断开 | 代码里有 print(),stdout 被污染 | 日志改 stderr 或文件 |
| ModuleNotFoundError | 装了 v2 却用 v1 的 FastMCP import,或相反 | 对齐版本和 import |
| Pending approval | 项目 .mcp.json 里的 server 还没批准 | 在项目里运行 claude 批准 |
| 启动慢被判超时 | 首次 uv run 要装依赖 | MCP_TIMEOUT=10000 claude(毫秒)调长启动超时 |
| 配置怪异报错 | 粘贴 token 带了首尾空格;${VAR} 没设置 | Claude 会给警告但不会自动 trim,手动改 |
| 工具调用中途被截断 | 输出超过 MAX_MCP_OUTPUT_TOKENS(默认 25000) | 缩小返回内容或调高上限 |
需要更详细的日志,用 claude --debug-file /tmp/claude-debug.log 启动,直接搜 server 名字。
关于规范版本
2026-07-28 版 MCP 规范主打无状态化:按我查到的几篇资料,initialize 握手和 session id 被去掉,每个请求自带上下文,方便放在普通负载均衡后面。这对要部署 HTTP 服务的人影响大,对本地 stdio 的小工具影响很小,因为 SDK 替你处理协议细节。无状态化的细节可以看站内那篇深度解析。具体哪些行为不向后兼容,以 SDK 迁移指南为准,我没有逐条核对。
我的判断
自己写 MCP server 的门槛已经很低,难点不在代码,而在环境。我的建议是:第一次一定用 stdio 加 Inspector,把工具跑通再谈部署;路径全写绝对路径;从第一行起就禁用 print。需要对外提供服务、要多实例部署的人,再去看 streamable-http 和无状态化。只想让 Claude 读几个本地文件或查一个内部接口的人,一个 20 行的 stdio server 已经够用,别一上来就搭网关。反过来,如果你打算把 server 公开给别人用,v2 的迁移指南必读,不要照旧教程抄 v1 写法。
相关阅读
参考
- PyPI: mcp(官方 Python SDK)
- MCP Python SDK v2 迁移指南
- Claude Code 文档:MCP
- Cloudflare: The next generation of MCP
- Appwrite: MCP goes stateless in the 2026-07-28 specification
最后核对日期:2026-10-09。