<!-- title: 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到用户安装的完整发布工作流。

## 前置条件

- Python 3.11+、Node.js 20+、Docker
- Claude Code已安装并配置（`npm install -g @anthropic-ai/claude-code`）
- PyPI账号（发布用）、GitHub账号
- 了解MCP基本概念（Client/Server架构、JSON-RPC）

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

### 1.1 创建项目结构

```bash
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`，这是发布的核心配置文件：

```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 = "you@example.com" }
]
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 安装依赖

```bash
# 使用 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`：

```python
"""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 代码结构解析

```mermaid
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是官方提供的可视化调试工具：

```bash
# 安装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`：

```python
"""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
```

运行测试：

```bash
# 运行全部测试
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`：

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

或者在全局配置 `~/.claude.json` 中添加（所有项目可用）：

```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

```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 构建与本地测试

```bash
# 构建镜像
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

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

### 5.4 Docker Compose（多Server场景）

```yaml
# 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` 底部添加：

```python
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 构建发布包

```bash
# 安装构建工具
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（先测试）

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

验证安装：
```bash
pip install --index-url https://test.pypi.org/simple/ \
  mcp-weather-advisor==0.1.0
```

### 6.3 正式发布到PyPI

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

发布后用户可以直接安装：
```bash
pip install mcp-weather-advisor
```

### 6.4 自动化发布（GitHub Actions）

创建 `.github/workflows/publish.yml`：

```yaml
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`：

```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

```bash
# 使用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(...)]` 声明约束：

```python
@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

```python
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流程：

```python
# 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",
    ),
)
```

## 实战案例：完整发布工作流

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

```mermaid
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
```

### 发布检查清单

发布前逐项确认：

```bash
# 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
```

## 性能优化技巧

### 缓存策略

天气数据变化频率有限，适合缓存：

```python
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
```

### 错误处理与降级

```python
@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官方规范 2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25) — 最新协议规范的完整定义
- [FastMCP官方文档](https://fastmcp.wiki/) — FastMCP框架完整API参考
- [MCP Registry发布指南](https://modelcontextprotocol.info/tools/registry/publishing/) — 官方Registry上架流程
- [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) — 无Token安全发布的最佳实践
- [MCP安全最佳实践](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) — OAuth 2.1授权规范

## 训练营适用性评估

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

## 参考来源

1. [MCP官方规范 2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25) — MCP协议最新稳定版完整定义，涵盖三原语、Elicitation、Streamable HTTP传输等
2. [FastMCP PyPI包页面](https://pypi.org/project/fastmcp/) — FastMCP 3.2+框架，Python MCP Server开发的官方推荐方案
3. [MCP Server打包与分发指南](https://chatforest.com/guides/mcp-server-packaging-distribution/) — 覆盖npm、PyPI、Docker、DXT等多种分发渠道的完整指南
4. [Building MCP Servers in Python: Production Primer 2026](https://dev.to/tufailkhan457/building-mcp-servers-in-python-a-production-primer-for-2026-4kh2) — 生产级MCP Server开发的最佳实践与经验总结
5. [MCP Elicitation教程](https://fastmcp.wiki/en/servers/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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
