首页 › tutorials › MCP Server Python实战:FastMCP开发到Docker部署

MCP Server Python实战:FastMCP从开发到Docker部署

概述

本教程面向有Python基础的开发者,从零构建一个完整的MCP Server。你将掌握FastMCP框架的三大核心原语(Tool、Resource、Prompt),学会stdio与Streamable HTTP双传输模式,最终将服务Docker化并接入Claude Code。学完后你可以为任何Python项目创建AI可调用的工具层。

适合人群:有Python基础、了解REST API概念、想为Claude Code创建自定义MCP工具的开发者。

学习目标: - 理解MCP协议核心架构与Python SDK选型 - 使用FastMCP装饰器快速暴露Tool/Resource/Prompt - 实现stdio本地调试与Streamable HTTP远程部署 - Docker容器化并接入Claude Code生产环境


前置条件

项目 最低版本 安装命令
Python 3.10+ pyenv install 3.12
uv 最新 curl -LsSf https://astral.sh/uv/install.sh \| sh
Docker 24+ 系统包管理器安装
Claude Code 最新 npm install -g @anthropic-ai/claude-code

验证环境:

python --version    # >= 3.10
uv --version        # 任意版本
docker --version    # >= 24.0
claude --version    # 最新

第一步:理解MCP架构与Python生态选型

MCP协议核心概念

MCP(Model Context Protocol)是Anthropic提出的开放协议,让LLM以标准化方式连接外部数据源和工具。协议基于JSON-RPC 2.0,核心架构如下:

graph LR
    Host["Host<br/>(Claude Code/Claude Desktop)"]
    Client1["MCP Client 1"]
    Client2["MCP Client 2"]
    Server1["MCP Server A<br/>(Python/FastMCP)"]
    Server2["MCP Server B<br/>(TypeScript)"]

    Host --> Client1
    Host --> Client2
    Client1 -->|"stdio / HTTP"| Server1
    Client2 -->|"stdio / HTTP"| Server2

    Server1 --> DB[("SQLite")]
    Server1 --> FS[("文件系统")]
    Server2 --> API[("外部API")]

MCP Server暴露三种原语:

原语 类比 方向 典型用途
Tool POST端点 LLM→Server 执行操作、查询数据库、调用API
Resource GET端点 LLM←Server 提供文件内容、数据快照、配置
Prompt 模板 LLM→Server 可复用的提示词模板

Python SDK选型:FastMCP vs 官方SDK

维度 mcp(官方SDK) fastmcp(社区增强)
维护方 Anthropic官方 PrefectHQ
包名 mcp[cli] fastmcp
装饰器风格 有 简洁,@mcp.tool()
学习曲线 较陡 平缓
内置CLI mcp dev fastmcp dev
生产就绪 是 是(已被官方推荐)

本教程选择FastMCP,原因是其装饰器API更符合Python开发者习惯,且官方已将其纳入推荐工具链。


第二步:项目初始化

使用 uv 创建项目骨架:

# 创建项目目录
mkdir weather-mcp-server && cd weather-mcp-server

# 初始化Python项目
uv init --name weather-mcp-server --python 3.12

# 添加依赖
uv add fastmcp httpx

# 创建源码目录
mkdir -p src/weather_mcp

项目结构:

weather-mcp-server/
├── pyproject.toml
├── uv.lock
├── Dockerfile
├── README.md
├── src/
│   └── weather_mcp/
│       ├── __init__.py
│       ├── server.py        # MCP Server主入口
│       ├── models.py        # 数据模型
│       └── weather.py       # 业务逻辑
└── tests/
    └── test_server.py

配置 pyproject.toml:

[project]
name = "weather-mcp-server"
version = "0.1.0"
description = "A weather MCP server for Claude Code"
requires-python = ">=3.10"
dependencies = [
    "fastmcp>=2.0",
    "httpx>=0.27",
]

[project.scripts]
weather-mcp = "weather_mcp.server:main"

