Metadata-Version: 2.4
Name: dahua-device-mcp
Version: 0.3.0
Summary: MCP stdio server for Dahua device JSON-RPC APIs
Author: bigoat
Project-URL: Repository, https://cnb.cool/leliven/leliven-mcp-skill
Keywords: mcp,dahua,nvr,onvif,rtsp,gb28181
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: mcp<2.0,>=1.30
Requires-Dist: pydantic<3,>=2
Requires-Dist: httpx<1.0,>=0.27
Requires-Dist: psutil<8.0,>=6.0
Requires-Dist: cryptography<51,>=44
Requires-Dist: python-dotenv<2,>=1

# dahua-device-mcp

基于大华设备 JSON-RPC 的 MCP stdio 服务，当前主要覆盖 NVR 的通道、摄像头与 GB28181 配置，以及局域网设备发现。当前版本为 **0.3.0**；以本地源码启动为准。

项目名和启动命令为 `dahua-device-mcp`，Python 包名为 `dahua_device_mcp`，MCP 服务名为 `dahua-device`。源码直接位于 `src/`，由 `pyproject.toml` 映射为 Python 包，无需再嵌套同名目录。上游协议及实现范围见 [API 文档](dahua-device-api.md)。

0.3.0 将所有工具升级为结构化输出，统一公共字段和各工具的输出 Schema。下文的 `verified`、`write_state` 等业务字段均位于 `data` 中；`target_base_url`、`warnings`、`errors` 位于公共外层。旧的字符串形式 `structuredContent.result` 已移除，调用方需按下文迁移并刷新工具 Schema。

## 摄像头添加规则

`dahua_add_camera` 必填 `camera_sn`（真实摄像头 SN）和 `channel_name`（车位范围）。设备名称
`DeviceInfo.Name` 固定使用 SN，通道名称 `VideoInputs[0].Name` 使用车位范围，两者分别保存。
`name` 如提供必须与 `camera_sn` 一致，不再作为通道名称的兼容参数。

先把待添加摄像头按车位编号数值从小到大排序，再依次使用 `dahua_list_cameras` 返回的最小空闲
`UniqueChannel`（RPC 从 0 开始）。车位名称支持 `594001-594005`、单个数字编号或
`594006, 594007, 594008`。工具拒绝跳过空槽、车位倒序、重叠以及无法解析的已有通道名称；
不会替用户重排已有通道或修改国标映射。

```json
{
  "rpc_channel": 0,
  "address": "192.0.2.31",
  "protocol": "RTSP",
  "camera_sn": "REPLACE_WITH_REAL_CAMERA_SN",
  "channel_name": "594001-594002",
  "main_stream_url": "rtsp://192.0.2.31:8557/h264",
  "extra_stream_url": "rtsp://192.0.2.31:8557/sub_stream_1"
}
```

每台添加后回读设备名称、通道名称、地址、端口及 RTSP 主辅地址，检查其他通道未变。
仅当已确认添加成功、且唯一差异为 RTSP 视频源名称时，单独补写名称并再次回读；
写入结果未知时不重放。必须得到 `verified=true` 和 `order_verified=true` 后才继续下一台。
`connected=false` 允许表示离线配置；不等于名称、地址保存失败。

`camera_sn` 同时传入原生 `SerialNo`，但某些固件仍回显旧 ONVIF SN 或空值，
因此以设备名称是否等于真实 SN 作为本规则的核验依据，不宣称原生 `SerialNo` 保存成功。
ONVIF 设备可能再次同步型号到设备名称；名称不符时会返回核验失败。

这是本地开发版的添加参数变更；旧调用需补齐 `camera_sn`、`channel_name`。
更新后重新连接/重启 MCP 进程以加载新 Schema，重启后需重新设置当前 NVR。


## 连接一次，后续自然对话

用户先在聊天中提供一台 NVR 的地址、账号和密码，Agent 调用 `dahua_set_current_device`。验证登录成功后，后续“查看通道”“查看设备信息”等请求不需要重复提供连接参数，也不需要 `connection_id`。

