Metadata-Version: 2.4
Name: waterlog-mcp
Version: 1.4.0
Summary: 涵安智行城市内涝 MCP 服务器：把水位、预警、灾情与避险路线能力包装成 MCP 工具（stdio，零依赖）
Author: 涵安智行
License: MIT
Project-URL: Homepage, https://water.cubestudio.top
Project-URL: Documentation, https://water.cubestudio.top/api-guide#mcp
Keywords: mcp,model-context-protocol,waterlog,flood,agentarts
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# 涵安智行 MCP 服务器（Python / uvx）

把后端 REST API 包成 **MCP（Model Context Protocol）工具**，供支持 MCP 的宿主直接调用：
VS Code（Copilot agent 模式）、Claude Desktop、华为云 AgentArts 的 MCP 接入等。

- **零第三方依赖**（纯标准库），stdio + 换行分隔 JSON-RPC 2.0，Python ≥ 3.9
- **16 个工具**，参数已扁平化、结果已整理，模型不用自己拼 URL
- 不需要任何环境变量：后端地址默认生产域名，凭据**直接写进包内**
- 已按 Python 包组织：云端宿主只接受 JSON 配置 + `npx/uvx`，这里用 **`uvx`** 拉起
- 通行判定（`assess_vehicle_access`）在本地按统一口径计算，不消耗接口

## 云端宿主：两种接法，别选错（这一步最容易 404）

| 接法 | 平台里填什么 | 谁跑进程 | 常见失败 |
| --- | --- | --- | --- |
| **A. stdio（平台拉起进程）** | JSON：`command: uvx` + args | 平台沙箱 | 平台没装 uv / 无外网 → 启动失败 |
| **B. HTTP（托管 MCP 服务）** | **服务地址 + 路径** | 你在服务器/Docker 里跑 | 路径填错 / 没监听 → `404`（如 `.../runtimes/<id>/invocations`） |

**看到 `404 Not Found` / “MCP服务目标地址路径错误” → 你用的是 B，但服务还在 stdio 模式。**
按 [HOSTED-HTTP.md](./HOSTED-HTTP.md) 起 HTTP 传输即可（`--transport http`，路径 `/mcp`）。

### A 模式：stdio，直接用这份 JSON（今天就能用，已验证）

现成的配置文件在仓库根目录（**复制整段贴进平台即可，`env` 可以空着**）：

| 文件 | 现在能用？ | 说明 |
| --- | --- | --- |
| [`mcp-config.testpypi.json`](./mcp-config.testpypi.json) | ✅ **已发布、已验证** | 从 TestPyPI 装 `1.4.0`，需要带 `--index-url` |
| [`mcp-config.vscode.json`](./mcp-config.vscode.json) | ✅ 已验证 | VS Code 形态（`servers` + `type: stdio`），内容同上 |
| [`mcp-config.pypi.json`](./mcp-config.pypi.json) | ⏳ 正式 PyPI 发布后 | 最干净的形态，无 `--index-url` |
| [`mcp-config.wheel.json`](./mcp-config.wheel.json) | ⏳ 上传 wheel 后 | 完全不依赖 PyPI，走自己的静态目录 |

主推这一份（今天就能跑）：

```json
{
  "mcpServers": {
    "waterlog": {
      "command": "uvx",
      "args": [
        "--index-url",
        "https://test.pypi.org/simple",
        "--from",
        "waterlog-mcp==1.4.0",
        "waterlog-mcp"
      ],
      "env": {}
    }
  }
}
```

- `env` 留空：服务密钥已硬编码在包内，宿主没有环境变量也能用；
- 换后端地址：在 `waterlog-mcp` 后面加 `"--base-url=https://your-domain"`（必须是**工具名之后**）；
- 访问 PyPI 慢：把 `--index-url` 换成 `https://pypi.tuna.tsinghua.edu.cn/simple`；
- 让服务器自己生成：`uvx --from waterlog-mcp==1.4.0 waterlog-mcp --print-config`。

