Command Palette

Search for a command to run...

UnylyUnyly
Browse all

ESP32 AI Loop

FreeNot checked

Closes the ESP32 firmware development loop — compile, flash, and monitor device output autonomously via 11 tools for serial port management and ESP-IDF operatio

GitHubEmbed

About

Closes the ESP32 firmware development loop — compile, flash, and monitor device output autonomously via 11 tools for serial port management and ESP-IDF operations.

README

License: MIT Python 3.10+ Platforms: macOS · Windows · Linux Built with FastMCP

Give Claude Code a real ESP32 on its desk. An MCP server that closes the edit → build → flash → observe loop on real hardware. The agent compiles, flashes, and reads device output autonomously — no human-in-the-loop for every iteration.

TL;DR

Eleven MCP tools that let an AI assistant — or you — drive an ESP32 dev board over USB without ever switching to a terminal:

You: "make the heartbeat print uptime in milliseconds too"

AI: edits main.c
    → build()
    → start_monitor()
    → flash()                    (monitor steps aside, returns automatically)
    → grep_log("tick=", last_n_only=200)
    → "done — uptime_ms=15ms shows up in tick=0, ticks 1..3 firing every 5s"

The server speaks MCP over stdio, runs on macOS, Windows, and Linux, and installs in one line via uvx. Designed primarily for Claude Code but works with any MCP client (Claude Desktop, Cursor, Cline, …).

Why this exists

Firmware development without device access is guess-work. For a human, the official idf.py monitor works fine — it ties up a terminal, you watch it in real time. For an AI assistant, that's a non-starter:

  • The agent can't watch a TTY in real time; it can only run a command, wait for it to exit, and read the output.
  • An always-running idf.py monitor blocks the next idf.py flash because the port is busy.
  • Without seeing device output, every change the AI makes is speculative — it can ship code that compiles cleanly and is silently broken at runtime.

This server closes that gap. The monitor runs as a background thread inside the MCP server, writes to a log file, and automatically yields the port when a flash starts. The agent calls flash() and grep_log() as first-class MCP tools — no shell, no port juggling.

Install

Prerequisite: ESP-IDF installed and activated in the shell that launches your MCP client. On macOS/Linux: source $IDF_PATH/export.sh. On Windows (PowerShell): . $env:IDF_PATH\export.ps1. The Espressif Installation Manager (EIM) is the recommended way to install IDF on every platform.

You'll also need uv (one-time, fast, ~5 MB):

Platform Command
macOS brew install uv
Windows winget install astral-sh.uv
Linux curl -LsSf https://astral.sh/uv/install.sh | sh

Path A — claude mcp add (recommended)

From the root of your ESP-IDF project:

claude mcp add esp32-ai-loop --scope project -- uvx esp32-ai-loop-mcp-server -C .

--scope project writes a .mcp.json you can commit, so anyone who clones the repo and re-launches Claude Code gets the same tools.

Path B — .mcp.json by hand

Drop this file at your project root:

{
  "mcpServers": {
    "esp32-ai-loop": {
      "type": "stdio",
      "command": "uvx",
      "args": ["esp32-ai-loop-mcp-server", "-C", "."]
    }
  }
}

Restart Claude Code from that directory. It'll show a trust dialog the first time it sees a new .mcp.json — accept it. Tools appear as mcp__esp32-ai-loop__build, …__flash, …__start_monitor, etc.

Other MCP clients work too. Claude Desktop, Cursor, and Cline all accept the same mcpServers JSON shape — just paste it into their config file.

What you get — the 11 tools

Three groups: serial monitor, ESP-IDF actions, port discovery.

Tool What it does
start_monitor Open a port, stream every line into a log file, auto-reconnect on disconnect, pulse RTS to capture the boot banner.
close_monitor Stop the monitor and release the port.
monitor_status Is the monitor running? On which port? Where's the log? Uptime, last error.
tail_log Return the last N lines of the log — replaces shell tail.
grep_log Search the log with a regex; optionally limited to the last N lines. Saves the agent a lot of context.
build Run idf.py build.
flash Run idf.py flash with automatic monitor coordination — port is yielded for the duration, resumed afterward, even on failure.
clean Run idf.py clean.
set_target Run idf.py set-target <chip> (esp32, esp32s3, esp32c3, esp32c6, …).
reset Pulse RTS to reboot the chip without reflashing — coordinates with the monitor too.
list_ports Cross-platform USB-CDC enumeration with VID/PID and an is_known_esp flag.
Tool signatures (full parameter list)
  • start_monitor(port?, baud=115200, log_path?, truncate=False, reset_on_connect=True)
  • close_monitor()
  • monitor_status()
  • tail_log(lines=50, log_path?)
  • grep_log(pattern, lines=100, case_insensitive=False, last_n_only?, log_path?)
  • build()
  • flash(port?, target?)
  • clean()
  • set_target(target)
  • reset(port?)
  • list_ports()