第三步:实现业务逻辑层

创建 src/weather_mcp/weather.py,封装天气API调用:

"""天气数据服务 - 调用Open-Meteo免费API"""

from dataclasses import dataclass
import httpx


@dataclass
class WeatherData:
    temperature: float
    humidity: float
    wind_speed: float
    description: str


# WMO天气代码映射
WMO_CODES = {
    0: "晴朗", 1: "大部晴朗", 2: "多云", 3: "阴天",
    45: "雾", 48: "雾凇", 51: "小毛毛雨", 53: "中毛毛雨",
    55: "大毛毛雨", 61: "小雨", 63: "中雨", 65: "大雨",
    71: "小雪", 73: "中雪", 75: "大雪", 80: "阵雨",
    95: "雷暴", 96: "雷暴伴冰雹",
}


async def get_coordinates(city: str) -> tuple[float, float]:
    """通过城市名获取经纬度"""
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            "https://geocoding-api.open-meteo.com/v1/search",
            params={"name": city, "count": 1, "language": "zh"},
        )
        data = resp.json()
        if not data.get("results"):
            raise ValueError(f"找不到城市: {city}")
        result = data["results"][0]
        return result["latitude"], result["longitude"]


async def fetch_weather(city: str) -> WeatherData:
    """获取指定城市的当前天气"""
    lat, lon = await get_coordinates(city)

    async with httpx.AsyncClient() as client:
        resp = await client.get(
            "https://api.open-meteo.com/v1/forecast",
            params={
                "latitude": lat,
                "longitude": lon,
                "current": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code",
            },
        )
        current = resp.json()["current"]
        code = current["weather_code"]

    return WeatherData(
        temperature=current["temperature_2m"],
        humidity=current["relative_humidity_2m"],
        wind_speed=current["wind_speed_10m"],
        description=WMO_CODES.get(code, f"未知代码{code}"),
    )

要点说明: - 使用 httpx.AsyncClient 实现异步HTTP调用,与FastMCP异步架构一致 - Open-Meteo是完全免费的天气API,无需API Key - WMO代码映射为中文天气描述,提升LLM理解准确度


第四步:构建MCP Server核心

创建 src/weather_mcp/server.py,这是整个教程的核心:

"""天气MCP Server - 使用FastMCP暴露三大原语"""

from datetime import datetime
from fastmcp import FastMCP, Context
from .weather import fetch_weather, get_coordinates

# 创建MCP Server实例
mcp = FastMCP(
    "weather-server",
    version="0.1.0",
    description="全球天气查询MCP服务器,支持当前天气和预报",
)


# ============================================================
# Tool:LLM可以主动调用的操作
# ============================================================

@mcp.tool()
async def get_current_weather(city: str) -> str:
    """获取指定城市的当前天气信息。

    Args:
        city: 城市名称(中文或英文),如"北京"、"Tokyo"

    Returns:
        格式化的天气报告字符串
    """
    weather = await fetch_weather(city)
    return (
        f"🌍 {city} 当前天气\n"
        f"🌡️ 温度: {weather.temperature}°C\n"
        f"💧 湿度: {weather.humidity}%\n"
        f"🌬️ 风速: {weather.wind_speed} km/h\n"
        f"📋 天气: {weather.description}\n"
        f"⏰ 更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}"
    )


@mcp.tool()
async def compare_weather(city_a: str, city_b: str) -> str:
    """对比两个城市的天气状况。

    Args:
        city_a: 第一个城市名称
        city_b: 第二个城市名称

    Returns:
        两个城市的天气对比报告
    """
    import asyncio
    weather_a, weather_b = await asyncio.gather(
        fetch_weather(city_a), fetch_weather(city_b)
    )
    diff = weather_a.temperature - weather_b.temperature
    diff_desc = f"{city_a}比{city_b}" + ("热" if diff > 0 else "冷") + f"{abs(diff):.1f}°C"

    return (
        f"📊 {city_a} vs {city_b} 天气对比\n"
        f"{'指标':<8} {city_a:<12} {city_b:<12}\n"
        f"{'温度':<8} {weather_a.temperature}°C{'':<6} {weather_b.temperature}°C\n"
        f"{'湿度':<8} {weather_a.humidity}%{'':<7} {weather_b.humidity}%\n"
        f"{'风速':<8} {weather_a.wind_speed}km/h{'':<3} {weather_b.wind_speed}km/h\n"
        f"{'天气':<8} {weather_a.description:<12} {weather_b.description:<12}\n"
        f"📌 {diff_desc}"
    )