### B 模式：HTTP，服务地址 + 路径

```bash
uv tool install waterlog-mcp==1.4.0 --index-url https://test.pypi.org/simple
waterlog-mcp --transport http --host 127.0.0.1 --port 8090 --path /mcp
```

平台里填：服务地址 `https://water.cubestudio.top`，路径 `/mcp`（旧版 SSE 用 `/mcp/sse`），
健康检查 `/health`。Nginx 段、systemd、Dockerfile 都在 [HOSTED-HTTP.md](./HOSTED-HTTP.md)。

### 上传/接入前先自检（强烈建议）

```bash
# A 模式：按 JSON 里的 command+args 真拉起一次
python scripts/smoke_test.py mcp-config.testpypi.json

# B 模式：直接打服务地址，也可以拿平台的网关地址来定位问题
python scripts/smoke_test.py --url https://water.cubestudio.top/mcp

# 想看工具清单
python scripts/smoke_test.py --url https://water.cubestudio.top/mcp --list-tools
```

> uvx 在全新机器上会自动下载一个受管 Python（CPython 3.9+），首次启动多花 10~30 秒属正常。

> ⚠️ **包内硬编码的服务密钥 = 公开可读**：任何能 `uvx --from waterlog-mcp ...` 的人都能解包看到
> `HARDCODED_API_KEY`。它只读（监测/预警/灾情/地名/规划路线），不会泄露用户数据、不能写、
> 不能读 `/me`，所以按「公开只读凭据」对待即可；但请：
> 1. 不要给它加任何写权限（`NewServiceKeyVerifier` 只挂 `DefaultDelegationScopes()`）；
> 2. 发现滥用就换 `AGENT_SERVICE_KEY` + 发新版（旧版的常量随之失效）；
> 3. 更严格时可用自托管 wheel 放到**需要鉴权**的路径，而不是公开 PyPI。

## 发布到 PyPI

```bash
cd deploy/mcp
python -m unittest discover -s tests -v   # 43 个用例（含版本/配置一致性校验）
uv build                                  # 生成 dist/*.whl 与 *.tar.gz
$env:UV_PUBLISH_TOKEN = "pypi-..."        # 见下方「token 怎么建」
.\scripts\publish.ps1                     # 或直接 uv publish
```

### token 怎么建（这里最容易踩坑）

PyPI **已关闭「用户名 + 密码」上传**，只接受 API Token：

1. https://pypi.org/manage/account/token/ → *Add API token*
   - 首次发布时 Scope 只能选 **All projects**（包还不存在时无法选单包），发布后再换成 `Project: waterlog-mcp`；
2. 复制 `pypi-...` 串，用环境变量传，**不要**在 `uv publish` 的交互提示里输入账号名：

```powershell
# ✅ 正确
$env:UV_PUBLISH_TOKEN = "pypi-AgEIcHlwaS5vcmc..."   # 用户名由 uv 自动填 __token__
uv publish

# ❌ 错误（会得到 403 Username/Password authentication is no longer supported）
uv publish      # 然后 Enter username: <你的账号> / Enter password: <账号密码>
```

`scripts/publish.ps1` 已经处理好这些：只传 `--token`（不再同时传 `--username`，否则 uv 报
`cannot be used with --token`），并支持 `-Repository testpypi` / `-DryRun`。

### 先发 TestPyPI 演练

```powershell
$env:UV_PUBLISH_TOKEN = "<TestPyPI token>"
.\scripts\publish.ps1 -Repository testpypi
# 验证（TestPyPI 上是私有/预发布，需要显式指定 index）
uvx --index-url https://test.pypi.org/simple --from waterlog-mcp==1.4.0 waterlog-mcp --version
```