```text
用户：连接 192.0.2.10，账号 admin，密码……
Agent：dahua_set_current_device(...) → 验证登录成功，设置当前设备
用户：查看通道
Agent：dahua_list_cameras({})
用户：查看设备信息
Agent：dahua_get_device_info({})
用户：切换到另一台设备，账号密码相同
Agent：从已授权上下文取得凭据，再次调用 dahua_set_current_device(...)
```

`dahua_set_current_device` 每次要求真实的 `base_url`、`username`、`password`。缺少凭据应先向用户确认，不能猜测。地址无协议时默认 HTTP。

```json
{
  "base_url": "http://192.0.2.10",
  "username": "admin",
  "password": "<用户提供的真实NVR密码>"
}
```

设置成功后，`dahua_list_cameras` 或 `dahua_get_device_info` 的参数可以是 `{}`。业务结果额外返回 `target_base_url`，说明这一次实际操作的目标，不依赖之后是否切换了设备。

- 只在本 MCP 进程内存保存不可变的连接配置，不写 `.env`、进程环境或其他文件。
- 不保留长期客户端或 NVR Session。设置设备时验证登录并注销，之后每次业务操作独立登录、执行、注销。
- 切换失败或被取消时保留原设备，错误中明确说明仍使用哪一台；首次设置失败则仍未配置。
- 操作开始时固定目标。中途切换不会将已经开始或排队的写入改投新设备。
- 进程重启后需重新设置。正常退出释放配置引用，但不承诺密码字符串的物理内存擦除。
- 当前设备是**进程级共享状态，不按聊天或用户隔离**；适合单人顺序操作，不适合多人/多聊天并行共用一个进程。
- 聊天和工具输入记录仍可能包含密码。降低重复传参不等于凭据输入保密。

保留显式单次调用：任一设备业务工具也可同时传入三项完整连接参数，只影响这一次，不会设置或切换当前设备；拒绝仅提供部分参数而暗中拼接旧凭据。

### 查询与清除当前设备

`dahua_get_current_device`、`dahua_clear_current_device` 均无参数，调用时传 `{}`。它们只访问当前 MCP 进程内状态，不创建客户端、不登录设备、不执行设备 RPC，不读写环境变量或配置文件；外层 `target_base_url=null`。

`dahua_get_current_device` 的 `data` 包含 `configured` 和 `current_device`。已设置时仅公开 `base_url`、`verify_tls`、`timeout_seconds`，不返回账号、密码、Session 或 Token；未设置时仍成功返回 `configured=false`、`current_device=null`。`configured=true` 只说明保存了选择，不保证设备在线或凭据仍有效。

```json
{
  "configured": true,
  "current_device": {
    "base_url": "http://192.0.2.10",
    "verify_tls": false,
    "timeout_seconds": 15.0
  }
}
```

`dahua_clear_current_device` 的 `data` 如下。`cleared=true` 表示状态已确保清空；`had_current_device` 表示调用前是否存在选择，重复清除时为 false。清除不修改 NVR 配置；之后省略连接信息的业务调用需重新设置当前设备，完整单次连接参数仍可使用。

```json
{"cleared": true, "had_current_device": true, "configured": false, "current_device": null}
```

清除时已捕获目标的业务调用（包括已排队的写入）继续使用原快照。清除前已经排队或正在登录的设备选择操作作废，验证和客户端清理结束后返回选择失败，不会把旧选择重新写回；清除后新发起的选择仍可正常验证并保存。清除只移除状态引用，不承诺凭据字符串的物理内存擦除，也不会清理已有聊天和工具输入记录。

### 三种密码不能混用

- `password`：NVR 登录密码，设置设备时必填；后续默认从内存中的当前设备取用。
- `camera_password`：摄像头登录密码，摄像头添加/修改时按协议要求提供。
- `sip_password`：GB28181 平台的 SIP 注册密码。原 0.1.0 `gb28181_update_config.password` 已改为此名称。

