首页 › tutorials › MCP Server全链路发布:开发到上架实战

MCP Server全链路发布:开发到上架实战

本教程面向有MCP基础概念但尚未发布过自己MCP Server的开发者。你将从零构建一个带完整三原语(Tool/Resource/Prompt)+ Elicitation的MCP Server,经历本地调试、Claude Code集成、Docker容器化、OAuth 2.1安全加固,最终发布到PyPI和官方MCP Registry,实现全球可用。学完后你将掌握MCP Server从idea到用户安装的完整发布工作流。

前置条件

第一步:项目初始化与架构设计

1.1 创建项目结构

mkdir mcp-weather-advisor && cd mcp-weather-advisor
mkdir -p src/mcp_weather_advisor tests
touch src/mcp_weather_advisor/__init__.py

1.2 配置pyproject.toml

创建 pyproject.toml,这是发布的核心配置文件:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mcp-weather-advisor"
version = "0.1.0"
description = "MCP Server providing weather data, forecasts, and clothing recommendations via multiple primitives"
readme = "README.md"
license = "MIT"
requires-python = ">=3.11"
authors = [
    { name = "Your Name", email = "[email protected]" }
]
keywords = ["mcp", "weather", "ai", "claude"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Topic :: Scientific/Engineering :: Atmospheric Science",
]
dependencies = [
    "fastmcp>=3.2",
    "httpx>=0.27",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "pytest-asyncio>=0.24",
    "ruff>=0.8",
]

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

[project.urls]
Homepage = "https://github.com/yourname/mcp-weather-advisor"
Repository = "https://github.com/yourname/mcp-weather-advisor"

关键点说明: - [project.scripts] 定义CLI入口点,安装后可直接运行 mcp-weather-advisor 命令 - fastmcp>=3.2 使用最新版FastMCP,支持Elicitation等新特性 - keywords 中加入 mcp 以便在Registry中被检索

1.3 安装依赖

# 使用 uv(推荐,速度快)
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"

# 或使用 pip
pip install -e ".[dev]"

第二步:实现MCP Server核心功能

2.1 Server主体结构

创建 src/mcp_weather_advisor/server.py:

"""MCP Weather Advisor Server - 全原语演示"""

from datetime import datetime
from typing import Annotated

import httpx
from fastmcp import FastMCP, Context
from pydantic import Field

# 创建MCP Server实例
mcp = FastMCP(
    "weather-advisor",
    version="0.1.0",
    description="天气查询与穿衣建议MCP服务器",
)


# ==================== Primitives 1: Tools ====================

@mcp.tool()
async def get_current_weather(
    city: Annotated[str, Field(description="城市名称,如'北京'、'Shanghai'")],
    units: Annotated[str, Field(description="温度单位:celsius 或 fahrenheit")] = "celsius",
) -> dict:
    """获取指定城市的当前天气信息。

    返回温度、湿度、风速、天气状况等实时数据。
    支持全球主要城市。
    """
    # 使用 wttr.in 免费API(无需API Key)
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://wttr.in/{city}",
            params={"format": "j1"},
            timeout=10.0,
        )
        response.raise_for_status()
        data = response.json()

    current = data["current_condition"][0]
    temp_c = int(current["temp_C"])
    temp_f = int(current["temp_F"])

    return {
        "city": city,
        "temperature": temp_f if units == "fahrenheit" else temp_c,
        "units": units,
        "humidity": current["humidity"],
        "description": current["weatherDesc"][0]["value"],
        "wind_speed_kmh": current["windspeedKmph"],
        "feels_like_c": current["FeelsLikeC"],
        "observation_time": current["observation_time"],
    }


@mcp.tool()
async def get_forecast(
    city: Annotated[str, Field(description="城市名称")],
    days: Annotated[int, Field(description="预报天数(1-3)", ge=1, le=3)] = 3,
) -> list[dict]:
    """获取未来几天的天气预报。

    返回每天的日期、最高/最低温度、天气描述。
    最多支持3天预报。
    """
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://wttr.in/{city}",
            params={"format": "j1"},
            timeout=10.0,
        )
        response.raise_for_status()
        data = response.json()

    forecasts = []
    for day_data in data["weather"][:days]:
        forecasts.append({
            "date": day_data["date"],
            "max_temp_c": day_data["maxtempC"],
            "min_temp_c": day_data["mintempC"],
            "avg_temp_c": day_data["avgtempC"],
            "sun_hours": day_data["sunHour"],
            "uv_index": day_data["uvIndex"],
            "description": day_data["hourly"][4]["weatherDesc"][0]["value"],
        })

    return forecasts


