# 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、限流、缓存与熔断