例如设置当前设备后修改其平台 1（会写设备配置）：

```json
{
  "platform_address": "sip.example.net",
  "platform_port": 15061,
  "sip_password": "<平台真实SIP密码>"
}
```

省略 `sip_password` 会保留平台密码；不能用 NVR 密码代替它。未知掩码仍拒绝写回，原生占位符仅对已核对固件放行。

### 工具名称迁移

公开工具统一采用 `dahua_<动词>_<对象>`，并提供中文标题、参数说明和显式读写标记。此次为公开工具名称调整，业务规则保持一致；当前新增 include_raw 诊断参数并升级输出为2.0，详见下文；旧工具名不再注册，不增加重复别名。自动化脚本、提示词和客户端工具白名单需按下表更新。Python 内部函数名与模块路径保持原有名称。

| 原工具名 | 当前工具名 |
| --- | --- |
| `discover_dahua_devices` | `dahua_discover_devices` |
| `set_current_device` | `dahua_set_current_device` |
| `get_current_device` | `dahua_get_current_device` |
| `clear_current_device` | `dahua_clear_current_device` |
| `camera_list` | `dahua_list_cameras` |
| `camera_add` | `dahua_add_camera` |
| `camera_update` | `dahua_update_camera` |
| `camera_delete` | `dahua_unbind_camera` |
| `gb28181_get_config` | `dahua_get_gb28181_config` |
| `gb28181_update_config` | `dahua_update_gb28181_config` |
| `device_info` | `dahua_get_device_info` |

`dahua_unbind_camera` 明确表示解除 NVR 通道绑定，不删除录像或修改摄像头本身。服务提示词按“发现 → 验证选择 → 查询/核对 → 按用户目标写入 → 分别解释配置与在线状态”引导；公共规则放在服务说明，特殊限制放在各工具说明和参数 Schema。所有设备查询标为只读、幂等，网络工具 openWorldHint=true；查询/清除当前选择仅操作本进程，openWorldHint=false。工具标记用于客户端提示，不能替代服务端校验。

修改后需重启 MCP 服务并重新连接 Inspector/Codex，刷新工具列表和 Schema；Inspector 的设备选择与其他进程独立。

## 启动与 MCP 客户端配置

从 PyPI 安装后启动（需要 Python 3.11 或更高版本）：

```powershell
pip install dahua-device-mcp
dahua-device-mcp
```

也可通过 `uvx dahua-device-mcp` 直接启动。MCP 客户端可配置 `command` 为 `uvx`、`args` 为 `["dahua-device-mcp"]`，无需本地源码目录。设备凭据通过工具调用提供，不写入启动配置。

需要 Python 3.11 或更高版本及 `uv`；项目依赖 MCP Python SDK 1.30 或更高的 1.x 版本，以支持本项目的输出契约。在本机项目目录同步依赖并启动：

```powershell
cd E:\leliven-mcp-skill\dahua-device-mcp
$env:UV_CACHE_DIR = "$PWD\.uv-cache"
uv sync
uv run dahua-device-mcp
```

也可通过 `uv run python -m dahua_device_mcp` 启动。`uv sync` 安装配置中的包映射；请使用包入口启动，避免直接运行 `src/server.py`。MCP 客户端配置如下，迁移到其他机器时替换项目绝对路径。

启动时不需要设备地址、账号和密码。可选的全局参数仅有 `--env-file`、`--verify-tls`、`--timeout-seconds`。旧的 `--base-url`、`--username`、`--password` 启动参数已移除；必须迁移到工具调用。Windows 若提示 exe 被占用，请先正常停止旧 MCP 后再同步、重启。

```json
{
  "mcpServers": {
    "dahua-device": {
      "command": "uv",
      "args": [
        "run", "--directory", "E:/leliven-mcp-skill/dahua-device-mcp",
        "dahua-device-mcp"
      ]
    }
  }
}
```


### 全局可选配置

项目不附带 `.env` 或示例文件，无配置也能启动。如需文件配置，可自行创建 `.env`，仅支持以下两项；`DAHUA_BASE_URL`、`DAHUA_USERNAME`、`DAHUA_PASSWORD` 不参与设备连接。