> **两个源不通用**：TestPyPI 和 PyPI 是两套账号、两套 token。
> 小技巧：`pypi-AgENdGVzdC5weXBpLm9yZw...` 这类 base64 里带 `test.pypi.org` 的，就是 TestPyPI 的 token，
> 拿去发正式 PyPI 一定 403。

### 当前状态

- **TestPyPI：已发布 1.4.0**（`https://test.pypi.org/project/waterlog-mcp/1.4.0/`），`uvx --index-url https://test.pypi.org/simple` 安装验证通过。
- **正式 PyPI：尚未发布**（`https://pypi.org/pypi/waterlog-mcp/json` 仍 404）。需要一把 pypi.org 的 token，命令同上。

## 凭据从哪来

| 凭据 | 哪里拿 | 有效期 | 能做什么 |
| --- | --- | --- | --- |
| **服务密钥 `apiKey`**（推荐） | 后端 `.env` 的 `AGENT_SERVICE_KEY` | **长期**（改配置才失效） | 只读：监测、预警、灾情、地名、规划路线 |
| 用户令牌 `token` | 小程序/网页登录后的 `accessToken` | 30 分钟 | 按该用户权限 |
| 委派令牌 `dlg.*` | `POST /api/v1/ai/consultations` 响应的 `delegation.token` | 10 分钟 | 只读，绑定某次会话 |

**为什么不要硬编码用户令牌**：`accessToken` 30 分钟就过期，`dlg.*` 只有 10 分钟，
写死在云端 MCP 里半天就全 401。托管 Agent 请用服务密钥。

### 生成服务密钥

```bash
# 1) 生成 32 字节随机串
openssl rand -hex 32
# Windows PowerShell：
# $b=New-Object byte[] 32;[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b);($b|%{$_.ToString('x2')})-join''

# 2) 写进后端 .env 并重启
AGENT_SERVICE_KEY=<上一步的串>
AGENT_SERVICE_KEY_LABEL=agentarts-triage   # 可选，只用于日志区分

# 3) 把同一个串粘到 src/waterlog_mcp/server.py 顶部
#    HARDCODED_API_KEY = '<上一步的串>'
```

服务密钥用 `X-Api-Key` 头（也接受 `Authorization: Bearer <服务密钥>`），后端会把它映射成
`service:agent` 身份 + `service` 角色：**只读白名单**内的接口 200，写接口一律 403
（`SERVICE_KEY_FORBIDDEN`），`/me`、管理端、AI 咨询都不可用。

## 启动

```bash
# 云端 / 宿主调用（包内已硬编码服务密钥）：什么都不用配
uvx --from waterlog-mcp==1.4.0 waterlog-mcp

# 本地源码直接跑
python -m waterlog_mcp --base-url=http://127.0.0.1:6732

# 指定凭据
python -m waterlog_mcp --base-url=http://127.0.0.1:6732 --api-key=<服务密钥>
python -m waterlog_mcp --base-url=http://127.0.0.1:6732 --token=<用户令牌>
```

凭据解析优先级（靠前的优先）：

1. 启动参数 `--api-key=` / `--token=`
2. 环境变量 `WATER_API_KEY` / `WATER_API_TOKEN`
3. 同目录（或包目录）`water-mcp.config.json` 的 `apiKey` / `token`
4. 包内 `HARDCODED_API_KEY`
5. 运行时工具 `configure_water_api({apiKey})`（进程内有效，重启失效）

> 用户令牌 `token` 始终优先于服务密钥 `apiKey`。只配了服务密钥时，写操作会返回一条
> 「需要运维/管理员令牌」的明确提示，而不是让模型去撞 403。