# ============================================================
# Resource:LLM可以读取的数据源
# ============================================================

@mcp.resource("weather://supported-cities")
def get_supported_cities() -> str:
    """返回支持查询的城市列表"""
    cities = [
        "北京", "上海", "广州", "深圳", "成都",
        "Tokyo", "Seoul", "New York", "London", "Paris",
    ]
    return "支持查询的城市:\n" + "\n".join(f"  - {c}" for c in cities)


@mcp.resource("weather://api-status")
def get_api_status() -> str:
    """返回天气API服务状态"""
    return (
        f"天气服务状态报告\n"
        f"  API: Open-Meteo (免费)\n"
        f"  状态: 正常运行\n"
        f"  数据源: 全球气象站\n"
        f"  更新频率: 15分钟\n"
        f"  查询时间: {datetime.now().isoformat()}"
    )


# ============================================================
# Prompt:可复用的提示词模板
# ============================================================

@mcp.prompt()
def weather_report_prompt(city: str, format: str = "detailed") -> str:
    """生成天气报告请求的提示词模板

    Args:
        city: 目标城市
        format: 报告格式 (detailed/brief/travel)
    """
    templates = {
        "detailed": (
            f"请查询{city}的当前天气,然后基于数据生成一份详细天气报告。"
            f"报告应包含:温度体感分析、穿衣建议、出行提示。"
        ),
        "brief": f"查询{city}天气,用一句话总结当前状况。",
        "travel": (
            f"查询{city}天气,从旅行角度给出建议:"
            f"适合户外活动吗?需要带伞吗?推荐穿着?"
        ),
    }
    return templates.get(format, templates["detailed"])


@mcp.prompt()
def travel_planning_prompt(
    destination: str, days: int = 3, activities: str = "观光"
) -> str:
    """旅行规划提示词模板

    Args:
        destination: 目的地城市
        days: 旅行天数
        activities: 主要活动类型
    """
    return (
        f"我计划去{destination}旅行{days}天,主要活动是{activities}。"
        f"请查询当地天气,然后给出:\n"
        f"1. 每日穿衣建议\n"
        f"2. 是否需要带雨具\n"
        f"3. 适合/不适合的户外活动\n"
        f"4. 天气相关的安全提示"
    )


# ============================================================
# 入口点
# ============================================================

def main():
    """启动MCP Server"""
    mcp.run()


if __name__ == "__main__":
    main()

关键设计解析

flowchart TB
    subgraph FastMCP["FastMCP Server"]
        direction TB

        subgraph Tools["Tool 原语"]
            T1["get_current_weather"]
            T2["compare_weather"]
        end

        subgraph Resources["Resource 原语"]
            R1["weather://supported-cities"]
            R2["weather://api-status"]
        end

        subgraph Prompts["Prompt 原语"]
            P1["weather_report_prompt"]
            P2["travel_planning_prompt"]
        end
    end

    subgraph Business["业务逻辑层"]
        B1["fetch_weather()"]
        B2["get_coordinates()"]
    end

    subgraph External["外部API"]
        E1["Open-Meteo Forecast"]
        E2["Open-Meteo Geocoding"]
    end

    Tools --> Business
    Business --> External

    style FastMCP fill:#e8f4e8,stroke:#2d5a2d
    style Tools fill:#fff3cd,stroke:#856404
    style Resources fill:#d1ecf1,stroke:#0c5460
    style Prompts fill:#f8d7da,stroke:#721c24