```dotenv
DAHUA_VERIFY_TLS=false
DAHUA_TIMEOUT_SECONDS=15
```

设置当前设备时保存解析后的 TLS/超时默认值；单次覆盖不会修改已保存配置。解析优先级：本次工具的 `verify_tls/timeout_seconds` > 启动参数 > 进程环境变量 > 指定的配置文件或工作目录 `.env` > 内置默认值。TLS 布尔值、超时在环境变量和参数路径均严格校验。无配置也能启动。

`false` 仅关闭 HTTPS 证书验证，不切换 HTTP/HTTPS、不关闭登录鉴权。HTTP 或未验证证书的 HTTPS 仅适合受信任网络；需要安全验证时显式传 `verify_tls=true`。

## 工具

| 工具 | 功能 |
| --- | --- |
| `dahua_discover_devices` | UDP 发现，不需要设备凭据 |
| `dahua_set_current_device` | 验证登录后设置/切换当前设备，失败不替换原配置 |
| `dahua_get_current_device` | 查询进程内公开选择，不重新验证设备登录 |
| `dahua_clear_current_device` | 清除进程内选择，可重复调用，不修改设备配置 |
| `dahua_list_cameras` | 摄像头通道列表 |
| `dahua_add_camera` | 添加至已有空闲 Remote 槽位 |
| `dahua_update_camera` | 修改已启用摄像头连接 |
| `dahua_unbind_camera` | 解除绑定，不删除录像 |
| `dahua_get_gb28181_config` | 读取平台表、客户端表 |
| `dahua_update_gb28181_config` | 修改平台表，默认平台 1 |
| `dahua_get_device_info` | 型号、序列号、版本、远程通道数量和硬盘信息 |

当前共 11 个工具。网卡枚举由 `dahua_discover_devices` 内部完成，不提供独立工具；省略 `local_ip` 自动扫描有效网卡，指定本机 IPv4 地址则仅扫描对应网卡。发现及查询、清除选择均不依赖当前设备；设置设备后，设备业务工具默认使用它。输出采用统一结构；设备业务结果的 `target_base_url` 表示操作开始时捕获的目标。SIP 密码使用上述 `sip_password` 名称。

### 结构化输出与迁移

所有 11 个工具的成功、部分成功和错误响应统一使用输出契约 **2.0**。每个工具公布自己的 `outputSchema`；文本 JSON 与 `structuredContent` 等价。

| 公共字段 | 含义 |
| --- | --- |
| `schema_version` | 固定字符串 `2.0`，与项目发行版本分别管理 |
| `status` | `success`、`partial`、`unverified`、`unknown`、`error` |
| `target_base_url` | 本次捕获的目标；发现、查询/清除选择及目标未确认的错误为 null |
| `data` | 当前工具的规范化业务对象；执行错误为 null |
| `raw` | 默认 null；支持诊断参数的工具传 `include_raw=true` 时返回脱敏后的原始响应/操作结果；错误时仍为 null |
| `warnings` | 警告字符串数组，无警告为 `[]` |
| `errors` | 错误数组，无错误为 `[]`；每项含 `code`、`message`、`scope`、`device_code`、`fields`、`retryable` |

`data` 及其嵌套对象使用 snake_case 和固定模型，不透传未知厂商字段。RPC 的 `result`、`params`、`id`、`session` 不再出现在默认业务结果中；扩展字段移入可选 `raw`，不递归猜测厂商字段含义。账号、密码、Session、Token 和 URL 凭据始终脱敏，`raw` 也不例外。原始数据用于诊断，不能将脱敏后的值用于配置写回。

### 各工具的业务结构

