从零搭建一个 MCP Server:Python 完整示例 + AI 调用机制详解
Model Context Protocol(MCP)是 Anthropic 在 2024 年底开源的协议,用于让 LLM 以标准化方式连接外部工具、数据源。本文先用 Python 实现一个最小可运行的 MCP Server,再详细拆解 AI 是如何知道 MCP 提供了哪些方法、如何调用它们的。
一、MCP 是什么?30 秒理解
MCP = Model Context Protocol,本质是一套 JSON-RPC 2.0 协议,定义了:
- Server 向 Client(通常是 LLM 客户端,如 Claude Desktop、Cursor)暴露三类能力:
tools—— 可调用的函数(类似 function calling)resources—— 可读取的数据源(类似文件 / API)prompts—— 预定义的提示词模板
- Client 通过协议握手发现这些能力,并把它们注入 LLM 的上下文
- LLM 决定调用某个 tool 时,Client 负责实际执行并把结果回传
┌──────────┐ MCP 协议(JSON-RPC) ┌──────────┐
│ Client │ ←─────────────────────→ │ Server │
│ (Claude) │ │ (你的代码)│
└────┬─────┘ └────┬─────┘
│ │
│ 把 tools 列表注入 LLM 上下文 │ 执行实际函数
│ LLM 决定调用 → Client 转发请求 │ 返回结果
└────────────────────────────────────┘
二、环境准备
2.1 安装官方 SDK
pip install "mcp[cli]"
# 或最小依赖:
pip install mcp
官方 Python SDK 仓库:https://github.com/modelcontextprotocol/python-sdk
2.2 目录结构
mcp_demo/
├── server.py # MCP Server 实现
└── README.md
三、最小可运行 MCP Server 示例
下面这个 Server 暴露 2 个工具:
add(a, b)—— 加法get_weather(city)—— 模拟天气查询
3.1 完整代码(server.py)
# -*- coding: utf-8 -*-
"""
最小 MCP Server 示例
运行方式:python server.py
默认通过 stdio 与 Client 通信
"""
import sys
import json
from mcp.server.fastmcp import FastMCP
sys.stdout.reconfigure(encoding="utf-8")
# 创建 MCP Server 实例
mcp = FastMCP("demo-server")
# ============ 工具 1:加法 ============
@mcp.tool()
def add(a: float, b: float) -> str:
"""两个数字相加,返回它们的和。
Args:
a: 第一个数字
b: 第二个数字
"""
return f"{a} + {b} = {a + b}"
# ============ 工具 2:模拟天气查询 ============
@mcp.tool()
def get_weather(city: str) -> str:
"""查询某个城市的当前天气(模拟数据)。
Args:
city: 城市名称,如 "北京"
"""
# 模拟数据,实际可对接真实天气 API
mock = {
"北京": "晴,18℃",
"上海": "多云,22℃",
"深圳": "雷阵雨,28℃",
}
return mock.get(city, f"未收录 {city} 的天气,默认晴 20℃")
# ============ 资源:静态资源示例 ============
@mcp.resource("config://app")
def get_config() -> str:
"""暴露一份配置文件作为资源"""
return json.dumps({"version": "1.0.0", "env": "prod"}, ensure_ascii=False)
# ============ 提示词模板示例 ============
@mcp.prompt()
def code_review(code: str) -> str:
"""代码评审提示词"""
return f"请评审以下代码,关注安全性和可读性:\n\n```\n{code}\n```"
if __name__ == "__main__":
# 启动 Server,通过 stdio 通信
mcp.run(transport="stdio")
3.2 代码解析
| 装饰器 | 作用 | 暴露给 AI 的方式 |
|---|---|---|
@mcp.tool() |
注册工具函数 | LLM 可主动调用 |
@mcp.resource() |
注册只读资源 | LLM 可读取内容 |
@mcp.prompt() |
注册提示词模板 | 用户可在客户端选择 |
关键设计:
- docstring 即文档:函数的 docstring 会被自动转成 tool 的
description,AI 看到的就是它 - 类型注解即入参 schema:
a: float会被转成 JSON Schema,AI 据此知道参数类型和是否必填 FastMCP:官方 SDK 的高级封装,把 JSON-RPC 协议细节隐藏起来,开发者只写函数
四、运行与接入
4.1 直接运行(自检)
python server.py
进程会阻塞,等待 stdin 输入。这是正常的 —— 它在等 Client 发 JSON-RPC 消息。
4.2 接入 Claude Desktop
编辑 Claude Desktop 配置文件:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
加入:
{
"mcpServers": {
"demo-server": {
"command": "python",
"args": ["D:/path/to/server.py"]
}
}
}
重启 Claude Desktop,在对话框右下角会看到 🔄 工具图标,点开能看到 add 和 get_weather。
4.3 接入 Cursor / 其他支持 MCP 的客户端
原理一致 —— 在客户端的 MCP 配置里注册 Server 的启动命令。
五、AI 是如何知道 MCP 提供了哪些方法的?
这是本文的核心问题。拆成 4 个阶段:
阶段 1:启动握手 → 阶段 2:能力发现 → 阶段 3:注入上下文 → 阶段 4:按需调用
5.1 阶段一:协议握手(Initialize)
Client 启动 Server 子进程后,立刻发一条 initialize 请求:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": { "listChanged": true },
"sampling": {}
},
"clientInfo": { "name": "claude-desktop", "version": "1.0.0" }
}
}
Server 回:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": {},
"prompts": {}
},
"serverInfo": { "name": "demo-server", "version": "1.0.0" }
}
}
关键字段:capabilities.tools / resources / prompts —— Server 在这里告诉 Client 「我有哪些类的能力」,但还没列具体方法。这就像饭店门口挂「本店有炒菜、面食、汤」的牌子,但还没给你菜单。
5.2 阶段二:能力发现(List)
握手完成后,Client 立刻请求 清单:
5.2.1 拉取工具列表 —— tools/list
Client 发:
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
Server 回(基于上面的 Python 代码):
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "add",
"description": "两个数字相加,返回它们的和。",
"inputSchema": {
"type": "object",
"properties": {
"a": { "type": "number", "description": "第一个数字" },
"b": { "type": "number", "description": "第二个数字" }
},
"required": ["a", "b"]
}
},
{
"name": "get_weather",
"description": "查询某个城市的当前天气(模拟数据)。",
"inputSchema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名称,如 \"北京\"" }
},
"required": ["city"]
}
}
]
}
}
注意三个来源:
| 字段 | 来源 |
|---|---|
name |
Python 函数名 |
description |
函数 docstring |
inputSchema |
类型注解 + 参数 docstring(Args: 段) |
也就是说 —— AI 看到的工具文档,完全由你写函数时的 docstring 和类型注解决定。docstring 不写,AI 就不知道这工具干嘛,很可能不调或乱调。
5.2.2 拉取资源列表 —— resources/list
{ "jsonrpc": "2.0", "id": 3, "method": "resources/list" }
Server 回:
{
"result": {
"resources": [
{
"uri": "config://app",
"name": "get_config",
"description": "暴露一份配置文件作为资源",
"mimeType": "text/plain"
}
]
}
}
5.2.3 拉取提示词列表 —— prompts/list
同理,返回所有 @mcp.prompt() 注册的模板。
5.3 阶段三:注入 LLM 上下文
Client 拿到 tools/list 的结果后,把它转换成 LLM 能理解的格式,塞进 system prompt 或 tools 字段:
5.3.1 转成 OpenAI function calling 格式
{
"model": "gpt-4o",
"messages": [...],
"tools": [
{
"type": "function",
"function": {
"name": "add",
"description": "两个数字相加,返回它们的和。",
"parameters": {
"type": "object",
"properties": {
"a": { "type": "number", "description": "第一个数字" },
"b": { "type": "number", "description": "第二个数字" }
},
"required": ["a", "b"]
}
}
}
]
}
5.3.2 转成 Anthropic tools 格式(Claude 系列)
{
"model": "claude-opus-4-7",
"tools": [
{
"name": "add",
"description": "两个数字相加,返回它们的和。",
"input_schema": {
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["a", "b"]
}
}
]
}
核心转换逻辑:
MCP tools/list 结果 ──[字段名映射]──> LLM tools 字段
name → name
description → description
inputSchema → parameters / input_schema
不同 LLM 的 tools 字段格式略有差异,Client 负责 协议适配,Server 永远只输出 MCP 标准 schema。
5.4 阶段四:LLM 决策调用 → Client 转发 → 返回
5.4.1 LLM 输出工具调用请求
当用户问「北京天气怎么样?」,LLM 看到自己有 get_weather 工具,会输出:
{
"role": "assistant",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\"}"
}
}
]
}
5.4.2 Client 拦截 → 转成 MCP 请求 → 发给 Server
Client 不直接执行函数,而是通过 MCP 协议把请求转给 Server:
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "city": "北京" }
}
}
5.4.3 Server 执行函数 → 返回结果
Server 根据名字找到 get_weather 函数,执行后回:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{ "type": "text", "text": "晴,18℃" }
]
}
}
5.4.4 Client 把结果转成 LLM 消息 → 继续对话
Client 把结果塞回 LLM 上下文:
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "晴,18℃"
}
LLM 看到结果,生成最终回答:「北京现在是晴天,气温 18℃。」
六、完整时序图
用户 Client(Claude) Server(server.py) LLM
│ │ │ │
│ "北京天气怎么样?" │ │ │
│─────────────────────>│ │ │
│ │ │ │
│ │ 1. initialize │ │
│ │──────────────────────────>│ │
│ │<──────────────────────────│ │
│ │ 2. tools/list │ │
│ │──────────────────────────>│ │
│ │<──────────────────────────│ │
│ │ 返回 [add, get_weather] │ │
│ │ │ │
│ │ 3. 注入 tools 到 LLM 上下文 │
│ │──────────────────────────────────────────────────>│
│ │ │ │
│ │ 4. LLM 决定调用 get_weather("北京") │
│ │<──────────────────────────────────────────────────│
│ │ │ │
│ │ 5. tools/call │ │
│ │ name=get_weather │ │
│ │ args={city:"北京"} │ │
│ │──────────────────────────>│ │
│ │ │ 执行 get_weather() │
│ │<──────────────────────────│ │
│ │ content: "晴,18℃" │ │
│ │ │ │
│ │ 6. 把结果回传给 LLM │
│ │──────────────────────────────────────────────────>│
│ │ │ │
│ │ 7. LLM 生成最终回答 │
│ │<──────────────────────────────────────────────────│
│ │ │ │
│ "北京现在是晴天,18℃"│ │ │
│<─────────────────────│ │ │
七、关键设计点:为什么是 JSON-RPC 而不是 REST?
| 维度 | JSON-RPC(MCP 选的) | REST |
|---|---|---|
| 双向通信 | ✅ Server 也可主动通知 Client(notifications/tools/list_changed) |
❌ 只能 Client 主动 |
| 统一入口 | 一个端点,method 区分 | 每个能力一个 URL |
| 传输层无关 | stdio / HTTP / SSE 都能跑 | 通常只 HTTP |
| 工具调用语义 | method + params,贴合 RPC 语义 |
GET/POST 语义不对应「调用函数」 |
实际传输方式(transport):
stdio—— 本地子进程,Claude Desktop 用这种HTTP+SSE—— 远程 Server,适合云端部署Streamable HTTP—— 2025 年新增,单端点双向流
八、调试与验证
8.1 用官方 Inspector 工具
npx @modelcontextprotocol/inspector python server.py
打开浏览器,可视化看到:
- 握手返回的 capabilities
tools/list、resources/list、prompts/list的实际结果- 在界面里手动调 tool,看返回
8.2 不写 SDK,手写最小 MCP Server
理解协议最快的方式是手写 JSON-RPC:
# 手写最小 MCP Server(仅 tools/list 和 tools/call)
import sys
import json
def handle(req):
method = req.get("method")
if method == "initialize":
return {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {"name": "handwritten", "version": "1.0.0"}
}
if method == "tools/list":
return {"tools": [{
"name": "echo",
"description": "原样返回输入",
"inputSchema": {
"type": "object",
"properties": {"msg": {"type": "string"}},
"required": ["msg"]
}
}]}
if method == "tools/call":
params = req.get("params", {})
if params.get("name") == "echo":
return {"content": [{"type": "text", "text": params["arguments"]["msg"]}]}
return None
for line in sys.stdin:
req = json.loads(line)
result = handle(req)
if result is not None:
print(json.dumps({"jsonrpc": "2.0", "id": req.get("id"), "result": result}), flush=True)
跑起来和官方 SDK 暴露的能力一样,AI 看不出差别 —— 这正说明 MCP 是 协议,SDK 只是语法糖。
九、最佳实践
- docstring 要写清楚 —— 这是 AI 唯一能看到的工具说明,写得越具体,AI 调用越准
- 类型注解必须准确 —— 别用
Any或dict,用具体的str/int/ Pydantic Model - 工具粒度要适中 —— 太粗(
run_anything)AI 不会用,太细(add_one)上下文爆掉 - 错误用 MCP 标准格式返回 ——
{"isError": true, "content": [...]}比 raise 异常更友好 - 副作用工具要幂等 —— AI 可能重复调用,确保多次调结果一致
- 资源优先于工具 —— 只读数据用
resource,有副作用的才用tool - prompt 用于固化高频用法 —— 比如固定的代码评审模板,不必让用户每次手敲
十、常见坑
坑 1:工具没出现在客户端
原因:@mcp.tool() 装饰器写错 / 函数没 return / docstring 为空。
排查:用 Inspector 看 tools/list 返回。
坑 2:AI 调用时参数对不上
原因:类型注解和实际接受类型不符,例如注解 str 但函数内当 int 用。
后果:AI 按注解传 "5",函数 a + b 变成字符串拼接。
坑 3:Windows 中文乱码
Server 进程默认编码可能是 GBK,导致 docstring 里的中文变乱码,AI 看不懂。
解决:sys.stdout.reconfigure(encoding="utf-8")(本文示例已加)。
坑 4:stdio 通信被污染
Server 里任何 print() 都会写到 stdout,被 Client 当 JSON-RPC 消息解析,直接报错。
解决:调试日志写 sys.stderr 或用 logging。
坑 5:同步函数阻塞
FastMCP 默认在事件循环里跑,长耗时同步函数会卡住整个 Server。
解决:用 async def 或把耗时任务丢线程池。
十一、总结
一份 MCP Server 暴露能力给 AI,核心只走 4 步:
- 握手 ——
initialize,告诉 Client「我有 tools/resources/prompts」 - 发现 ——
tools/list等,把每个方法的 name + description + schema 列出来 - 注入 —— Client 把这个清单转成 LLM 能理解的 tools 字段
- 调用 —— LLM 决定调用 → Client 通过
tools/call转发 → Server 执行 → 结果回流
AI 知道你的 MCP 有什么方法、怎么调,本质是看你函数的 docstring 和类型注解 —— 这两个东西通过 MCP 的 tools/list 协议化输出,成为 AI 决策的唯一依据。所以写 MCP Server 时,把 docstring 当 API 文档写,把类型注解当契约写,工具质量直接决定 AI 调用质量。
十二、参考
- 官方 Python SDK:https://github.com/modelcontextprotocol/python-sdk
- MCP 协议规范:https://modelcontextprotocol.io
- Inspector 调试工具:
npx @modelcontextprotocol/inspector - Anthropic MCP 公告:https://www.anthropic.com/news/model-context-protocol
更多推荐

所有评论(0)