装饰器约定:

装饰器 函数签名要求 返回值 LLM调用方式
@mcp.tool() 参数自动推断为schema str 或 JSON 主动调用
@mcp.resource(uri) URI模板参数 str 被动读取
@mcp.prompt() 模板参数 str 选择使用

Docstring是关键:FastMCP自动从函数的docstring和类型注解生成MCP schema。LLM通过schema理解Tool的用途并决定何时调用。因此docstring必须清晰描述: 1. 函数做什么 2. 每个参数的含义和取值范围 3. 返回内容的格式


第五步:本地调试与验证

使用FastMCP CLI测试

# 开发模式运行(带Inspector UI)
uv run fastmcp dev src/weather_mcp/server.py

# 直接运行stdio模式
uv run fastmcp run src/weather_mcp/server.py

fastmcp dev 会自动打开MCP Inspector Web界面(默认 http://localhost:5173),你可以: - 可视化查看所有已注册的Tool/Resource/Prompt - 手动调用Tool查看返回结果 - 检查生成的JSON Schema是否正确

命令行直接测试

# 通过echo发送JSON-RPC请求测试
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | \
  uv run fastmcp run src/weather_mcp/server.py

# 测试tool调用
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_current_weather","arguments":{"city":"北京"}}}' | \
  uv run fastmcp run src/weather_mcp/server.py

编写单元测试

创建 tests/test_server.py:

"""MCP Server单元测试"""
import pytest
from unittest.mock import AsyncMock, patch
from weather_mcp.weather import fetch_weather, WeatherData, WMO_CODES
from weather_mcp.server import mcp


@pytest.fixture
def mock_weather_data():
    return WeatherData(
        temperature=25.0,
        humidity=60.0,
        wind_speed=10.0,
        description="晴朗",
    )


@pytest.mark.asyncio
async def test_fetch_weather(mock_weather_data):
    with patch("weather_mcp.weather.get_coordinates", return_value=(39.9, 116.4)):
        with patch("weather_mcp.weather.httpx.AsyncClient") as mock_client:
            instance = mock_client.return_value.__aenter__.return_value
            instance.get.return_value.json.return_value = {
                "current": {
                    "temperature_2m": 25.0,
                    "relative_humidity_2m": 60.0,
                    "wind_speed_10m": 10.0,
                    "weather_code": 0,
                }
            }
            result = await fetch_weather("北京")
            assert result.temperature == 25.0
            assert result.description == "晴朗"


def test_wmo_codes_completeness():
    assert 0 in WMO_CODES
    assert 95 in WMO_CODES
    assert len(WMO_CODES) >= 15


def test_server_has_tools():
    """验证Server注册了正确的工具"""
    # FastMCP内部维护工具列表
    tool_names = [t.name for t in mcp._tool_manager._tools.values()]
    assert "get_current_weather" in tool_names
    assert "compare_weather" in tool_names

运行测试:

uv add --dev pytest pytest-asyncio
uv run pytest tests/ -v

第六步:接入Claude Code

stdio模式(本地开发)

在项目根目录创建 .mcp.json(项目级配置):

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": ["run", "fastmcp", "run", "src/weather_mcp/server.py"],
      "cwd": "/absolute/path/to/weather-mcp-server"
    }
  }
}

或使用Claude Code CLI命令注册:

# 在项目目录内执行
claude mcp add weather -- uv run fastmcp run src/weather_mcp/server.py

# 验证注册成功
claude mcp list

# 测试调用
claude -p "查询北京当前天气"

Claude Code会自动: 1. 发现 .mcp.json 中的服务器配置 2. 以子进程方式启动MCP Server(stdio传输) 3. 通过JSON-RPC协商能力并注册Tool/Resource/Prompt 4. 在对话中根据需要调用Tool

Claude Desktop配置