| 工具 | `data` 字段及含义 |
| --- | --- |
| `dahua_discover_devices` | `device_count`、`devices`、`interfaces`；设备版本统一为 `firmware_version`，未解析详情为 null |
| `dahua_set_current_device` | `configured`、`login_verified`、`current_device`；去掉重复文本 message |
| `dahua_get_current_device` | `configured`、`current_device` |
| `dahua_clear_current_device` | `configured`、`current_device`、`cleared`、`had_current_device` |
| `dahua_list_cameras` | `camera_count`、`cameras`；每项统一输出 `rpc_channel`、`remote_input`、`channel_type`、`enabled`、`address`、`protocol`、端口、名称、标题、主辅流地址和 `serial_number` |
| `dahua_add_camera` | 保留已规范的写入、核验和连接字段：`accepted`、`write_state`、`verified`、`target_verified`、`other_channels_unchanged`、`order_verified`、`connected` 等；`camera` 为规范化摘要 |
| `dahua_update_camera` | 同一摄像头写入模型，未参与的添加字段为 null |
| `dahua_unbind_camera` | 同一摄像头写入模型；解绑核验包含通道为空及其他通道不变 |
| `dahua_get_gb28181_config` | `server`、`clients`、`transport`、`default_slot_index`；不再以厂商表名作为业务对象键 |
| `dahua_update_gb28181_config` | 保留写入、配置、密码和注册核验字段；`table=server`，`config` 使用与查询的 `server` 相同的规范化结构 |
| `dahua_get_device_info` | `model`、`serial_number`、`firmware_version`、`web_version`、`build_date`、`hardware_version`、`security_baseline_version`、`channels`、`storage` |

`camera_count` 是返回的全部视频源条目数，包含未启用槽位和 Compose 合成条目；不表示已连接摄像头数量。`rpc_channel` 来源于 `UniqueChannel`，`remote_input` 来源于远端输入 `Channel`，不能互换。列表中的 `serial_number` 是设备报告的缓存值，不证明摄像头真实 SN；添加仍需用户提供真实 `camera_sn`。`enabled` 不是在线状态。

`server` 包含 `enabled`、`version`、`platforms`；平台条目包含 `slot_index`、`platform_number`、`enabled`、`online`、`protocol`、`platform_id`、`platform_address`、`platform_port`、`domain`、`device_id`、`local_sip_port`、`channel_count`、心跳/注册时间及 `video_channels`、`audio_channels`。映射使用 `channel_index`（返回数组中从0开始的位置）和 `channel_id`，视频映射另有 `alarm_level`；不要把该索引推断为通道列表中的 UniqueChannel。客户端条目含 `index`、`server_id`、`enabled`。未请求/读取失败的表为 null，已读取的空数组才是 `[]`。所有未列出的厂商扩展可通过 raw 查看。

状态规则不变：`partial` 保留有效数据并报告缺失项；`unverified` 表示配置整体核验未通过；写入 `write_state=unknown` 优先输出 `status=unknown`，即使回读匹配也不改变。`success` 不证明设备在线或密码已验证。`error` 同时设置 MCP `isError=true`；当前错误的 `retryable=false`，禁止按相同参数自动重放未知写入。

### 原始数据与迁移

七个设备业务工具（设备信息、摄像头列表及三种绑定操作、GB28181 读写）增加可选 `include_raw=false`，不影响输入凭据或写入策略。发现和三种设备选择工具没有原始厂商响应，因此不提供该参数，raw 始终为 null。

| 1.0 输出路径 | 2.0 输出路径 |
| --- | --- |
| `data.params.camera` | `data.cameras` |
| `data.device_type.params.type` | `data.model` |
| `data.serial_no.params.sn` | `data.serial_number` |
| `data.software_version.params.version.Version` | `data.firmware_version` |
| `data.hardware_version.params.version` | `data.hardware_version` |
| `data.channel_counts.max_remote_input_channels` | `data.channels.max_remote_inputs` |
| `data.channel_counts.enabled_remote_channels` | `data.channels.enabled` |
| `data.disk_count` | `data.storage.disk_count` |
| `data.disk_total_gb` / `disk_used_gb` / `disk_free_gb` | `data.storage.total_gb` / `used_gb` / `free_gb` |
| `data.storage_info.params.info` | `data.storage.disks` |
| `data.configs.VSP_GAYS_SERVER.sipServer` | `data.server.platforms` |
| `data.configs.VSP_GB28181_CLIENT` | `data.clients` |
| `data.config.sipServer`（GB 写入） | `data.config.platforms` |