@mcp.tool()
async def get_clothing_advice(
    temperature_c: Annotated[float, Field(description="当前温度(摄氏度)")],
    humidity: Annotated[int, Field(description="当前湿度百分比", ge=0, le=100)] = 50,
    wind_speed_kmh: Annotated[float, Field(description="风速(km/h)")] = 0,
    activity: Annotated[str, Field(description="活动类型:outdoor/commute/exercise/formal")] = "commute",
) -> dict:
    """根据天气条件提供穿衣建议。

    综合考虑温度、湿度、风速和活动类型,
    给出具体的穿衣推荐和注意事项。
    """
    if temperature_c >= 30:
        level = "极热"
        advice = "穿轻薄透气的短袖短裤,注意防晒和补水"
        items = ["防晒霜", "太阳帽", "墨镜", "水壶"]
    elif temperature_c >= 22:
        level = "炎热"
        advice = "短袖T恤搭配薄裤或裙装即可"
        items = ["薄外套(室内空调用)", "防晒用品"]
    elif temperature_c >= 15:
        level = "温暖"
        advice = "长袖衬衫加薄外套,早晚注意加衣"
        items = ["薄外套", "长裤"]
    elif temperature_c >= 5:
        level = "凉爽"
        advice = "穿毛衣或卫衣搭配外套"
        items = ["厚外套", "围巾", "保暖内衣"]
    else:
        level = "寒冷"
        advice = "穿羽绒服或厚棉衣,注意保暖"
        items = ["羽绒服", "帽子", "手套", "保暖内衣", "围巾"]

    # 湿度修正
    if humidity > 80:
        advice += "。湿度较高,建议选择速干面料"
        items.append("雨伞")
    elif humidity < 30:
        advice += "。空气干燥,建议多涂润肤霜"

    # 风速修正
    if wind_speed_kmh > 30:
        advice += "。风力较大,注意防风保暖"
        items.append("防风外套")

    # 活动修正
    activity_tips = {
        "outdoor": "户外活动建议穿运动鞋,携带应急雨具",
        "commute": "通勤建议洋葱式穿法,方便室内脱减",
        "exercise": "运动建议穿排汗速干衣物,运动后及时加衣",
        "formal": "正式场合建议选择商务休闲搭配,注意保暖",
    }

    return {
        "temperature_level": level,
        "clothing_advice": advice,
        "recommended_items": items,
        "activity_tip": activity_tips.get(activity, ""),
    }


# ==================== Primitives 2: Resources ====================

@mcp.resource("weather://cities/supported")
def get_supported_cities() -> str:
    """列出支持的常用城市列表"""
    cities = {
        "中国": ["北京", "上海", "广州", "深圳", "成都", "杭州", "武汉", "南京"],
        "国际": ["Tokyo", "Seoul", "New York", "London", "Paris", "Sydney"],
    }
    import json
    return json.dumps(cities, ensure_ascii=False, indent=2)


@mcp.resource("weather://alerts/{city}")
async def get_weather_alerts(city: str) -> str:
    """获取指定城市的天气预警信息"""
    import json
    # 示例:实际项目中对接气象局API
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://wttr.in/{city}",
            params={"format": "j1"},
            timeout=10.0,
        )
        data = response.json()

    current = data["current_condition"][0]
    alerts = []
    temp = int(current["temp_C"])

    if temp >= 35:
        alerts.append({"type": "高温预警", "level": "橙色", "message": f"当前温度{temp}°C,注意防暑降温"})
    elif temp <= -10:
        alerts.append({"type": "寒潮预警", "level": "蓝色", "message": f"当前温度{temp}°C,注意防寒保暖"})

    if int(current["windspeedKmph"]) >= 60:
        alerts.append({"type": "大风预警", "level": "黄色", "message": "风力较大,减少户外活动"})

    if not alerts:
        alerts.append({"type": "无预警", "level": "正常", "message": f"{city}当前无气象预警"})

    return json.dumps(alerts, ensure_ascii=False, indent=2)


