Skip to main content
Glama
SunstanYu

embedded-mcp

by SunstanYu
README.md
# embedded-mcp

给 AI agent 用的嵌入式开发板 MCP server:编译、烧录、读串口、擦除,带护栏。

## 为什么分层

```
protocol.py   JSON-RPC / 工具分派      ← 完全不知道硬件存在
tools.py      工具实现 + 所有护栏
backends/     工具链适配(怎么编译、怎么烧)
boards.py     板型档案(纯数据)
state.py      锁 / 预算 / 模式标记
```

**变的是工具链,不是板。** RP2040、SAMD、AVR 全都走 `arduino-cli`,加它们
一行代码都不用写,只在 `boards.py` 里加一条数据。真正需要新 Backend 的是
换了构建体系的:ESP-IDF 的 `idf.py`、PlatformIO 的 `pio`、STM32 的 `openocd`。

护栏全在工具层,Backend 只管执行——**新增工具链不需要重新实现任何一道闸**。

## 三类状态的边界不同

这是 `state.py` 存在的唯一理由:

| 状态 | 边界 | 放哪 |
| --- | --- | --- |
| 串口锁 | 物理硬件 | `~/.embedded-mcp/locks/<port>.json` |
| 下载模式标记 | 物理硬件 | `~/.embedded-mcp/mode/<port>.flash` |
| 烧录预算 | 项目 / 会话 | `~/.embedded-mcp/projects/<项目>/iteration_count` |

把它们堆在一个项目目录下看着像一类东西,插上第二块板就露馅:两个项目各锁
各的文件,等于没锁。

烧录预算**不是** flash 寿命保护(ESP32 flash 有 10 万次擦写寿命,5 次这个
量级显然不是为寿命设的),它是**熔断器**,防 agent 陷入「改一点→烧→还不对」
的失控循环。所以边界是项目,不是硬件。

## 安装

```bash
python3 -m pip install pyserial          # read_serial 需要
brew install arduino-cli                 # 或按官方文档安装
arduino-cli core install esp32:esp32     # 按你的板子装 core
```

## 用法

```bash
python3 -m embedded_mcp doctor           # 检查环境、串口、项目
python3 -m embedded_mcp boards           # 列出已知板型
python3 -m embedded_mcp confirm /dev/cu.usbmodem101   # 人工确认已进下载模式
python3 -m embedded_mcp serve --project ~/code/my-sketch
```

接进 Claude Code,在项目里放 `.mcp.json`:

```json
{
  "mcpServers": {
    "embedded": {
      "type": "stdio",
      "command": "python3",
      "args": ["-m", "embedded_mcp", "serve", "--project", "."],
      "env": {"PYTHONPATH": "/Users/you/code/embedded-mcp"}
    }
  }
}
```

## 工具

| 工具 | 级别 | 护栏 |
| --- | --- | --- |
| `status` | 只读 | 无 |
| `compile` | 只读 | 无 |
| `read_serial` | 只读 | 串口互斥 |
| `flash` | 写硬件 | 预算上限 + 串口互斥 + 条件性人工确认 |
| `erase_flash` | 危险 | 强制 human-in-the-loop(MRTR 两程确认) |

`flash` 的人工确认**由板型决定**:`auto_download_mode=True` 的板(实测
ESP32-S3 的 esptool 能自己触发下载模式复位)直接烧;`False` 的板
(RP2040 要按 BOOTSEL)才要求先跑 `confirm`。

## 加一块新板

多数情况只改数据:

```python
BoardProfile(
    id="my_board",
    display_name="My Board",
    fqbn="vendor:arch:board",
    usb_vids=(0x1234,),
    auto_download_mode=True,
    reenumerates_after_flash=False,
    crash_markers=("PANIC",),
)
```

字段大多是踩出来的:`reenumerates_after_flash` 来自「ESP32-S3 烧完从
Espressif VID 变成 Adafruit VID」;`boot_log_window_s` 来自「setup() 的输出
会在主机连上串口之前就打完」。

## 加一种工具链

继承 `Backend`,实现 `detect` / `compile` / `flash`,用 `@register` 注册。
不用碰协议层,也不用重新实现护栏。

## 已知限制

- `erase_flash` 走 MRTR(2026-07-28),**legacy 客户端不认识 `resultType:
  "input_required"`**,在它们那里只能走降级路径(告知用户手工执行)。
- 本 server 只能保证经由它的调用守规矩。Host 给模型的 Bash 工具它管不了——
  要拦住那条路得靠 hooks,两者互补而非替代。

## 测试

```bash
python3 tests/smoke.py       # 19 项,不碰硬件
```

TDQS

A4.3/5.0

Scored across 5 tools

Disambiguation5/5

Each tool maps to a distinct operation: compile, flash, read serial, erase flash, and status. Although flash and erase_flash both touch the flash memory, their actions and safety levels are clearly separated. No tool appears to duplicate another's purpose.

Naming Consistency4/5

Most tools use lowercase snake_case and a verb-like pattern: erase_flash, read_serial, compile, flash. 'status' is a noun-style exception, but it is still a common and predictable command name. Minor deviation does not cause confusion.

Tool Count5/5

Five tools is a tight, well-scoped set for an embedded development workflow. Each tool serves a necessary step in the compile-flash-debug cycle. No redundant or filler tools are present.

Completeness4/5

The core workflow is covered: compile, flash, read_serial, erase_flash, and status provide a complete lifecycle for flashing and debugging a board. Minor gaps remain, such as no explicit reset/reboot tool or project selection command, but agents can work around these with status and serial output. Overall the surface is well matched to its declared purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues