bmahs-mcp-gateway
by XJPeng12
README.md
# bmahs-mcp-gateway
> **BMAHS(比马斯)** 是一个开放的局域网硬件协议:每台设备上电即用自然语言「自我介绍」——我是谁、能做什么、安全边界在哪——让大模型智能体像接入 USB 设备一样,即插即用地发现、识别、按权限独占并安全地操作它们。
BMAHS(比马斯)设备协议 ↔ MCP 网关:把局域网内按 `bmahs/1.0` 协议发布的硬件设备(解析侧兼容旧版 1–1.2 字段名)动态映射为 [Model Context Protocol](https://modelcontextprotocol.io) 工具,让大模型客户端可以直接发现、占用与操作这些设备。
## 特性
- **零配置发现**:UDP 组播(`239.255.42.42:5354` / `[ff02::4242]:5354`)+ Bonjour/mDNS 双通道,设备上线即被识别;也支持 `BMAHS_STATIC_DEVICES` 静态设备表(适配不支持组播的环境)。
- **动态工具映射**:设备的动作清单(`ops`)自动映射为 MCP 工具,含参数 Schema 与 `any_of` 预检,无需为每类设备写适配代码。
- **协议级占用安全**:控制前自动 `occupy`、自动携带 token、任务结束/进程退出自动 `release`,token 不写入 UDP/TXT/日志。
- **标准错误信封**:设备错误按 §4.7 信封(`ok/action/code/error/retryable`)透传给模型,`error` 为自然语言中文。
- **参数死循环防护**(1.3.0,默认开启):参数净化器自动矫正畸形入参(device 传成 `{id: 名称}` 对象、数字加引号、id/动作名近似错),同参连续失败升级提示直至停止令;报错附可逐字照抄的 `retry_with` 模板。
- **设备侧 SDK**:单文件 `sdk/bmahs_device.py`(仅标准库、拷走即用)——厂商声明「我是谁、有哪些动作」,发现/hello/信封/校验/占用全托管,声明即校验。
- **两种接入模式**:stdio(单客户端,MCP 客户端直接拉起)与 Streamable HTTP(多客户端共享一个网关进程,可选 Bearer Token 鉴权)。
## 安装
推荐隔离安装(uv tool 或 pipx):包与依赖装在独立环境,不污染系统/conda Python,任何终端可直接用 `bmahs-mcp`:
```bash
uv tool install "bmahs-mcp-gateway[http]" # 首选;只用 stdio 模式可去掉 [http]
pipx install "bmahs-mcp-gateway[http]" # 等效的 pipx 写法
```
也可直接 pip 装(装进当前 Python 环境,依赖与其它包共享、可能冲突,换环境后命令不可用):
```bash
pip install bmahs-mcp-gateway # stdio 模式,最小依赖
pip install "bmahs-mcp-gateway[http]" # 需要 Streamable HTTP 共享模式时
```
作为库集成时用 [uv](https://docs.astral.sh/uv/):`uv add bmahs-mcp-gateway`。要求 Python ≥ 3.10。
验证:`bmahs-mcp --version` 输出 `bmahs-mcp 1.4.0`。
## 快速开始
```bash
# 扫描局域网内的 BMAHS 设备
bmahs-mcp discover
# 联调:对设备执行一个动作(控制类动作自动 occupy → 执行 → release)
bmahs-mcp ctl 客厅灯 on
bmahs-mcp ctl 客厅灯 brightness --arg level=80
bmahs-mcp ctl 客厅灯 scene --arg name=cinema --ttl 600 # 指定占用租约 600 秒
bmahs-mcp ctl 客厅灯 on --no-release # 动作后保持占用(打印 token)
bmahs-mcp ctl 客厅灯 release --arg token=<占有时返回的 token> # 手动释放保持的占用
# 启动 MCP 网关(stdio,供 MCP 客户端连接;默认子命令)
bmahs-mcp serve
# 以 Streamable HTTP 共享模式启动(多客户端同时连接)
bmahs-mcp http --host 0.0.0.0 --port 9530 --token 换成你的令牌
```
在 MCP 客户端(ZCode / Claude Desktop / WorkBuddy / Trae 等通用 MCP 客户端)中配置 stdio 接入:
```json
{
"mcpServers": {
"bmahs": {
"command": "bmahs-mcp",
"args": ["serve"]
}
}
}
```
HTTP 模式的端点为 `http://<host>:9530/mcp`;设置了 `--token` 后客户端须携带 `Authorization: Bearer <token>`。
重启客户端后,模型可见两类工具:**7 个固定工具**——`bmahs_devices`(列设备)、`bmahs_refresh`(重扫描)、`bmahs_describe`(读自述)、`bmahs_occupy` / `bmahs_release`(占用/释放)、`bmahs_call`(泛化调用)、`bmahs_screenshot`(ui 设备抓屏);以及**每台设备的动态工具**——`<设备id>__<动作>`(如 `demo-light-001__brightness`),参数说明来自设备自述。典型流程:`bmahs_devices` 选型 → 直接调动态工具(网关自动 occupy 并携带 token,默认 120 秒租约)→ 用完 `bmahs_release`;token 由网关代管并遮蔽,不进模型上下文。
完整教程(也可在 GitHub 仓库 `docs/` 目录阅读):
- **[docs/quickstart.md](docs/quickstart.md)** —— 双轨快速开始:路线 A 十分钟跑通「三台虚拟设备 + MCP 客户端 + 一句话编排」;路线 B 写一台自己的设备。
- **[docs/write-a-device.md](docs/write-a-device.md)** —— 用设备侧 SDK 写一台设备:从声明到被模型调用的六步。
## 设备侧 SDK 与示例
**设备侧 SDK** [`sdk/bmahs_device.py`](sdk/bmahs_device.py):单文件、仅 Python 标准库、拷走即用(随 sdist 分发)。厂商只声明「我是谁、有哪些动作」,其余全部托管:UDP 发现(announce burst / 稳态心跳 / want 过滤应答 / goodbye)、连接即推 hello 通知、JSON-RPC 信封与 §9.1 错误码映射、参数校验(声明即校验,args 的 type/min/max/enum 自动变成 `bad-arg`/-32602)、last-wins / exclusive(token + 租约)占用状态机。业务失败直接 `raise DeviceError("busy", "…", retryable=True)`。六步教程见 [docs/write-a-device.md](docs/write-a-device.md)。
| 示例 | 说明 |
| --- | --- |
| `examples/dashboard.py` | 一键演示:两台虚拟开关 + 全彩氛围灯 + 组播自动发现 + 网页面板(`http://127.0.0.1:19530`) |
| `examples/demo_light.py` | 全彩灯完整参考实现(1.0/1.1/1.2 三形态、last-wins/exclusive、Bonjour、地址变化自愈) |
| `examples/demo_switch.py` | 虚拟开关设备(嵌入 dashboard 使用) |
| `examples/sdk_light.py` | 用 SDK 写的最小 1.2 示例灯(约 60 行声明,`sdk-light-001`,端口 9541) |
SDK 协议栈回归测试:`uv run python -m pytest tests/test_bmahs_device.py -q`。
## 常见问题
- **扫不到设备?** 确认设备已上电且同网段、Windows 防火墙放行 UDP 5354 入站;多网卡机器用 `BMAHS_MCAST_IF_V4` 指定网卡;跨网段/容器用 `BMAHS_STATIC_DEVICES=tcp://IP:端口` 静态表兜底。
- **报「正被 xxx 占用」(occupied)?** 设备独占中:`bmahs_devices` 看 `holder`/`until`,等租约到期或请占用方 `bmahs_release`;协议无强夺机制(防止两个模型打架)。
- **HTTP 模式 401?** 请求头须带 `Authorization: Bearer <--token 设置的值>`。
- **stdio 模式没有输出?** 正常:stdout 是 MCP 协议通道,日志全走 stderr(`BMAHS_LOG_LEVEL=debug` 调高)。
## 环境变量
| 变量 | 说明 |
| --- | --- |
| `BMAHS_AGENT_ID` | 网关在协议中的智能体 id(默认自动生成) |
| `BMAHS_STATIC_DEVICES` | 静态设备表,如 `tcp://192.168.1.10:9527`,逗号分隔 |
| `BMAHS_BONJOUR_BROWSE` | `0` 关闭 mDNS 浏览通道(默认开) |
| `BMAHS_AUTO_OCCUPY` / `BMAHS_AUTO_OCCUPY_TTL` / `BMAHS_MAX_LEASE` | 自动占用策略与租约上限 |
| `BMAHS_CALL_TIMEOUT` / `BMAHS_QUERY_INTERVAL` / `BMAHS_EXPIRE_SEC` | 调用超时、设备表刷新与过期时间 |
| `BMAHS_HTTP_HOST` / `BMAHS_HTTP_PORT` / `BMAHS_HTTP_PATH` / `BMAHS_HTTP_TOKEN` | HTTP 模式默认参数 |
| `BMAHS_LOG_LEVEL` | 日志级别(日志一律走 stderr,不污染 stdio 协议通道) |
| `BMAHS_ARG_COERCE` | `1` 开启参数净化器(防线②):device 传成对象自动解包、字符串数字转声明类型、id/动作名近似矫正;`0` 关闭 |
| `BMAHS_LOOP_GUARD` / `BMAHS_LOOP_GUARD_MAX` | `1` / `3` 防循环守卫(防线③):同一工具同参连续失败第 2 次升级提示、第 N 次下达停止令 |
| `BMAHS_DESCRIBE_OPTIONAL` | `1` bmahs_describe 的 device 可选(唯一设备自动选中,多台返回选择清单) |
| `BMAHS_DEVICE_TOOLS` | `0` 备用:为每台设备生成零参数 `<id>__describe` 动态别名 |
## 协议
BMAHS 协议要点:UDP 组播一报文一 JSON(≤1400 字节)负责发现,TCP 一行 JSON + `\n` 负责控制,连接后先读设备 hello;网关在协议中承担「智能体」角色。完整协议文档见 [docs/BMAHS1.0.md](https://github.com/XJPeng12/bmahs-mcp-gateway/blob/main/docs/BMAHS1.0.md)。
## 本地开发与构建
```bash
uv sync # 安装依赖(含 dev 组)
uv build # 在本目录构建 wheel + sdist(注意在包目录内执行并加 --out-dir dist)
uv publish # 发布到 PyPI(需配置 token)
uv run pytest # 运行测试
```
## License
[MIT](https://github.com/XJPeng12/bmahs-mcp-gateway/blob/main/LICENSE)
TDQS
A4.2/5.0
Scored across 7 tools
Disambiguation5/5
每个工具都有明确且不重叠的职责:列表、刷新、描述、占用、释放、通用调用和截图,描述清晰,不存在混淆风险。
Naming Consistency5/5
所有工具均以 bmahs_ 前缀,采用动词形式(devices, refresh, describe, occupy, release, call, screenshot),模式统一,一致性好。
Tool Count5/5
7 个工具覆盖设备发现、生命周期管理和操作执行,没有冗余,每个工具都必要,规模恰当。
Completeness4/5
覆盖了设备的发现、扫描、描述、占用/释放、操作调用和截图,但缺少如设备配置更新或批量操作等功能,不过对网关场景而言已基本完整。
Maintenance
ActivityMaintained
ResponsivenessNo issues