基础篇讲了 MCP 是什么、能做什么。进阶篇做两件事:第一,从零手写一个能跑通的 MCP Server;第二,用抓包工具拆解 MCP 协议的每一帧消息,看清楚 Host 和 Server 到底在说什么。
为什么要手写而不是直接用现成的?
ClawHub 上已经有数千个现成的 MCP Server,你不需要从头写。
但手写一次能给你带来用成品永远无法得到的东西:
- 真正理解
@mcp.tool()装饰器背后做了什么 - 知道为什么 docstring 写不好,LLM 就不调用你的工具
- 明白 Tool、Resource、Prompt 三种能力单元的实现差异
- 遇到 MCP 报错时,能准确定位是协议层还是业务层的问题
环境搭建
bash
# 安装 uv(现代 Python 包管理器,比 pip 快 10 倍)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 创建项目
uv init my-mcp-server
cd my-mcp-server
# 安装依赖
uv add "mcp[cli]" httpx
# 项目结构
my-mcp-server/
├── pyproject.toml
├── weather.py # MCP Server 主文件
└── mcp_logger.py # 协议抓包工具
完整的 MCP Server 实现
python
# weather.py
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather", log_level="ERROR")
NWS_API_BASE = "https://api.weather.gov"
# ─── Tool 实现 ───────────────────────────────────────
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""
获取指定经纬度的详细天气预报(未来5个时段)
注意:此工具使用美国国家气象局 API,仅支持美国境内的坐标。
Args:
latitude: 纬度,范围 -90 到 90,精确到小数点后4位
longitude: 经度,范围 -180 到 180,精确到小数点后4位
Returns:
包含未来5个时段天气预报的字符串
"""
async with httpx.AsyncClient() as client:
# Step 1: 获取网格信息
try:
r1 = await client.get(
f"{NWS_API_BASE}/points/{latitude},{longitude}",
headers={"User-Agent": "weather-mcp/1.0", "Accept": "application/geo+json"},
timeout=30.0
)
r1.raise_for_status()
points = r1.json()
except Exception as e:
return f"获取位置信息失败:{e}"
# Step 2: 获取天气预报
forecast_url = points["properties"]["forecast"]
try:
r2 = await client.get(
forecast_url,
headers={"User-Agent": "weather-mcp/1.0"},
timeout=30.0
)
r2.raise_for_status()
forecast = r2.json()
except Exception as e:
return f"获取天气预报失败:{e}"
# Step 3: 格式化输出
periods = forecast["properties"]["periods"][:5]
lines = []
for p in periods:
lines.append(
f"**{p['name']}**\n"
f" 温度:{p['temperature']}°{p['temperatureUnit']}\n"
f" 风速:{p['windSpeed']} {p['windDirection']}\n"
f" 天气:{p['shortForecast']}"
)
return "\n\n".join(lines)
@mcp.tool()
async def get_alerts(state: str) -> str:
"""
获取美国指定州的当前气象预警信息
Args:
state: 美国州的两字母缩写,例如 "CA" (加利福尼亚), "NY" (纽约), "TX" (德克萨斯)
Returns:
当前有效的气象预警列表,若无预警则返回"当前无有效预警"
"""
async with httpx.AsyncClient() as client:
try:
r = await client.get(
f"{NWS_API_BASE}/alerts/active?area={state.upper()}",
headers={"User-Agent": "weather-mcp/1.0", "Accept": "application/geo+json"},
timeout=30.0
)
r.raise_for_status()
data = r.json()
except Exception as e:
return f"获取预警信息失败:{e}"
features = data.get("features", [])
if not features:
return f"{state} 州当前无有效气象预警"
alerts = []
for f in features[:5]:
props = f["properties"]
alerts.append(
f"⚠️ {props.get('headline', '未知预警')}\n"
f" 类型:{props.get('event', '-')}\n"
f" 有效期:{props.get('effective', '-')} 至 {props.get('expires', '-')}\n"
f" 描述:{props.get('description', '-')[:200]}..."
)
return f"{state} 州当前有 {len(features)} 条气象预警:\n\n" + "\n\n".join(alerts)
# ─── Resource 实现 ────────────────────────────────────
@mcp.resource("weather://supported-states")
def get_supported_states() -> str:
"""返回支持查询的美国州列表"""
states = {
"CA": "加利福尼亚", "NY": "纽约", "TX": "德克萨斯",
"FL": "佛罗里达", "WA": "华盛顿", "OR": "俄勒冈",
# ... 更多州
}
return "\n".join(f"{code}: {name}" for code, name in states.items())
# ─── 启动 ─────────────────────────────────────────────
if __name__ == "__main__":
mcp.run(transport='stdio')
深度解析:@mcp.tool() 背后发生了什么
python
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""..."""
这一行装饰器,FastMCP 自动完成了三件事:
1. 提取函数签名生成 JSON Schema
json
{
"name": "get_forecast",
"inputSchema": {
"type": "object",
"properties": {
"latitude": {
"type": "number",
"description": "纬度,范围 -90 到 90..."
},
"longitude": {
"type": "number",
"description": "经度,范围 -180 到 180..."
}
},
"required": ["latitude", "longitude"]
}
}
2. 把 docstring 第一行作为 description
json
{
"description": "获取指定经纬度的详细天气预报(未来5个时段)"
}
⚠️ 重要:这个 description 直接影响 LLM 是否调用你的工具。写得越清晰、越具体,LLM 调用的准确率越高。
3. 注册到 MCP Server 的工具列表
当 Host 发送 tools/list 请求时,这个工具的完整信息会被返回。
抓包拆解协议底层
用一个简单的脚本拦截 MCP 通信:
python
# mcp_logger.py
import subprocess
import sys
import json
import threading
def log(direction: str, data: str):
"""格式化记录通信内容"""
try:
parsed = json.loads(data)
print(f"\n{'→' if direction == 'send' else '←'} {direction.upper()}")
print(json.dumps(parsed, ensure_ascii=False, indent=2))
except json.JSONDecodeError:
print(f"[{direction}] 非 JSON 数据: {data[:100]}")
# 启动真实的 MCP Server
proc = subprocess.Popen(
["uv", "run", "weather.py"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True
)
# 透传并记录
def forward(src, dst, direction):
for line in src:
log(direction, line.strip())
dst.write(line)
dst.flush()
t1 = threading.Thread(target=forward, args=(sys.stdin, proc.stdin, "send"))
t2 = threading.Thread(target=forward, args=(proc.stdout, sys.stdout, "recv"))
t1.start(); t2.start()
t1.join(); t2.join()
实际抓包记录
完整的交互帧序列:
swift
→ SEND(初始化)
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "Cline", "version": "3.8.0"}
}
}
← RECV(初始化确认)
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}, "resources": {}},
"serverInfo": {"name": "weather", "version": "1.0.0"}
}
}
→ SEND(发送初始化完成通知)
{"jsonrpc": "2.0", "method": "notifications/initialized"}
→ SEND(查询工具列表)
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
← RECV(工具列表)
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "get_forecast",
"description": "获取指定经纬度的详细天气预报(未来5个时段)...",
"inputSchema": {
"type": "object",
"properties": {
"latitude": {"type": "number", "description": "..."},
"longitude": {"type": "number", "description": "..."}
},
"required": ["latitude", "longitude"]
}
},
{"name": "get_alerts", ...}
]
}
}
→ SEND(工具调用)
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_forecast",
"arguments": {"latitude": 40.7128, "longitude": -74.006}
}
}
← RECV(工具结果)
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{"type": "text", "text": "**Tonight**\n 温度:58°F\n 风速:S 10 mph\n..."}
],
"isError": false
}
}
一个容易踩的坑:协议版本
MCP 协议还在快速迭代,不同版本的消息格式有差异。
python
# 确保使用和 Host 兼容的版本
mcp = FastMCP("my-server", log_level="ERROR")
# FastMCP 会自动使用最新支持的协议版本
如果遇到工具无法调用,先检查:
- Server 和 Host 的协议版本是否兼容
- JSON-RPC 请求格式是否正确(必须有
jsonrpc: "2.0"和id) - Tool 的
inputSchema是否符合 JSON Schema 规范
总结
进阶篇的核心收获:
- 手写 MCP Server:掌握 Tool / Resource 的实现方式,理解装饰器的作用
- 抓包分析:理解初始化握手、工具发现、工具调用的完整流程
- 协议本质:MCP = JSON-RPC 2.0,加上三种能力单元(Tool/Resource/Prompt)的注册和调用规范
理解了这些,你就不再是 MCP 的"用户",而是能够开发和调试 MCP Server 的"构建者"。