这次输出升级不保留旧字段别名。请依据 `schema_version` 调整消费方，并重启 MCP、重连 Inspector 刷新 Schema。Python 直接调用工具返回 Pydantic `ToolResult`，用 `.model_dump(mode="json")` 导出；直接调用的异常与 MCP 边界结构化错误的区别不变。

设备信息结果示例（disks 为空仅用于演示字段位置，实际条目以设备返回为准）：

```json
{
  "schema_version": "2.0",
  "status": "success",
  "target_base_url": "http://192.0.2.10",
  "data": {
    "model": "DH-NVR1108HC-HDS4",
    "serial_number": "example-sn",
    "firmware_version": "example-version",
    "channels": {
      "max_remote_inputs": 10,
      "remote_slots": 10,
      "enabled": 7
    },
    "storage": {
      "disk_count": 0,
      "total_gb": 0,
      "used_gb": 0,
      "free_gb": 0,
      "disks": []
    }
  },
  "raw": null,
  "warnings": [],
  "errors": []
}
```

### 参数校验与写入结果

所有工具的输入 Schema 均声明 `additionalProperties=false`，未知字段在执行前返回 MCP 工具错误，不会被静默忽略。例如 `platfrom_port` 会被拒绝，正确参数为 `platform_port`。错误不回显未知参数的值。

Schema 同时公布 RPC 通道号和平台索引非负、端口 1～65535、发现超时大于 0 且不超过 60 秒、注册等待大于 0 且不超过 45 秒等限制；业务关联规则仍由工具校验。更新后需重启 MCP 并刷新客户端工具 Schema。

摄像头添加、修改和删除均分别报告：

| 字段 | 含义 |
| --- | --- |
| `target_verified` | 目标通道的预期配置已回读匹配；删除还要求连接状态为 `Empty` |
| `other_channels_unchanged` | 其他通道的身份、名称和连接配置未变化 |
| `verified` | 上述两项均为 true，才表示整体核验通过 |

目标通道已匹配但其他通道变化时，`target_verified=true`、`verified=false`，应先排查并发修改，停止后续批量写入。`verified` 仍不代表摄像头已在线或密码已核验。

摄像头与 GB28181 写入结果还包含 `write_state`：`acknowledged` 表示收到设备成功响应；`unknown` 表示发生传输异常、HTTP 错误、无效 JSON、非对象响应或加密响应无法核验，写请求是否执行无法由响应确认；国标配置无变化且未发送写请求时为 `not_sent`。

`write_state=unknown` 时仅尝试回读，不自动重放写请求。回读成功可得到 `verified=true`，但不能据此推定先前写请求已被设备确认；回读失败则保持 `verified=false`，应先调用对应查询工具核对状态。设备返回明确 RPC 拒绝时保留工具错误，不掩盖为成功或自动重试。

- RPC 通道从 0 开始，不能用列表下标代替 `UniqueChannel`；GB28181 槽位从 0 开始，默认 0 表示平台 1。
- 发现报文使用 UDP 5050/5051，5051 需独占，先退出 ConfigTool；支持指定网卡或自动扫描有效网卡。
- 摄像头添加支持 Onvif/Private/RTSP/General。RTSP 的主流地址 `main_stream_url` 必填，辅流 `extra_stream_url` 可选，标题用 `channel_name`；URL 主机需匹配摄像头 `address`，不能内嵌账号密码。RTSP 添加等待 5 秒回读，摄像头更新目前仅支持 Onvif/Private。
- 写入前读取最新完整对象/表，只改目标字段，保留其他通道、平台及映射；超时先回读，不自动重发。
- GB28181 默认普通 JSON RPC（`transport=plain`）；`secure/auto` 是可选固件兼容模式。客户端表仅支持读取。
- `accepted`、`config_verified`、`password_verification`、`registration_status` 分别说明接受、配置、密码和注册结果，不能互相替代。
- 所有工具公开输出统一脱敏密码、Session、Token、URL 凭据。内存中的设备基线不被脱敏函数修改，禁止把脱敏占位符作为原值写回。