# ==================== Primitives 3: Prompts ====================

@mcp.prompt()
def daily_outfit_prompt(
    city: Annotated[str, Field(description="城市名称")] = "北京",
) -> str:
    """生成每日穿搭建议的提示词模板"""
    return f"""你是一位专业的穿搭顾问。请根据以下信息为用户提供今日穿搭建议:

1. 首先调用 get_current_weather 获取 {city} 的当前天气
2. 然后调用 get_clothing_advice 获取穿搭建议
3. 最后综合天气和穿搭信息,用友好的语气给出建议

请以以下格式输出:
- 🌡️ 今日天气概况
- 👔 穿搭建议(上装、下装、鞋子、配饰)
- ⚠️ 注意事项
- 🎯 今日小贴士"""


@mcp.prompt()
def travel_planning_prompt(
    destination: Annotated[str, Field(description="目的地城市")],
    days: Annotated[int, Field(description="旅行天数")] = 3,
) -> str:
    """生成旅行天气规划提示词模板"""
    return f"""你是一位旅行规划助手。请帮助用户规划前往 {destination} 的 {days} 天旅行:

1. 调用 get_current_weather 获取 {destination} 当前天气
2. 调用 get_forecast 获取未来 {days} 天天气预报
3. 调用 get_clothing_advice 根据天气预报给出每日穿搭建议
4. 调用 get_weather_alerts 查看是否有天气预警

请以以下格式输出:
- 📍 目的地天气概览
- 📅 每日天气详情与穿搭建议
- 🎒 行李清单
- ⚠️ 天气预警与注意事项"""


# ==================== Elicitation:人机交互增强 ====================

@mcp.tool()
async def personalized_weather_report(
    ctx: Context,
) -> dict:
    """生成个性化天气报告。通过Elicitation与用户交互收集偏好。

    该工具演示MCP 2025-11-25规范中的Elicitation能力:
    服务端可以主动向用户请求信息,实现人机协作。
    """
    # 第一步:询问用户的城市偏好
    city_response = await ctx.elicit(
        message="请告诉我您想查看哪个城市的天气?",
        requested_fields={
            "city": {
                "type": "string",
                "description": "城市名称",
                "label": "城市",
            }
        },
    )

    city = city_response.data["city"]

    # 第二步:询问报告偏好
    pref_response = await ctx.elicit(
        message="请选择您需要的报告内容:",
        requested_fields={
            "include_forecast": {
                "type": "boolean",
                "description": "是否包含未来3天预报",
                "label": "包含预报",
            },
            "include_clothing": {
                "type": "boolean",
                "description": "是否包含穿搭建议",
                "label": "包含穿搭建议",
            },
            "temperature_unit": {
                "type": "string",
                "description": "温度单位",
                "label": "温度单位",
                "enum": ["celsius", "fahrenheit"],
            },
        },
    )

    prefs = pref_response.data

    # 获取天气数据
    weather = await get_current_weather(city, prefs.get("temperature_unit", "celsius"))
    result = {"weather": weather}

    if prefs.get("include_forecast"):
        result["forecast"] = await get_forecast(city, 3)

    if prefs.get("include_clothing"):
        result["clothing"] = await get_clothing_advice(
            float(weather["temperature"]),
            int(weather["humidity"]),
            float(weather["wind_speed_kmh"]),
        )

    return result


# ==================== 启动入口 ====================

def main():
    """CLI入口点 - 支持stdio和SSE两种传输模式"""
    mcp.run()


if __name__ == "__main__":
    main()

2.2 代码结构解析

