Skip to main content
Glama
Jayleonc

Weather MCP

by Jayleonc
README.md
# weather-mcp

`weather-mcp` 是一个独立的 **Capability Service(能力服务)**,对外提供“天气查询”这一 MCP 能力。

## 为什么要拆成 weather-mcp

在已有后台系统中:

- `admin`:唯一的用户与认证来源(登录、签发 Token、`/api/v1/auth/me`)
- `weather-mcp`:只负责能力提供,不做用户系统

拆分的目标:

- 清晰边界:避免多套用户体系
- 能力可复用:MCP 层独立于 HTTP,可被编排/复用
- 可演进:未来接入更多 MCP(空气质量/预报/第三方数据源)

## 与 admin 的关系(文字关系图)

- Client -> `weather-mcp`:携带 `Authorization: Bearer <JWT>`
- `weather-mcp`:验证 Token 是否可信(第一版为本地 HS256 验签)
- `admin`:身份真实性与用户语义的权威来源

## 鉴权设计

第一版采用 **方案 A:本地 HS256 验签**(与 `admin` 使用同一套 `SECRET_KEY`)。

- Header 读取:`Authorization: Bearer <token>`
- 解析规则:
  - 算法:HS256
  - `sub`:`user_id`
  - 不依赖 `roles/scopes`

工程化要求:

- 鉴权逻辑在 `app/core/auth/` 与 `app/auth_client/` 中
- API 层通过 `Depends(get_auth_context)` 获取 `AuthContext`
- handler 不直接处理 token、不写鉴权逻辑

可替换性:

- 通过 `WEATHER_MCP_AUTH_MODE` 切换
  - `local_jwt`:本地验签
  - `admin_introspect`:调用 admin 的 `GET /api/v1/auth/me`(已预留实现)

## MCP 设计理念

MCP 是独立于 HTTP 的“能力协议层”:

- 输入输出是结构化 DTO(Pydantic Model)
- 不依赖 FastAPI `Request`
- 易于单测与复用

目录:

- `app/mcp/`:MCP 抽象与实现(例如 `WeatherMCP`)
- `app/schemas/`:MCP 输入输出结构(与 HTTP 无关)
- `app/api/`:HTTP API 层(校验/鉴权/调用 MCP)

## 运行

### 依赖

使用 `uv` 管理依赖:

- `uv sync`

### 环境变量

- `WEATHER_MCP_SECRET_KEY`:与 admin 一致的 HS256 对称密钥
- `WEATHER_MCP_AUTH_MODE`:默认 `local_jwt`

和风天气(QWeather)Web API:

- `WEATHER_MCP_QWEATHER_API_HOST`:你的 QWeather API Host(在 QWeather Console -> Setting 查看)
- `WEATHER_MCP_QWEATHER_API_KEY`:你的 QWeather API Key(务必只放在环境变量/密钥管理中)
- `WEATHER_MCP_QWEATHER_TIMEOUT_SECONDS`:HTTP 超时(秒)

### 启动

- `uv run uvicorn app.main:app --reload --port 9001`

## API

- `GET /health`

### MCP 能力:实时天气(Current Weather)

本服务通过 QWeather `GET /v7/weather/now` 提供实时天气能力,并将返回结构映射成统一的 MCP 输出。

#### 1) MCP 直连路由(兼容/调试)

- `GET /mcp/weather?location=...&lang=...&unit=m|i`

说明:

- `location` 支持多种输入形式(统一为一个字符串参数):
  - 城市名称(例如 `北京` / `Beijing`)
  - 经纬度(格式 `longitude,latitude`,例如 `116.41,39.92`)
  - Location ID(和风天气城市码,例如 `101010100`)
- `lang`:天气描述语言(默认 `zh`,例如返回 `多云`)
- `unit`:单位(`m`=metric,`i`=imperial;默认 `m`)

城市名解析策略:

- 当 `location` 不是纯数字(LocationID)且不是 `lon,lat` 时,会先调用 QWeather GeoAPI `GET /geo/v2/city/lookup` 做解析,取 Top-1 结果。
- 解析时会使用英文 `geo_lang=en` 获取标准化地点名称(例如输出 `Beijing`),而天气接口仍使用 `lang=zh` 以保证天气状态中文显示。

#### 2) Tool 路由(推荐对外使用)

- `POST /tools/weather.current`

请求示例:

```
POST /tools/weather.current
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "location": "北京"
}
```

返回示例:

```json
{
  "location": "Beijing",
  "observed_at": "2025-12-13T15:00:00+08:00",
  "temperature": 12,
  "condition": "多云",
  "humidity": 55,
  "wind_speed": 5.4,
  "wind_dir": "西北风",
  "raw": { "code": "200", "now": { "...": "..." } }
}
```

字段映射说明(QWeather -> MCP):

- `now.obsTime` -> `observed_at`
- `now.temp` -> `temperature`
- `now.text` -> `condition`
- `now.humidity` -> `humidity`
- `now.windSpeed` -> `wind_speed`
- `now.windDir` -> `wind_dir`
- 完整原始响应 -> `raw`

错误处理(HTTP 层):

- 401:缺少/无效 JWT(由 `get_auth_context` 处理)
- 500:服务端缺少 QWeather 配置(host/key)
- 400:无效地点(上游返回 400/404)
- 429:上游限流
- 502:上游网络或权限问题(QWeather 返回 401/403 等)

### MCP 能力:每日天气预报(Daily Forecast)

本服务通过 QWeather `GET /v7/weather/{days}` 提供每日天气预报能力,并将返回结构映射成统一的 MCP 输出。

#### Tool 路由

- `POST /tools/weather.forecast.daily`

请求示例:

```
POST /tools/weather.forecast.daily
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "location": "北京",
  "days": 3,
  "lang": "zh",
  "unit": "m"
}
```

参数说明:

- `location`:城市名称 / `lon,lat` / LocationID
- `days`:仅支持 `3|7|10|15`(对应上游 `3d/7d/10d/15d`)
- `lang`:天气描述语言(默认 `zh`)
- `unit`:单位(`m`=metric,`i`=imperial;默认 `m`)

返回示例:

```json
{
  "location": "Beijing",
  "forecasts": [
    {
      "date": "2025-12-14",
      "temp_max": 8,
      "temp_min": -2,
      "condition_day": "多云",
      "condition_night": "晴",
      "humidity": 35,
      "wind_speed": 10,
      "wind_dir": "西北风"
    }
  ],
  "raw": { "code": "200", "daily": [{ "...": "..." }] }
}
```

字段映射说明(QWeather -> MCP):

- `daily.fxDate` -> `forecasts[].date`
- `daily.tempMax` -> `forecasts[].temp_max`
- `daily.tempMin` -> `forecasts[].temp_min`
- `daily.textDay` -> `forecasts[].condition_day`
- `daily.textNight` -> `forecasts[].condition_night`
- `daily.humidity` -> `forecasts[].humidity`
- `daily.windSpeedDay` -> `forecasts[].wind_speed`
- `daily.windDirDay` -> `forecasts[].wind_dir`
- 完整原始响应 -> `raw`

### MCP 能力:每小时天气预报(Hourly Forecast)

本服务通过 QWeather `GET /v7/weather/{hours}` 提供每小时天气预报能力,并将返回结构映射成统一的 MCP 输出。

#### Tool 路由

- `POST /tools/weather.forecast.hourly`

请求示例:

```
POST /tools/weather.forecast.hourly
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "location": "北京",
  "hours": 24,
  "lang": "zh",
  "unit": "m"
}
```

参数说明:

- `location`:城市名称 / `lon,lat` / LocationID
- `hours`:仅支持 `24|72|168`(对应上游 `24h/72h/168h`)
- `lang`:天气描述语言(默认 `zh`)
- `unit`:单位(`m`=metric,`i`=imperial;默认 `m`)