All optional parameters have sensible defaults. port defaults to whatever list_ports() autodetects when exactly one ESP32-class device is plugged in.

Every connect captures a full boot

When the monitor opens (or reopens) the port, it pulses RTS to hard-reset the chip — same trick idf.py monitor uses on connect. That guarantees the next thing the AI sees in the log is the ROM banner, the bootloader header, and your app_main startup output, every single time.

For human debugging that's nice-to-have. For an AI that has to infer device state from text, a clean boot capture every iteration is the difference between "I can tell you exactly which init step failed" and "I'm guessing — please run it again and paste the output."

Disable per-call with start_monitor(reset_on_connect=False) if you need to attach to a running device without disturbing it.

How it works (architecture)

One Python process. The MCP tools and the serial monitor live as siblings inside it; they coordinate via in-process events, no signals, no PID files.

            ┌─────────────────────────────────────────┐
            │   Claude Code / Cursor / Cline / ...    │
            └─────────────────────┬───────────────────┘
                                  │ MCP (stdio JSON-RPC)
                                  ▼
   ┌─────────────────────────────────────────────────────────────┐
   │            esp32-ai-loop-mcp-server (Python)                │
   │                                                             │
   │   ┌─────────────────────┐       ┌────────────────────────┐  │
   │   │ MCP tools           │       │ monitor thread         │  │
   │   │                     │ yield │  • USB-CDC reader      │  │
   │   │ • build  • flash    │──────►│  • RTS reset on every  │  │
   │   │ • clean  • reset    │       │    connect → boot log  │  │
   │   │ • set_target        │◄──────│  • auto-reconnect      │  │
   │   │ • start/close_      │resume │  • yields on flash     │  │
   │   │   monitor           │       └────────────┬───────────┘  │
   │   │ • monitor_status    │                    │              │
   │   │ • tail_log          │                    │ append+flush │
   │   │ • grep_log          │                    ▼              │
   │   │ • list_ports        │     <project>/logs/serial.log     │
   │   └──────────┬──────────┘                                   │
   │              │ subprocess                                   │
   │              ▼                                              │
   │       idf.py build / flash                                  │
   │       (ESP-IDF env activated in the parent shell)           │
   └─────────┬────────────────────────────┬──────────────────────┘
             │ esptool (USB-CDC)          │ pyserial (USB-CDC)
             ▼                            ▼
           ┌──────────────────────────────────┐
           │           ESP32 board            │
           └──────────────────────────────────┘

The flash + monitor handoff

Three things compete for the USB-CDC port: the monitor (reading device output), esptool (when flashing), and the reset helper. Only one can hold the port at a time.

Time  │ flash() tool                    │ monitor thread                    │ Port
──────┼─────────────────────────────────┼───────────────────────────────────┼──────
 t0   │ monitor.yield_port()            │                                   │ open
      │   (sets threading.Event)        │                                   │
 t0+  │                                 │ event observed → break read loop  │
      │                                 │ → ser.close()                     │ closed
 t1   │ subprocess: idf.py flash        │                                   │
      │   esptool: open port            │                                   │ esptool
      │   ...flashing...                │   (parked, yield_event still set) │
 t2   │   esptool: hard reset, exit     │                                   │ closed
 t3   │ monitor.resume_port()           │                                   │
      │   (clears event, in finally)    │ event clear → reopen → RTS pulse  │ open
      │                                 │ → boot banner streams to log      │

Key properties:

  • Cross-platform. All coordination is in-process via threading.Event — no signals, no PID files, no named pipes. Works identically on macOS, Linux, and Windows.
  • Crash-safe. The flash tool releases the port in a finally block, so the monitor returns even if idf.py flash fails.
  • Safety-net deadline. If something kills the MCP server mid-flash, the monitor's 60-second yield ceiling kicks in and reconnects on its own.
  • Sub-200ms overhead. The monitor's read loop has a 200ms timeout, so yield + reacquire round-trip costs roughly that much vs raw esptool.

