本教程面向有一定Python基础的开发者,从MCP协议原理讲起,逐步带你用Python FastMCP框架构建一个完整的MCP工具服务器,并接入Claude Code使用。学完后你将掌握MCP三大原语(Tools、Resources、Prompts)的实现方法、调试技巧和部署方案,具备为公司内部系统开发专属MCP工具的能力。
适用人群:后端开发者、DevOps工程师、AI应用开发者
| 项目 | 要求 |
|---|---|
| Python | 3.10+ |
| Node.js | 18+(用于MCP Inspector调试) |
| Claude Code | 已安装并配置 |
| 包管理器 | uv 或 pip |
| 基础知识 | REST API概念、JSON格式 |
在写代码之前,理解MCP的通信模型至关重要。MCP(Model Context Protocol)是Anthropic推出的开放协议,目标是标准化AI模型与外部工具/数据源之间的连接方式——被称为"AI的USB-C接口"。

图表 1
关键角色: - Host(宿主):Claude Code、Claude Desktop等AI应用 - Client(客户端):嵌入Host中的协议客户端,与Server保持1:1连接 - Server(服务器):暴露工具、资源、提示词的轻量级程序
MCP支持两种传输方式:
| 传输方式 | 原理 | 适用场景 |
|---|---|---|
| Stdio | 通过stdin/stdout管道通信,JSON-RPC 2.0编码 | 本地开发、Claude Code集成(最常用) |
| Streamable HTTP | HTTP POST请求 + SSE流式响应 | 远程部署、团队共享、生产环境 |
Claude Code中90%的场景使用Stdio传输,Server作为子进程被自动拉起。