编辑 ~/Library/Application Support/Claude/claude_desktop_config.json(macOS)或对应路径:

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/weather-mcp-server", "fastmcp", "run", "src/weather_mcp/server.py"]
    }
  }
}

第七步:Streamable HTTP模式(远程部署)

MCP 2025-03-26规范引入了Streamable HTTP传输,适合远程部署场景。

修改Server支持HTTP模式

FastMCP内置HTTP传输支持,只需更改启动方式:

# server.py 底部的main函数改为:

def main():
    """启动MCP Server - 支持多种传输"""
    import argparse

    parser = argparse.ArgumentParser(description="Weather MCP Server")
    parser.add_argument(
        "--transport",
        choices=["stdio", "streamable-http"],
        default="stdio",
        help="传输模式",
    )
    parser.add_argument("--host", default="0.0.0.0")
    parser.add_argument("--port", type=int, default=8000)
    args = parser.parse_args()

    if args.transport == "stdio":
        mcp.run()  # 默认stdio
    else:
        mcp.run(transport="streamable-http", host=args.host, port=args.port)

远程MCP配置

{
  "mcpServers": {
    "weather-remote": {
      "type": "http",
      "url": "http://your-server:8000/mcp"
    }
  }
}

或使用CLI:

claude mcp add weather-remote --transport http --url http://your-server:8000/mcp

第八步:Docker容器化部署

多阶段Dockerfile

# ---- 构建阶段 ----
FROM python:3.12-slim AS builder

RUN pip install uv

WORKDIR /app

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev

COPY src/ src/

# ---- 运行阶段 ----
FROM python:3.12-slim

WORKDIR /app

COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/src /app/src

ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD python -c "import httpx; httpx.get('http://localhost:8000/health')" || exit 1

EXPOSE 8000

CMD ["python", "-m", "weather_mcp.server", "--transport", "streamable-http", "--host", "0.0.0.0", "--port", "8000"]

Docker Compose(含健康检查)

# docker-compose.yml
services:
  weather-mcp:
    build: .
    ports:
      - "8000:8000"
    environment:
      - PYTHONUNBUFFERED=1
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8000/health')"]
      interval: 30s
      timeout: 5s
      retries: 3

构建与部署

# 构建镜像
docker build -t weather-mcp-server:0.1.0 .

# 本地测试stdio模式
docker run --rm -i weather-mcp-server:0.1.0 \
  python -m weather_mcp.server --transport stdio

# 启动HTTP服务
docker compose up -d

# 验证服务运行
curl http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}'

Claude Code连接Docker化服务

# 添加远程MCP Server
claude mcp add weather-docker \
  --transport http \
  --url http://localhost:8000/mcp

# 测试
claude -p "对比北京和上海的天气"

第九步:高级模式——Context与进度报告

FastMCP支持通过 Context 对象访问MCP协议级能力:

from fastmcp import FastMCP, Context

mcp = FastMCP("advanced-weather")


@mcp.tool()
async def batch_weather_report(
    cities: list[str], ctx: Context
) -> str:
    """批量查询多个城市的天气,带进度报告。

    Args:
        cities: 城市名称列表
    """
    results = []
    total = len(cities)

    for i, city in enumerate(cities):
        # 发送进度通知给客户端
        await ctx.report_progress(i + 1, total)

        # 记录日志
        await ctx.info(f"正在查询 {city} ({i+1}/{total})...")

        try:
            weather = await fetch_weather(city)
            results.append(f"✅ {city}: {weather.temperature}°C, {weather.description}")
        except Exception as e:
            results.append(f"❌ {city}: 查询失败 - {e}")

    return "\n".join(results)


@mcp.tool()
async def weather_with_logging(city: str, ctx: Context) -> str:
    """查询天气并记录详细日志"""
    await ctx.debug("开始查询流程")
    await ctx.info(f"目标城市: {city}")

    weather = await fetch_weather(city)

    await ctx.warning(f"温度 {'偏高' if weather.temperature > 35 else '正常'}")
    return f"{city}: {weather.temperature}°C, {weather.description}"