返回示例:

```json
{
  "location": "Beijing",
  "hourly": [
    {
      "time": "2025-12-13T18:00:00+08:00",
      "temperature": 2,
      "condition": "多云",
      "wind_speed": 12,
      "wind_dir": "北风",
      "humidity": 40
    }
  ],
  "raw": { "code": "200", "hourly": [{ "...": "..." }] }
}
```

字段映射说明(QWeather -> MCP):

- `hourly.fxTime` -> `hourly[].time`
- `hourly.temp` -> `hourly[].temperature`
- `hourly.text` -> `hourly[].condition`
- `hourly.windSpeed` -> `hourly[].wind_speed`
- `hourly.windDir` -> `hourly[].wind_dir`
- `hourly.humidity` -> `hourly[].humidity`
- 完整原始响应 -> `raw`

### MCP 能力:空气质量(Current Air Quality)

本服务通过 QWeather Air Quality v1 `GET /airquality/v1/current/{latitude}/{longitude}` 提供空气质量能力。

说明:

- 该能力需要经纬度。若传入的是城市名或 LocationID,会先调用 GeoAPI 获取 `lat/lon`。

#### Tool 路由

- `POST /tools/air_quality.current`

请求示例:

```
POST /tools/air_quality.current
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "location": "北京"
}
```

返回示例:

```json
{
  "location": "Beijing",
  "aqi": 46,
  "category": "Good",
  "primary_pollutant": "PM 2.5",
  "pm2p5": 11.0,
  "pm10": 12.0,
  "o3": 0.02,
  "no2": 6.77,
  "so2": 1.0,
  "co": 0.25,
  "raw": { "indexes": [{ "...": "..." }], "pollutants": [{ "...": "..." }] }
}
```

字段映射说明(QWeather -> MCP):

- `indexes[0].aqi` -> `aqi`(优先选择 `code=us-epa`)
- `indexes[0].category` -> `category`
- `indexes[0].primaryPollutant.name`(或 `.code`)-> `primary_pollutant`
- `pollutants[].concentration.value`(按 `code` 匹配)-> `pm2p5/pm10/o3/no2/so2/co`
- 完整原始响应 -> `raw`

### MCP 能力:极端天气预警(Weather Alerts)

本服务通过 QWeather `GET /v7/warning/now` 提供天气预警能力。

说明:

- QWeather 文档中标注该接口为“弃用”,但目前仍可按现有返回结构使用。
- 当查询地区当前没有预警信息时,上游返回 `warning=[]`,本服务返回 `alerts=[]`。

#### Tool 路由

- `POST /tools/weather.alerts`

请求示例:

```
POST /tools/weather.alerts
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "location": "北京",
  "lang": "zh"
}
```

返回示例:

```json
{
  "location": "Beijing",
  "alerts": [
    {
      "type": "大风",
      "level": "Blue",
      "title": "发布大风蓝色预警",
      "description": "预计将出现大风",
      "start": "2025-12-13T10:30:00+08:00",
      "end": "2025-12-14T10:30:00+08:00"
    }
  ],
  "raw": { "code": "200", "warning": [{ "...": "..." }] }
}
```

字段映射说明(QWeather -> MCP):

- `warning.typeName`(或 `warning.type`)-> `alerts[].type`
- `warning.severityColor`(或 `warning.severity`)-> `alerts[].level`
- `warning.title` -> `alerts[].title`
- `warning.text` -> `alerts[].description`
- `warning.startTime` -> `alerts[].start`
- `warning.endTime` -> `alerts[].end`
- 完整原始响应 -> `raw`

## 测试

依赖由 `uv` 管理,推荐直接运行:

- `uv run pytest -q`

## 后续演进方向

- 多 MCP:`AirQualityMCP`、`ForecastMCP`
- MCP 编排:一个请求触发多个 MCP,或链式执行
- 内部 RPC / Agent 化:HTTP 之外的复用入口
- 可观测性:日志/trace、限流、缓存与熔断