图表 2
生命周期分为三个阶段: 1. 初始化(Initialization):Client与Server交换能力声明 2. 运行(Operation):正常的请求-响应通信 3. 关闭(Shutdown):连接终止
MCP Server通过三种原语(Primitives)向AI暴露能力。理解何时使用哪种原语,是设计好服务器的关键。
Tools是模型控制(model-controlled)的原语。AI根据用户需求自主决定调用哪个工具、传什么参数。
适用场景:执行计算、调用API、修改数据、运行脚本等有副作用的操作。
Resources是应用控制(application-controlled)的原语。由Host应用决定何时读取,类似于REST API的GET请求。
适用场景:暴露文件内容、数据库记录、配置信息等只读数据。
Prompts是用户控制(user-controlled)的原语。提供预定义的提示词模板,用户可以在对话中主动选择使用。
适用场景:标准化工作流模板、常用查询模式、角色扮演预设。
graph LR
subgraph 谁来触发?
T["Tools<br/>模型自主决策"]
R["Resources<br/>应用按需读取"]
P["Prompts<br/>用户主动选择"]
end
T -->|有副作用| Act["执行操作"]
R -->|只读| Data["获取数据"]
P -->|模板化| Tmpl["复用提示"]
style T fill:#ffcdd2
style R fill:#c8e6c9
style P fill:#bbdefb
# 创建项目目录
mkdir mcp-weather-server && cd mcp-weather-server
# 使用 uv 初始化(推荐)
uv init
uv add "mcp[cli]"
# 或使用 pip
pip install "mcp[cli]"
mcp[cli] 包含了 FastMCP 框架和 MCP Inspector 调试工具。
mcp-weather-server/
├── pyproject.toml
├── weather_server.py # 主服务器文件
├── .mcp.json # Claude Code 配置(后文创建)
└── README.md
我们构建一个完整的天气预报MCP服务器,同时演示Tools、Resources、Prompts三种原语的用法。
创建 weather_server.py:
"""
天气预报 MCP Server
演示 Tools / Resources / Prompts 三种原语的完整实现
"""
import asyncio
import json
from datetime import datetime, timedelta
from typing import Optional
from mcp.server.fastmcp import FastMCP
# 初始化 FastMCP 服务器
mcp = FastMCP(
name="weather-server",
version="1.0.0",
instructions="天气查询服务器,支持实时天气、预报和天气报告模板",
)
# ============================================================
# 模拟数据(实际项目中替换为真实 API 调用)
# ============================================================
WEATHER_DB = {
"北京": {"temp": 22, "humidity": 45, "wind": "东北风3级", "condition": "晴", "aqi": 85},
"上海": {"temp": 26, "humidity": 72, "wind": "东南风2级", "condition": "多云", "aqi": 62},
"深圳": {"temp": 30, "humidity": 80, "wind": "南风2级", "condition": "阵雨", "aqi": 45},
"成都": {"temp": 24, "humidity": 65, "wind": "微风", "condition": "阴", "aqi": 78},
"广州": {"temp": 31, "humidity": 78, "wind": "南风3级", "condition": "雷阵雨", "aqi": 55},
}
def get_mock_forecast(city: str, days: int) -> list[dict]:
"""生成模拟天气预报数据"""
base = WEATHER_DB.get(city, {"temp": 20, "humidity": 50, "wind": "微风", "condition": "晴", "aqi": 60})
forecast = []
conditions = ["晴", "多云", "阴", "小雨", "中雨", "阵雨", "雷阵雨"]
for i in range(days):
date = (datetime.now() + timedelta(days=i + 1)).strftime("%Y-%m-%d")
import random
forecast.append({
"date": date,
"temp_high": base["temp"] + random.randint(-3, 5),
"temp_low": base["temp"] - random.randint(3, 8),
"condition": random.choice(conditions),
"humidity": base["humidity"] + random.randint(-10, 10),
})
return forecast
# ============================================================
# Tools(工具)—— 模型可调用的操作
# ============================================================
@mcp.tool()
def get_current_weather(city: str) -> str:
"""获取指定城市的当前天气信息
Args:
city: 城市名称,如"北京"、"上海"
"""
if city not in WEATHER_DB:
return f"错误:未找到城市 '{city}'。支持的城市:{', '.join(WEATHER_DB.keys())}"
data = WEATHER_DB[city]
result = (
f"📍 {city} 当前天气\n"
f"🌡️ 温度:{data['temp']}°C\n"
f"💧 湿度:{data['humidity']}%\n"
f"🌬️ 风力:{data['wind']}\n"
f"☁️ 天气:{data['condition']}\n"
f"🫁 AQI:{data['aqi']}"
)
return result
@mcp.tool()
def get_weather_forecast(city: str, days: int = 3) -> str:
"""获取指定城市未来几天的天气预报
Args:
city: 城市名称
days: 预报天数,1-7天,默认3天
"""
if city not in WEATHER_DB:
return f"错误:未找到城市 '{city}'"
days = max(1, min(7, days))
forecast = get_mock_forecast(city, days)
lines = [f"📅 {city} 未来{days}天天气预报\n"]
for day in forecast:
lines.append(
f" {day['date']} | "
f"{day['condition']} | "
f"{day['temp_low']}~{day['temp_high']}°C | "
f"湿度 {day['humidity']}%"
)
return "\n".join(lines)
@mcp.tool()
def compare_cities(city1: str, city2: str) -> str:
"""对比两个城市的天气状况
Args:
city1: 第一个城市名称
city2: 第二个城市名称
"""
if city1 not in WEATHER_DB or city2 not in WEATHER_DB:
missing = [c for c in [city1, city2] if c not in WEATHER_DB]
return f"错误:未找到城市 {missing}"
d1, d2 = WEATHER_DB[city1], WEATHER_DB[city2]
diff = d1["temp"] - d2["temp"]
warmer = city1 if diff > 0 else city2
return (
f"📊 天气对比:{city1} vs {city2}\n"
f"温度:{d1['temp']}°C vs {d2['temp']}°C(温差 {abs(diff)}°C,{warmer}更暖)\n"
f"湿度:{d1['humidity']}% vs {d2['humidity']}%\n"
f"天气:{d1['condition']} vs {d2['condition']}\n"
f"AQI:{d1['aqi']} vs {d2['aqi']}"
)
# ============================================================
# Resources(资源)—— 应用可读取的数据
# ============================================================
@mcp.resource("weather://cities/supported")
def get_supported_cities() -> str:
"""返回支持查询的所有城市列表"""
cities = list(WEATHER_DB.keys())
return json.dumps({"cities": cities, "total": len(cities)}, ensure_ascii=False, indent=2)
@mcp.resource("weather://cities/{city}/snapshot")
def get_city_snapshot(city: str) -> str:
"""获取指定城市的天气快照(JSON格式)"""
if city not in WEATHER_DB:
return json.dumps({"error": f"城市 '{city}' 不在支持列表中"}, ensure_ascii=False)
return json.dumps(
{"city": city, "timestamp": datetime.now().isoformat(), "data": WEATHER_DB[city]},
ensure_ascii=False,
indent=2,
)
# ============================================================
# Prompts(提示词模板)—— 用户可选的预设模板
# ============================================================
@mcp.prompt()
def daily_weather_briefing(city: str = "北京") -> str:
"""生成每日天气简报的提示词模板
Args:
city: 目标城市,默认北京
"""
return (
f"请为{city}生成一份详细的每日天气简报,包含以下内容:\n"
f"1. 当前天气状况概述\n"
f"2. 温度变化趋势分析\n"
f"3. 穿衣建议(基于温度和风力)\n"
f"4. 出行建议(基于天气状况和AQI)\n"
f"5. 健康提醒(如紫外线、花粉等)\n\n"
f"请用友好的语气,适合在早晨阅读。"
)
@mcp.prompt()
def travel_planning(cities: str = "北京,上海") -> str:
"""旅行规划辅助的提示词模板
Args:
cities: 逗号分隔的城市列表
"""
city_list = [c.strip() for c in cities.split(",")]
return (
f"我正在规划一次途经 {', '.join(city_list)} 的旅行。\n"
f"请帮我:\n"
f"1. 查询每个城市的当前天气\n"
f"2. 对比各城市天气差异\n"
f"3. 根据天气情况推荐最佳出行顺序\n"
f"4. 给出每个城市的行李打包建议\n"
f"5. 标注需要注意的恶劣天气风险"
)
# ============================================================
# 启动服务器
# ============================================================
if __name__ == "__main__":
mcp.run()
FastMCP的装饰器魔法:FastMCP利用Python的类型注解(Type Hints)和文档字符串(Docstrings)自动生成MCP协议需要的工具描述和参数Schema。
@mcp.tool()
def get_current_weather(city: str) -> str:
"""获取指定城市的当前天气信息 ← 描述自动提取
Args:
city: 城市名称,如"北京"、"上海" ← 参数说明自动提取
"""
这段代码被FastMCP处理后,会自动生成如下MCP工具定义:
{
"name": "get_current_weather",
"description": "获取指定城市的当前天气信息",
"inputSchema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如\"北京\"、\"上海\""
}
},
"required": ["city"]
}
}
Resource的URI模板:@mcp.resource("weather://cities/{city}/snapshot") 中的 {city} 是动态参数,Client可以通过不同的URI读取不同城市的快照数据。
在项目根目录创建 .mcp.json:
{
"mcpServers": {
"weather": {
"command": "uv",
"args": ["run", "weather_server.py"],
"cwd": "/absolute/path/to/mcp-weather-server"
}
}
}
注意:使用
uv run会自动处理虚拟环境。如果用系统Python,改为"command": "python", "args": ["weather_server.py"]。
# 添加到项目级别
claude mcp add weather uv run weather_server.py
# 添加到用户全局级别(所有项目可用)
claude mcp add --scope user weather uv run /path/to/weather_server.py
# 通过JSON直接添加(适合复杂配置)
claude mcp add-json weather '{"command":"uv","args":["run","weather_server.py"]}'
# 列出已配置的MCP服务器
claude mcp list
# 输出示例:
# weather: uv run weather_server.py
# Scope: project (.mcp.json)
# Status: connected ✓
在Claude Code对话中测试:
> 北京现在天气怎么样?
# Claude Code 会自动调用 get_current_weather("北京")
# 📍 北京 当前天气
# 🌡️ 温度:22°C
# ...
claude mcp list # 列出所有服务器
claude mcp remove weather # 移除服务器
claude mcp reset # 重置所有配置
MCP Inspector是官方提供的Web调试工具,可以独立测试Server,不需要通过Claude Code。
# 方法一:使用 mcp CLI
mcp dev weather_server.py
# 方法二:直接用 npx
npx @modelcontextprotocol/inspector uv run weather_server.py
Inspector会在浏览器中打开一个Web界面,提供:
| 问题 | 原因 | 解决方案 |
|---|---|---|
Server不显示在 mcp list |
配置文件路径错误 | 检查 .mcp.json 是否在项目根目录 |
mcp list 显示但工具不可用 |
Python依赖缺失 | 运行 uv add "mcp[cli]" 安装完整依赖 |
| 工具调用超时 | Server启动慢/代码报错 | 先用 mcp dev 独立测试Server |
| 中文参数乱码 | 编码问题 | 确保文件UTF-8编码,系统locale正确 |
command not found |
命令路径问题 | 使用绝对路径:/home/user/.local/bin/uv |
在Server代码中添加日志输出(输出到stderr,不会干扰MCP的stdio通信):
import sys
@mcp.tool()
def get_current_weather(city: str) -> str:
print(f"[DEBUG] 收到天气查询请求: city={city}", file=sys.stderr)
# ... 业务逻辑
print(f"[DEBUG] 返回天气数据: {data}", file=sys.stderr)
return result
查看Claude Code的MCP日志:
# Claude Code 的 MCP 日志通常在
~/.claude/logs/
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("production-server")
@mcp.tool()
def query_database(sql: str) -> str:
"""执行SQL查询(只读)"""
try:
# 参数验证
if not sql.strip().upper().startswith("SELECT"):
return "错误:仅支持SELECT查询"
# 执行查询(伪代码)
result = execute_sql(sql)
return json.dumps(result, ensure_ascii=False, indent=2)
except ConnectionError:
return "错误:数据库连接失败,请检查网络"
except TimeoutError:
return "错误:查询超时,请简化查询条件"
except Exception as e:
# 不要暴露内部堆栈信息
return f"错误:查询执行失败 - {type(e).__name__}"
关键原则: - 永远返回字符串,不要抛出未捕获的异常 - 区分用户输入错误和系统内部错误 - 错误消息对AI友好——帮助Claude理解失败原因并调整策略 - 不要暴露内部堆栈信息或敏感数据
import os
@mcp.tool()
def call_external_api(endpoint: str) -> str:
"""调用外部API"""
api_key = os.environ.get("MY_API_KEY")
if not api_key:
return "错误:未配置API密钥,请设置 MY_API_KEY 环境变量"
# ...
在 .mcp.json 中传递环境变量:
{
"mcpServers": {
"my-server": {
"command": "uv",
"args": ["run", "server.py"],
"env": {
"MY_API_KEY": "sk-xxx",
"DB_HOST": "localhost:5432"
}
}
}
}
安全提醒:
.mcp.json可能被提交到Git。对于敏感密钥,建议在env中引用系统环境变量或使用${MY_API_KEY}语法(如果支持)。
对于耗时操作,MCP支持进度通知:
@mcp.tool()
async def batch_process(items: list[str]) -> str:
"""批量处理数据项"""
results = []
total = len(items)
ctx = mcp.get_context() # 获取MCP上下文
for i, item in enumerate(items):
result = await process_single(item)
results.append(result)
# 发送进度通知
await ctx.report_progress(
progress=i + 1,
total=total,
)
return f"处理完成:{len(results)} 项成功"
当需要将MCP Server部署到远程服务器供团队共享时,使用Streamable HTTP传输:
# server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("remote-server", host="0.0.0.0", port=8080)
@mcp.tool()
def hello(name: str) -> str:
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run(transport="streamable-http")
Claude Code中配置远程服务器:
{
"mcpServers": {
"remote-tool": {
"url": "https://your-server.com/mcp",
"headers": {
"Authorization": "Bearer your-token"
}
}
}
}
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "server.py"]
.mcp.json 中使用Docker运行:
{
"mcpServers": {
"docker-tool": {
"command": "docker",
"args": ["run", "-i", "--rm", "my-mcp-server"]
}
}
}
Hooks是Claude Code的另一层自动化机制,可以与MCP Server协同工作:

图表 4
在 .claude/settings.json 中配置Hook,对MCP工具调用进行前置校验:
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__weather__query_database",
"hooks": [
{
"type": "command",
"command": "python validate_sql.py"
}
]
}
]
}
}
如果团队更熟悉TypeScript,MCP官方同样提供了一流的TypeScript SDK:
// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "my-tools", version: "1.0.0" });
// 使用 Zod 定义参数Schema
server.tool(
"get_weather",
"获取城市天气",
{ city: z.string().describe("城市名称") },
async ({ city }) => ({
content: [{ type: "text", text: `${city}: 25°C, 晴` }],
})
);
// 启动
const transport = new StdioServerTransport();
await server.connect(transport);
.mcp.json 配置:
{
"mcpServers": {
"ts-server": {
"command": "npx",
"args": ["tsx", "server.ts"]
}
}
}
| 考量维度 | Python (FastMCP) | TypeScript (MCP SDK) |
|---|---|---|
| 上手速度 | 极快(装饰器+自动Schema) | 快(需手动Zod定义) |
| 生态集成 | 数据科学、AI/ML、运维脚本 | Web开发、前端工具链 |
| 类型安全 | 运行时校验 | 编译时+运行时双重保障 |
| 异步支持 | asyncio,需要理解事件循环 | 原生async/await |
| 部署方式 | uv/pip/Docker | npx/Docker |

图表 5
| 维度 | 评估 |
|---|---|
| 适用训练营 | AI工程化实战营、Python后端进阶营、DevOps自动化训练营 |
| 教学时长 | 3-4小时(含动手实践) |
| 难度等级 | 中等(需Python基础) |
| 实验环境 | 每人一台有网络连接的开发机,预装Python 3.10+和Claude Code |
| 分组建议 | 2-3人一组,每组开发不同主题的MCP Server(天气、数据库、CI/CD等) |
| 考核方式 | 每组演示自己开发的MCP Server在Claude Code中的运行效果 |
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