Context 提供的方法:

方法 用途 典型场景
ctx.report_progress(current, total) 发送进度通知 批量操作、长时间任务
ctx.info(msg) / ctx.debug(msg) 发送日志 调试、审计
ctx.warning(msg) / ctx.error(msg) 发送警告/错误 异常处理
ctx.read_resource(uri) 读取其他Resource Resource间组合

第十步:生产就绪——错误处理与安全

统一错误处理

from fastmcp import FastMCP
import logging

mcp = FastMCP("production-weather")
logger = logging.getLogger("weather-mcp")


@mcp.tool()
async def safe_weather_query(city: str) -> str:
    """安全查询天气,内置错误处理。

    Args:
        city: 城市名称
    """
    # 输入验证
    if not city or len(city.strip()) == 0:
        return "错误:城市名称不能为空"

    if len(city) > 100:
        return "错误:城市名称过长(最多100字符)"

    try:
        weather = await fetch_weather(city.strip())
        return (
            f"{city}: {weather.temperature}°C, {weather.description}\n"
            f"湿度: {weather.humidity}%, 风速: {weather.wind_speed}km/h"
        )
    except ValueError as e:
        logger.warning(f"城市未找到: {city} - {e}")
        return f"未找到城市 '{city}',请检查拼写或尝试英文名称"
    except httpx.TimeoutException:
        logger.error(f"API超时: {city}")
        return f"查询 '{city}' 超时,请稍后重试"
    except Exception as e:
        logger.exception(f"未知错误: {city}")
        return f"查询失败: {e}"

安全最佳实践清单

flowchart LR
    A[请求到达] --> B{输入验证}
    B -->|通过| C{速率限制}
    B -->|失败| E[返回错误]
    C -->|通过| D{执行操作}
    C -->|超限| E
    D --> F{敏感数据过滤}
    F --> G[返回结果]

    style B fill:#fff3cd
    style C fill:#fff3cd
    style F fill:#f8d7da

关键安全措施:

  1. 输入验证:检查所有参数的类型、长度、格式
  2. 速率限制:对外部API调用设置频率上限
  3. 超时控制:所有HTTP调用设置合理超时
  4. 日志脱敏:不记录敏感数据(用户位置偏好等)
  5. 最小权限:Tool只暴露必要的能力,不提供任意文件读写
# 超时与重试配置示例
httpx_client = httpx.AsyncClient(
    timeout=httpx.Timeout(10.0, connect=5.0),
    limits=httpx.Limits(max_connections=20, max_keepalive_connections=5),
)

完整实战案例:旅行助手MCP Server

将以上所有知识整合为一个完整项目,支持天气查询、城市对比、旅行建议:

"""
travel_assistant.py - 旅行助手MCP Server
功能:天气查询 + 穿衣建议 + 行程规划提示
"""

import asyncio
from datetime import datetime
from fastmcp import FastMCP, Context
from weather_mcp.weather import fetch_weather

mcp = FastMCP(
    "travel-assistant",
    instructions=(
        "你是一个旅行天气助手。用户可能询问天气、请求穿衣建议、或规划旅行。"
        "优先使用 get_current_weather 获取数据,再用 travel_advice 生成建议。"
    ),
)


# ---------- 穿衣建议逻辑 ----------

CLOTHING_ADVICE = {
    "freezing": (lambda t: t < 0, "厚重羽绒服、围巾、手套、帽子"),
    "cold": (lambda t: t < 10, "厚外套或羽绒服"),
    "cool": (lambda t: t < 18, "薄外套或夹克"),
    "mild": (lambda t: t < 25, "长袖衬衫或薄外套"),
    "warm": (lambda t: t < 32, "短袖、轻薄衣物"),
    "hot": (lambda t: t < 38, "轻薄透气衣物,注意防晒"),
    "extreme": (lambda t: True, "极端高温,减少外出,充分补水"),
}