graph TB
    subgraph "MCP Server 三原语 + Elicitation"
        A[Tool 工具] --> A1[get_current_weather<br/>实时天气查询]
        A --> A2[get_forecast<br/>多日天气预报]
        A --> A3[get_clothing_advice<br/>穿衣建议]
        A --> A4[personalized_weather_report<br/>Elicitation交互式报告]

        B[Resource 资源] --> B1[weather://cities/supported<br/>支持城市列表]
        B --> B2[weather://alerts/城市名<br/>天气预警信息]

        C[Prompt 提示词] --> C1[daily_outfit_prompt<br/>每日穿搭提示词]
        C --> C2[travel_planning_prompt<br/>旅行规划提示词]
    end

    subgraph "外部依赖"
        D[wttr.in API<br/>免费天气数据]
        E[FastMCP 3.2+<br/>MCP框架]
    end

    A1 --> D
    A2 --> D
    A4 --> A1
    A4 --> A2
    A4 --> A3

第三步:本地调试与测试

3.1 使用MCP Inspector调试

MCP Inspector是官方提供的可视化调试工具:

# 安装MCP Inspector
npx @anthropic-ai/mcp-inspector

# 启动Inspector并加载你的Server
npx @anthropic-ai/mcp-inspector \
  mcp-weather-advisor

Inspector会打开一个Web界面,你可以: - 查看所有注册的Tools/Resources/Prompts - 手动调用每个Tool并查看返回值 - 测试Elicitation交互流程 - 检查JSON-RPC通信细节

3.2 单元测试

创建 tests/test_server.py:

"""MCP Weather Advisor 单元测试"""

import pytest
from mcp_weather_advisor.server import mcp


@pytest.fixture
def client():
    """创建MCP测试客户端"""
    from fastmcp import Client
    return Client(mcp)


@pytest.mark.asyncio
async def test_list_tools():
    """验证所有Tool已正确注册"""
    async with Client(mcp) as client:
        tools = await client.list_tools()
        tool_names = [t.name for t in tools]

        assert "get_current_weather" in tool_names
        assert "get_forecast" in tool_names
        assert "get_clothing_advice" in tool_names
        assert "personalized_weather_report" in tool_names


@pytest.mark.asyncio
async def test_get_current_weather():
    """测试天气查询Tool"""
    async with Client(mcp) as client:
        result = await client.call_tool("get_current_weather", {
            "city": "Beijing",
            "units": "celsius",
        })

        assert result is not None
        assert result.data["city"] == "Beijing"
        assert "temperature" in result.data
        assert "humidity" in result.data


@pytest.mark.asyncio
async def test_get_clothing_advice_cold():
    """测试穿衣建议 - 寒冷场景"""
    async with Client(mcp) as client:
        result = await client.call_tool("get_clothing_advice", {
            "temperature_c": -5.0,
            "humidity": 40,
            "activity": "outdoor",
        })

        assert result.data["temperature_level"] == "寒冷"
        assert "羽绒服" in result.data["recommended_items"]


@pytest.mark.asyncio
async def test_get_clothing_advice_hot_humid():
    """测试穿衣建议 - 高温高湿场景"""
    async with Client(mcp) as client:
        result = await client.call_tool("get_clothing_advice", {
            "temperature_c": 35.0,
            "humidity": 90,
            "wind_speed_kmh": 5,
        })

        assert result.data["temperature_level"] == "极热"
        assert "雨伞" in result.data["recommended_items"]


@pytest.mark.asyncio
async def test_list_resources():
    """验证Resource已注册"""
    async with Client(mcp) as client:
        resources = await client.list_resources()
        uris = [str(r.uri) for r in resources]

        assert "weather://cities/supported" in uris


@pytest.mark.asyncio
async def test_list_prompts():
    """验证Prompt已注册"""
    async with Client(mcp) as client:
        prompts = await client.list_prompts()
        prompt_names = [p.name for p in prompts]

        assert "daily_outfit_prompt" in prompt_names
        assert "travel_planning_prompt" in prompt_names

运行测试:

# 运行全部测试
pytest tests/ -v

# 仅运行tool测试
pytest tests/test_server.py::test_get_clothing_advice_cold -v

预期输出:

tests/test_server.py::test_list_tools PASSED
tests/test_server.py::test_get_current_weather PASSED
tests/test_server.py::test_get_clothing_advice_cold PASSED
tests/test_server.py::test_get_clothing_advice_hot_humid PASSED
tests/test_server.py::test_list_resources PASSED
tests/test_server.py::test_list_prompts PASSED

3.3 常见调试问题

问题 原因 解决方案
ModuleNotFoundError: fastmcp 虚拟环境未激活 source .venv/bin/activate
Tool注册后Inspector看不到 函数缺少@mcp.tool()装饰器 检查装饰器是否正确
httpx.ConnectError wttr.in被墙或网络问题 配置代理或使用其他天气API
Resource URI不生效 URI格式错误 确保使用 scheme://path 格式
Elicitation不触发 FastMCP版本过低 升级到 fastmcp>=3.2

第四步:接入Claude Code

4.1 配置Claude Code连接本地MCP Server

在项目根目录创建 .claude/settings.json:

{
  "mcpServers": {
    "weather-advisor": {
      "command": "mcp-weather-advisor",
      "args": [],
      "type": "stdio"
    }
  }
}

或者在全局配置 ~/.claude.json 中添加(所有项目可用):

{
  "mcpServers": {
    "weather-advisor": {
      "command": "python",
      "args": ["-m", "mcp_weather_advisor.server"],
      "type": "stdio",
      "cwd": "/path/to/mcp-weather-advisor"
    }
  }
}

4.2 验证连接

启动Claude Code后执行:

/mcp

预期输出:

weather-advisor (stdio) — Connected
  Tools: get_current_weather, get_forecast, get_clothing_advice, personalized_weather_report
  Resources: weather://cities/supported, weather://alerts/{city}
  Prompts: daily_outfit_prompt, travel_planning_prompt

4.3 实际使用测试

在Claude Code对话中输入:

帮我查一下北京今天的天气,并告诉我穿什么衣服

Claude Code会自动调用你的MCP Server中的Tool组合来完成任务。

第五步:Docker容器化

5.1 创建Dockerfile

FROM python:3.12-slim AS builder

WORKDIR /app

# 安装uv加速依赖安装
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

COPY pyproject.toml .
COPY src/ src/

RUN uv venv /app/.venv && \
    uv pip install --python /app/.venv/bin/python .

# 运行阶段
FROM python:3.12-slim

RUN useradd -m -s /bin/bash mcp
USER mcp

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

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

ENTRYPOINT ["mcp-weather-advisor"]

5.2 构建与本地测试

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

# 本地测试运行
docker run --rm mcp-weather-advisor:0.1.0 --help

5.3 在Claude Code中使用Docker版MCP Server

{
  "mcpServers": {
    "weather-advisor": {
      "command": "docker",
      "args": [
        "run", "--rm",
        "-i",
        "mcp-weather-advisor:0.1.0"
      ],
      "type": "stdio"
    }
  }
}

5.4 Docker Compose(多Server场景)

# docker-compose.mcp.yml
services:
  weather-advisor:
    build: .
    ports:
      - "8080:8080"
    environment:
      - MCP_TRANSPORT=sse
      - MCP_PORT=8080
    healthcheck:
      test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8080/health')"]
      interval: 30s
      timeout: 5s
      retries: 3

为支持HTTP模式,在 server.py 底部添加:

def main():
    """支持stdio和HTTP两种传输模式"""
    import os
    transport = os.environ.get("MCP_TRANSPORT", "stdio")

    if transport == "sse":
        port = int(os.environ.get("MCP_PORT", "8080"))
        mcp.run(transport="sse", port=port)
    else:
        mcp.run()  # 默认stdio模式

第六步:发布到PyPI

6.1 构建发布包

# 安装构建工具
pip install build twine

# 构建wheel和source distribution
python -m build

# 检查包质量
twine check dist/*

预期输出:

dist/mcp_weather_advisor-0.1.0-py3-none-any.whl: OK
dist/mcp-weather-advisor-0.1.0.tar.gz: OK

6.2 发布到TestPyPI(先测试)

twine upload --repository testpypi dist/* \
  --username __token__ \
  --password <TESTPYPI_TOKEN>

验证安装:

pip install --index-url https://test.pypi.org/simple/ \
  mcp-weather-advisor==0.1.0

6.3 正式发布到PyPI

twine upload dist/* \
  --username __token__ \
  --password <PYPI_TOKEN>

发布后用户可以直接安装:

pip install mcp-weather-advisor

6.4 自动化发布(GitHub Actions)

创建 .github/workflows/publish.yml:

name: Publish to PyPI

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # Trusted Publishing
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install build tools
        run: pip install build

      - name: Build package
        run: python -m build

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        # 使用Trusted Publishing,无需TOKEN

Trusted Publishing是PyPI推荐的安全发布方式,通过OIDC身份验证,无需在GitHub中存储任何PyPI Token。在PyPI项目设置中配置GitHub repository为Trusted Publisher即可。

第七步:发布到MCP Registry

7.1 创建MCP Server元数据

在项目根目录创建 mcp-metadata.json:

{
  "name": "mcp-weather-advisor",
  "version": "0.1.0",
  "description": "Weather data, forecasts, and clothing recommendations MCP Server",
  "author": {
    "name": "Your Name",
    "url": "https://github.com/yourname"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/yourname/mcp-weather-advisor"
  },
  "installations": {
    "pip": {
      "package": "mcp-weather-advisor",
      "command": "mcp-weather-advisor"
    },
    "docker": {
      "image": "yourname/mcp-weather-advisor:latest",
      "command": "docker",
      "args": ["run", "--rm", "-i", "yourname/mcp-weather-advisor:latest"]
    }
  },
  "tools": [
    {
      "name": "get_current_weather",
      "description": "Get current weather for a city"
    },
    {
      "name": "get_forecast",
      "description": "Get multi-day weather forecast"
    },
    {
      "name": "get_clothing_advice",
      "description": "Get clothing recommendations based on weather"
    },
    {
      "name": "personalized_weather_report",
      "description": "Interactive personalized weather report via Elicitation"
    }
  ],
  "resources": [
    {
      "uri": "weather://cities/supported",
      "description": "List of supported cities"
    },
    {
      "uri": "weather://alerts/{city}",
      "description": "Weather alerts for a city"
    }
  ],
  "prompts": [
    {
      "name": "daily_outfit_prompt",
      "description": "Daily outfit recommendation prompt template"
    },
    {
      "name": "travel_planning_prompt",
      "description": "Travel weather planning prompt template"
    }
  ]
}

7.2 注册到官方MCP Registry

# 使用MCP CLI发布到Registry
npx @anthropic-ai/mcp-registry publish \
  --metadata mcp-metadata.json \
  --package mcp-weather-advisor

或通过Web界面提交:访问 https://registry.modelcontextprotocol.io/ 并按指引提交。

7.3 第三方Registry同步

同时发布到主流MCP目录以增加曝光:

Registry URL 特点
官方 MCP Registry registry.modelcontextprotocol.io Anthropic官方,Claude生态首选
Glama.ai glama.ai/mcp/servers 社区最大,自动索引npm/PyPI
MCPWorld mcpworld.com 评级系统,质量分高
Smithery smithery.ai 自动化安装配置

第八步:安全加固

8.1 输入验证(已通过Pydantic实现)

FastMCP集成了Pydantic进行参数验证。每个Tool的参数都使用 Annotated[type, Field(...)] 声明约束:

@mcp.tool()
async def get_forecast(
    city: Annotated[str, Field(
        description="城市名称",
        min_length=1,
        max_length=100,
    )],
    days: Annotated[int, Field(
        description="预报天数(1-3)",
        ge=1,
        le=3,
    )] = 3,
) -> list[dict]:
    ...

Pydantic会自动拒绝无效输入(如 days=10),返回结构化错误信息。

8.2 Rate Limiting

from datetime import datetime, timedelta
from collections import defaultdict

# 简单的内存级限流
_rate_limits: dict[str, list[datetime]] = defaultdict(list)
RATE_LIMIT = 30  # 每分钟30次
RATE_WINDOW = 60  # 60秒窗口


def check_rate_limit(client_id: str) -> bool:
    """检查是否超过限流阈值"""
    now = datetime.now()
    cutoff = now - timedelta(seconds=RATE_WINDOW)

    # 清理过期记录
    _rate_limits[client_id] = [
        t for t in _rate_limits[client_id] if t > cutoff
    ]

    if len(_rate_limits[client_id]) >= RATE_LIMIT:
        return False

    _rate_limits[client_id].append(now)
    return True


@mcp.tool()
async def get_current_weather(
    city: str,
    units: str = "celsius",
    _client_id: str = "default",
) -> dict:
    if not check_rate_limit(_client_id):
        return {"error": "Rate limit exceeded. Try again later."}
    # ... 原有逻辑

8.3 远程部署安全(OAuth 2.1)

当Server通过HTTP对外暴露时,必须启用认证。MCP 2025-11-25规范引入了标准化OAuth 2.1+PKCE流程:

# FastMCP 内置OAuth支持
mcp = FastMCP(
    "weather-advisor",
    auth_provider=OAuthProvider(
        issuer="https://your-domain.com",
        client_id="weather-advisor-mcp",
        jwks_uri="https://your-domain.com/.well-known/jwks.json",
    ),
)

实战案例:完整发布工作流

以下是将上述所有步骤串联起来的完整工作流:

flowchart LR
    A[1. 代码开发<br/>FastMCP三原语] --> B[2. 本地调试<br/>MCP Inspector]
    B --> C[3. 单元测试<br/>pytest]
    C --> D[4. Claude Code<br/>集成验证]
    D --> E[5. Docker<br/>容器化]
    E --> F[6. 安全加固<br/>OAuth/限流]
    F --> G[7. PyPI<br/>发布Python包]
    G --> H[8. MCP Registry<br/>注册上架]
    H --> I[9. GitHub Release<br/>自动化发布]

    style A fill:#4CAF50,color:#fff
    style D fill:#2196F3,color:#fff
    style G fill:#FF9800,color:#fff
    style H fill:#9C27B0,color:#fff

发布检查清单

发布前逐项确认:

# 1. 代码质量检查
ruff check src/ tests/

# 2. 运行全部测试
pytest tests/ -v --tb=short

# 3. 构建包并检查
python -m build && twine check dist/*

# 4. 验证CLI入口点
mcp-weather-advisor --help

# 5. Docker构建测试
docker build -t mcp-weather-advisor:0.1.0 .

# 6. 检查元数据文件
cat mcp-metadata.json | python -m json.tool

# 7. 确认版本号一致
grep "version" pyproject.toml mcp-metadata.json

性能优化技巧

缓存策略

天气数据变化频率有限,适合缓存:

from functools import lru_cache
from datetime import datetime, timedelta
import json

_cache: dict[str, tuple[datetime, dict]] = {}
CACHE_TTL = timedelta(minutes=15)


async def get_cached_weather(city: str) -> dict:
    """带缓存的天气查询"""
    now = datetime.now()

    if city in _cache:
        cached_time, cached_data = _cache[city]
        if now - cached_time < CACHE_TTL:
            return cached_data

    # 实际请求
    data = await _fetch_weather(city)
    _cache[city] = (now, data)
    return data

错误处理与降级

@mcp.tool()
async def get_current_weather(city: str, units: str = "celsius") -> dict:
    """获取天气信息 - 带降级策略"""
    try:
        return await _fetch_from_wttr(city, units)
    except httpx.TimeoutException:
        # 主API超时,尝试备用源
        try:
            return await _fetch_from_open_meteo(city, units)
        except Exception:
            return {
                "error": "天气服务暂时不可用",
                "city": city,
                "fallback": True,
            }
    except httpx.HTTPStatusError as e:
        return {
            "error": f"天气查询失败: {e.response.status_code}",
            "city": city,
        }

版本迭代策略

MCP Server的版本管理建议遵循语义化版本:

变更类型 版本号 示例
新增Tool/Resource/Prompt minor(0.2.0) 添加 get_air_quality Tool
Tool参数变更(兼容) minor(0.2.0) 为Tool添加可选参数
Tool参数变更(不兼容) major(1.0.0) 修改必选参数类型
修复Bug patch(0.1.1) 修复温度单位转换错误
移除Tool major(1.0.0) 删除废弃的Tool

延伸阅读

训练营适用性评估

维度 评估
适合训练营类型 MCP生态开发训练营、AI工具链训练营、Python后端训练营
预计教学时长 4-6小时(含动手实践)
难度等级 中级(需Python和MCP基础)
学员产出 每人完成一个可安装、可发布的MCP Server
扩展方向 接入真实付费API、添加OAuth认证、多语言SDK

参考来源

  1. MCP官方规范 2025-11-25 — MCP协议最新稳定版完整定义,涵盖三原语、Elicitation、Streamable HTTP传输等
  2. FastMCP PyPI包页面 — FastMCP 3.2+框架,Python MCP Server开发的官方推荐方案
  3. MCP Server打包与分发指南 — 覆盖npm、PyPI、Docker、DXT等多种分发渠道的完整指南
  4. Building MCP Servers in Python: Production Primer 2026 — 生产级MCP Server开发的最佳实践与经验总结
  5. MCP Elicitation教程 — FastMCP官方Elicitation功能文档与人机交互实现指南

[!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学通信知识星球海报-扫码加入