Quick start (with the bundled example)

# 1. Get the example project (or use any ESP-IDF project of yours).
git clone https://github.com/cmd0s/esp32-ai-loop-mcp-server.git
cd esp32-ai-loop-mcp-server/examples/hello-heartbeat

# 2. Activate ESP-IDF in this shell, then register the MCP server.
source $IDF_PATH/export.sh                              # macOS/Linux
# . $env:IDF_PATH\export.ps1                            # Windows PowerShell
claude mcp add esp32-ai-loop --scope project -- uvx esp32-ai-loop-mcp-server -C .

# 3. Launch Claude Code from this directory and ask:
#    "set target to esp32s3, build, flash, and watch for 30 seconds"
claude

The agent will pick set_target, build, start_monitor, flash, then grep_log("tick=") — and tell you whether the heartbeat is firing. The full session usually takes under 90 seconds end-to-end.

Comparison with adjacent tools

idf.py monitor idf.py mcp-server (built into IDF v6.0) esp32-ai-loop-mcp-server
MCP-native build / flash / set-target
Background log capture across flashes
Automatic port-yield during flash
tail_log / grep_log for AI context
reset (RTS pulse) without reflash
Cross-platform (macOS + Windows + Linux)
Persistent log file across sessions

Use idf.py mcp-server alone if you only need build/flash and you're fine watching idf.py monitor in a separate terminal. Use this server if you want the agent to drive the full loop without you copy-pasting log output back to it.

Configuration

The MCP server takes only one runtime knob — the project directory:

esp32-ai-loop-mcp-server -C /absolute/path/to/your/esp-idf/project

Everything else is read at tool-call time. The server inherits the parent shell's environment, so IDF_PATH, IDF_PYTHON_ENV_PATH, ESPPORT, etc. flow through to idf.py.

Logs go to <project>/logs/serial.log by default (override per-call with the log_path argument).

Caveats / known issues

  • ESP-IDF activation is your problem. This server shells out to idf.py and trusts that it's on PATH. If you forgot to source export.sh / export.ps1 before launching your MCP client, every build/flash tool returns a clear error pointing you at the activation step. Recommended: launch Claude Code from a shell that's already activated, or use Espressif's EIM to wrap the activation.
  • Single monitor at a time. The current design assumes one ESP32 per project. Multi-board support is on the roadmap.
  • Monitor dies with the MCP server. If you restart Claude Code, call start_monitor again. The log file persists — tail_log still works even with no monitor running.
  • Native USB-Serial/JTAG (S3/C3/C6/H2) reset quirks. RTS-driven auto-reset is universal on classic CP210x/CH9102 boards but can behave differently on chips using the SoC's built-in USB-Serial peripheral. The reset tool tries the standard pulse; if your board ignores it, use flash (which goes through esptool's full reset sequence).

About the author

Robert Mordzon

Built by Robert Mordzon — embedded + firmware + full-stack engineer and entrepreneur, with a focus on blockchain/Web3 systems. Behind the Web3 Pi project and Web3 Pi UPS hardware, and lead engineer of Pegasus Speedway — a live telemetry system for speedway (the dirt-track motorcycle sport) that locates riders to within a centimeter mid-race. Many years of broadcast-TV systems engineering before that.

This MCP server was born out of necessity: an AI assistant cannot debug a power-loss bug it never gets to observe — so I built the tool that lets it.

If this saves you a few hours of "the agent shipped broken firmware because it couldn't read the log," ⭐ the repo and let me know what you built.

License

MIT — see LICENSE.

Links

from github.com/cmd0s/esp32-ai-loop-mcp-server

Installing ESP32 AI Loop

This server has no published package — it is built from source. Open the repository and follow its README.

▸ github.com/cmd0s/esp32-ai-loop-mcp-server

FAQ

Is ESP32 AI Loop MCP free?

Yes, ESP32 AI Loop MCP is free — one-click install via Unyly at no cost.

Does ESP32 AI Loop need an API key?

No, ESP32 AI Loop runs without API keys or environment variables.

Is ESP32 AI Loop hosted or self-hosted?

Self-hosted: the server runs locally on your machine via the install command above.

How do I install ESP32 AI Loop in Claude Desktop, Claude Code or Cursor?

Open ESP32 AI Loop on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.

Related MCPs

Compare ESP32 AI Loop with

Not sure what to pick?

Find your stack in 60 seconds

Author?

Embed badge for your README

Browse similar

All ai MCPs