RAIN_KEYWORDS = {"小雨", "中雨", "大雨", "阵雨", "毛毛雨", "雷暴"}
SNOW_KEYWORDS = {"小雪", "中雪", "大雪"}


def _get_clothing(temp: float) -> str:
    for _, (check, advice) in CLOTHING_ADVICE.items():
        if check(temp):
            return advice
    return "根据体感穿着"


# ---------- Tools ----------

@mcp.tool()
async def get_current_weather(city: str) -> str:
    """获取城市当前天气"""
    weather = await fetch_weather(city)
    return (
        f"🌡️ 温度: {weather.temperature}°C\n"
        f"💧 湿度: {weather.humidity}%\n"
        f"🌬️ 风速: {weather.wind_speed}km/h\n"
        f"📋 天气: {weather.description}"
    )


@mcp.tool()
async def travel_advice(city: str, days: int = 1) -> str:
    """根据天气生成旅行建议。

    Args:
        city: 目的地城市
        days: 旅行天数(影响行李建议)
    """
    weather = await fetch_weather(city)
    clothing = _get_clothing(weather.temperature)
    need_umbrella = any(kw in weather.description for kw in RAIN_KEYWORDS)
    need_warm = any(kw in weather.description for kw in SNOW_KEYWORDS)

    tips = [f"📍 {city} 旅行建议({days}天行程)", ""]
    tips.append(f"🌡️ 当前温度 {weather.temperature}°C")
    tips.append(f"👔 推荐穿着: {clothing}")

    if need_umbrella:
        tips.append("☂️ 建议: 带雨伞/雨衣")
    if need_warm:
        tips.append("🧣 建议: 注意保暖,路面可能结冰")
    if weather.temperature > 35:
        tips.append("⚠️ 高温预警: 多喝水,避免长时间户外活动")
    if weather.wind_speed > 30:
        tips.append("💨 大风预警: 注意安全,避免高空作业")

    if days > 1:
        tips.append(f"🧳 行李建议: 准备{days}天的换洗衣物" +
                    ("和雨具" if need_umbrella else ""))

    return "\n".join(tips)


@mcp.tool()
async def multi_city_weather(cities: str, ctx: Context) -> str:
    """批量查询多个城市的天气。

    Args:
        cities: 逗号分隔的城市列表,如"北京,上海,广州"
    """
    city_list = [c.strip() for c in cities.split(",") if c.strip()]
    total = len(city_list)
    results = []

    for i, city in enumerate(city_list):
        await ctx.report_progress(i + 1, total)
        await ctx.info(f"查询 {city} ({i+1}/{total})")
        try:
            weather = await fetch_weather(city)
            results.append(f"{'✅':<3} {city:<8} {weather.temperature:>6.1f}°C  {weather.description}")
        except Exception as e:
            results.append(f"{'❌':<3} {city:<8} 查询失败: {e}")

    header = f"{'状态':<3} {'城市':<8} {'温度':<9} 天气"
    return header + "\n" + "-" * 40 + "\n" + "\n".join(results)


# ---------- Resources ----------

@mcp.resource("travel://clothing-guide")
def clothing_guide() -> str:
    """穿衣指南参考"""
    guide = "温度穿衣参考表:\n"
    for name, (check, advice) in CLOTHING_ADVICE.items():
        guide += f"  {advice}\n"
    return guide


# ---------- Prompts ----------

@mcp.prompt()
def plan_trip_prompt(
    destination: str, 
    duration: int = 3,
    season: str = "不限",
    budget: str = "中等",
) -> str:
    """生成旅行规划提示词

    Args:
        destination: 目的地
        duration: 天数
        season: 季节偏好
        budget: 预算级别
    """
    return (
        f"帮我规划一次{destination}的{duration}天旅行。\n"
        f"季节偏好: {season}\n"
        f"预算级别: {budget}\n\n"
        f"请先查询当地天气,然后基于天气情况:\n"
        f"1. 推荐每日穿着\n"
        f"2. 列出适合/不适合的户外活动\n"
        f"3. 给出天气相关的注意事项\n"
        f"4. 推荐每日行程安排"
    )