`data.channels` 包含 `max_remote_inputs`、`remote_slots`、`enabled`，分别为容量、远程槽位数（包含未启用槽位）、已启用数，排除合成通道；均不代表在线数，未知为 null。

`data.storage` 包含 `disk_count`、`total_gb`、`used_gb`、`free_gb`、`disks`。每块磁盘含 `name`、`state`、自己的 GB 容量以及 `partitions`。分区字段为 `path`、`storage_type`、`total_bytes`、`used_bytes`、`has_error`，不把分区数量当作硬盘数量。state 和 has_error 是设备报告，不能替代完整的硬盘健康检测。

汇总按全部分区字节数相加、先计算剩余字节，再除以 1,000,000,000 换算为十进制 GB，使用四舍五入保留两位小数；不使用 GiB，不累加已舍入的分区值。空磁盘列表时 disk_count=0、三项容量为0、disks=[]；读取失败时相关值及 disks 为 null。分区信息缺失、容量缺失、已用大于总量或路径重复时，汇总容量为 null，保留已读取信息，返回 partial 和 scope=storage.capacity。单盘容量可独立可用。分别舍入后，总量减已用可能与独立舍入的剩余值相差 0.01 GB。

## 项目结构与测试

```text
src/              安装时映射为 dahua_device_mcp 包
  __init__.py     包初始化
  __main__.py     python -m dahua_device_mcp 入口
  server.py       工具注册、请求级连接参数
  config.py       连接参数校验及全局 TLS/超时选项
  current_device.py  进程内当前设备、验证后切换、目标快照
  client.py       登录、普通/可选加密 RPC、注销
  discovery.py    IPv4 网卡及 UDP 发现
  device_info.py  设备信息、远程通道统计与硬盘查询
  camera_ops.py   摄像头操作和核验
  gb28181_ops.py  平台读写和注册核验
  profiles.py     固件兼容性证据
  security.py     输出及 URL 脱敏
  results.py      公共返回契约、业务模型和安全错误转换
  output_mapping.py  厂商响应到2.0业务字段的白名单映射
tests/            单元、stdio、Schema 与并发隔离测试
dahua-device-api.md  上游 API 接口文档
pyproject.toml    项目、依赖、包映射及启动命令
README.md         使用及开发说明
.python-version   uv 使用的 Python 版本
.gitignore        环境、缓存和编译产物排除规则
```

Python 模块使用小写下划线命名，测试文件使用 `test_<模块或场景>.py`；例如 `test_gb28181_ops.py` 对应平台操作，`test_entrypoints.py` 检查启动入口，`test_rtsp_stdio.py` 检查 RTSP 工具的 stdio 调用。项目目录、发行名称和启动命令使用 `dahua-device-mcp`，Python 包使用 `dahua_device_mcp`；文档保留 `README.md` 和 `dahua-device-api.md`。

```powershell
uv run python -m unittest discover -s tests -v
```

测试使用模拟设备和本地 stdio 子进程，覆盖连接隔离、工具参数、写入回读、敏感信息脱敏及两种启动入口。回归测试还覆盖其他通道变化导致整体核验失败、写入 HTTP/解析异常后的单次回读与禁止重放、未知参数拒绝及数值 Schema 约束。结构化输出测试逐个核对 11 个工具的成功和错误响应与其公布的 `outputSchema`，检查文本/结构化结果一致、部分成功、未核验、未知写入及敏感字段脱敏。状态测试覆盖未设置/已设置查询、重复清除、清除后的单次调用和重新选择、在途/排队业务快照，以及清除阻止在途/排队选择回写。测试通过不代表对实际 NVR 型号、固件或媒体播放完成验证。