| 变量 / 参数 | 必填 | 说明 |
| --- | --- | --- |
| `--base-url` / `WATER_API_BASE_URL` / `baseUrl` | 否 | 后端地址，默认 `https://water.cubestudio.top` |
| `--api-key` / `WATER_API_KEY` / `apiKey` | 二选一 | 长期只读服务密钥 |
| `--token` / `WATER_API_TOKEN` / `token` | 二选一 | 用户令牌或 `dlg.*` 委派令牌 |
| `--admin-token` / `WATER_ADMIN_TOKEN` / `adminToken` | 否 | 触发/写回研判时用（需 operator/admin） |
| `--timeout-ms` / `WATER_TIMEOUT_MS` / `timeoutMs` | 否 | 单次请求超时，默认 20000 |

## 工具清单

| 工具 | 对应接口 | 说明 |
| --- | --- | --- |
| `connection_status` | `GET /health/live` + `GET /monitors` | 先看后端是否活、凭据是否有效（只回显末 4 位） |
| `configure_water_api` | — | 运行时注册 `apiKey` / `token` / `baseUrl` |
| `list_monitors` | `GET /monitors` | 找监测点、拿 `monitorId` |
| `get_latest_reading` | `GET /monitors/{id}/latest` | 当前水位/风险/新鲜度 |
| `get_monitor_trend` | `GET /monitors/{id}/trend` | 涨落趋势 |
| `get_monitor_prediction` | `GET /monitors/{id}/prediction` | 未来水位预测 |
| `list_alerts` | `GET /alerts` | 预警（可按未读/状态/点位过滤） |
| `list_confirmed_floods` | `GET /disaster-reports/map` | 已确认积水点 |
| `geocode_search` | `GET /maps/geocode` | 地名 → 坐标 |
| `reverse_geocode` | `GET /maps/reverse-geocode` | 坐标 → 地址 |
| `plan_avoidance_route` | `POST /routes/plan` | 按进气口规划避障路线 |
| `assess_vehicle_access` | 本地计算 | 安全余量 → 通行结论 |
| `list_disaster_reports` | `GET /disaster-reports` | 上报列表（服务密钥/运维可看全部） |
| `get_disaster_report` | `GET /disaster-reports/{id}` | 单条详情（含图片签名 URL） |
| `analyze_disaster_report` | `POST /disaster-reports/{id}/analyze` | 触发自动研判（**需用户令牌 + 运维/管理员**） |
| `update_disaster_report` | `PUT /disaster-reports/{id}` | 人工复核 / 修正（写操作） |

## 在 VS Code 里注册

`.vscode/mcp.json`（或用户级 `mcp.json`），推荐与云端同一份 uvx 配置：

```json
{
  "servers": {
    "waterlog": {
      "type": "stdio",
      "command": "uvx",
      "args": ["--from", "waterlog-mcp==1.4.0", "waterlog-mcp"]
    }
  }
}
```

本地改代码时用源码路径：

```json
"command": "python",
"args": ["-m", "waterlog_mcp", "--base-url=https://water.cubestudio.top"],
"env": { "PYTHONPATH": "deploy/mcp/src" }
```

## 自测

```bash
python -m unittest discover -s tests -v
```

覆盖：协议握手、工具清单与 JSON Schema、参数映射（含 `origin/destination` 组装）、
错误码透传（`isError`）、本地判定口径、查询参数空值过滤、凭据优先级与服务密钥通道
（`X-Api-Key`）、`configure_water_api` 两种凭据、只读写回拒绝、stdin 关闭时把应答写完，
以及 **pyproject / `configs/*.json` 与 `__version__` 的一致性**（防止发版后配置忘改）。

## 与 OAS 的关系

| 方式 | 适合 | 差异 |
| --- | --- | --- |
| OpenAPI（[agentarts-openapi.json](../agentarts-openapi.json)） | AgentArts 网关 Target / 插件 | 覆盖全部 29 个操作，参数是原始 REST 形态 |
| MCP（本目录） | 本地或远程 MCP 宿主 | 只挑 Agent 真正需要的 16 个，参数扁平、错误统一、含本地计算工具 |

两者都指向同一套后端接口；委派令牌（`delegation`）与服务密钥（`X-Api-Key`）在两边都适用。