if __name__ == "__main__":
    mcp.run()

注册到Claude Code

claude mcp add travel-assistant -- uv run python src/weather_mcp/travel_assistant.py

# 测试
claude -p "帮我规划一个北京3天旅行,注意天气情况"

Claude Code会自动: 1. 发现 get_current_weather Tool并查询北京天气 2. 发现 travel_advice Tool并获取穿衣/出行建议 3. 发现 plan_trip_prompt Prompt并组织完整回复 4. 读取 travel://clothing-guide Resource补充参考


全流程总结

flowchart TB
    Start["项目初始化<br/>uv init + fastmcp"] --> Code["编写业务逻辑<br/>weather.py"]
    Code --> Server["构建MCP Server<br/>三大原语装饰器"]
    Server --> Test["本地调试<br/>fastmcp dev"]
    Test --> Local["Claude Code接入<br/>.mcp.json"]
    Local --> Docker["Docker容器化<br/>多阶段构建"]
    Docker --> Remote["远程部署<br/>Streamable HTTP"]
    Remote --> Prod["生产就绪<br/>错误处理+安全"]

    style Start fill:#e8f5e9
    style Server fill:#fff3cd
    style Docker fill:#e3f2fd
    style Prod fill:#fce4ec

常见问题速查

问题 原因 解决方案
claude mcp list 不显示 配置文件位置错误 确认 .mcp.json 在项目根目录
Tool调用返回超时 外部API响应慢 设置 httpx.Timeout,建议10秒
中文参数乱码 Python编码问题 确保 PYTHONUTF8=1 环境变量
Docker内无法访问API 网络配置 Docker Compose加 network_mode: host
fastmcp 命令不存在 未正确安装 uv add fastmcp 后用 uv run
Claude Code不调用Tool docstring不清晰 重写docstring,明确Args和功能描述
Resource读取失败 URI格式不对 检查 scheme://path 格式,无空格

调试技巧

# 1. 查看MCP Server的完整日志
claude --mcp-debug -p "查询天气"

# 2. 验证Server生成的Schema
uv run python -c "
from weather_mcp.server import mcp
import json
tools = mcp._tool_manager._tools
for name, tool in tools.items():
    print(f'Tool: {name}')
    print(json.dumps(tool.parameters_schema, indent=2, ensure_ascii=False))
"

# 3. 单独测试业务逻辑
uv run python -c "
import asyncio
from weather_mcp.weather import fetch_weather
result = asyncio.run(fetch_weather('北京'))
print(result)
"

延伸阅读


参考来源

  1. FastMCP: Build Custom MCP Servers | Claude Code Skill — FastMCP框架核心功能与Claude Code集成指南
  2. Build an MCP Server in Python with FastMCP - The Python Code — 完整的Python MCP Server构建教程,含Tool/Resource/Prompt示例
  3. How to Build an MCP Server with Python, Docker, and Claude Code — Docker容器化MCP Server部署实战
  4. Connect Claude Code to tools via MCP - Claude Code Docs — Claude Code官方MCP配置与接入文档
  5. FastMCP Python Server Guide 2026 | AI Workflow Lab — FastMCP 3.0生产级服务器开发指南

训练营适用性评估

维度 评估
适合训练营类型 Python全栈训练营、AI工具开发训练营、DevOps实践训练营
预计教学时长 4-6小时(含动手实践)
难度级别 中级(需要Python异步编程基础)
前置知识 Python 3.10+、async/await、HTTP API、Docker基础
硬件要求 任意开发机,需联网(调用Open-Meteo API)
可拆分模块 步骤1-5为基础篇(2h),步骤6-8为部署篇(2h),步骤9-10为进阶篇(2h)

[!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 等场景全覆盖。

51学通信知识星球海报-扫码加入