β‘ 60-second install
You need uv (curl -LsSf https://astral.sh/uv/install.sh | sh). Then:
uvx platformio.mcp install --claude-code # or --cursor --claude-desktop --codex --windsurf
No PlatformIO on this machine? Add --with-platformio and the server brings PlatformIO Core along. Optional extras: platformio.mcp[coredump] adds the ESP32 core-dump analyzer, platformio.mcp[power] adds the Nordic PPK2 driver.
Any other MCP client (Cline, Zed, Continue, ...)
uvx platformio.mcp install --print
{ "mcpServers": { "platformio": { "command": "uvx", "args": ["platformio.mcp"] } } }
Use "args": ["platformio.mcp[platformio]"] to bundle PlatformIO Core.
As a plugin (Claude Code, Codex, compatible plugin clients)
Install uv first. The plugin bundles PlatformIO Core automatically through platformio.mcp[platformio], plus a skill that teaches the build, flash, and verify loop.
In Claude Code, use /plugin (singular):
/plugin marketplace add powerdragonfire/platformio.mcp
/plugin install platformio@platformio.mcp
The same installation from a terminal:
claude plugin marketplace add powerdragonfire/platformio.mcp # Claude Code
claude plugin install platformio@platformio.mcp
For Codex, add the repository marketplace and install the plugin:
codex plugin marketplace add powerdragonfire/platformio.mcp
codex plugin add platformio@platformio.mcp
In Codex's plugin browser (/plugins where supported), add powerdragonfire/platformio.mcp as a marketplace source and select PlatformIO. Restart the client or open a new session after installation. Remove any separately configured platformio MCP server before enabling the plugin to avoid duplicate registrations.
The repo includes portable plugin.json and mcp.json, Claude compatibility files, a Codex manifest and marketplace, and skills/platformio/SKILL.md. These are repository marketplace installs, not a claim of listing in a public plugin directory. See the OpenAI packaging reference and Claude plugin reference.
Other MCP clients can use uvx platformio.mcp install --print --with-platformio and paste the resulting configuration. Plugin commands belong to the client; there is no universal /plugins command across LLM applications.
Already have PlatformIO?
The server finds platformio / pio on your PATH or in ~/.platformio/penv. Override with PLATFORMIO_MCP_PIO=/path/to/pio. Run uvx platformio.mcp doctor to see what the agent will see.
π¬ What it looks like
You: flash the
viewenv and make sure it boots.Agent β
pio_flash_and_verify(env="view", expect="setup done")FAIL: firmware flashed but the boot log matched the failure pattern: 'Guru Meditation Error: Core 1 panic'ed (LoadProhibited)'. Cause: LoadProhibited. 3/3 addresses resolved: DisplayTask::run() (display_task.cpp:22) <- DisplayTask::run() (display_task.cpp:23) <- vPortTaskWrapper (port.c:143)Agent: null pointer on line 22 of
display_task.cpp,tft_is used beforebegin(). Fixing, rebuilding, flashing again.PASS: flashed env view in 14.2s and saw 'setup done' on /dev/cu.usbserial-0001 after 2.1s of boot output.
No 40 KB build logs in the context window. No human reading the serial monitor. The agent gets a verdict, a file and a line.
π The loop the agent runs
flowchart LR
A[pio_project_envs] --> B[edit code]
B --> C[pio_build]
C -- errors with file:line --> B
C -- ok --> D[pio_flash_and_verify]
D -- PASS --> E([done])
D -- FAIL: decoded backtrace --> B
D -- TIMEOUT --> F[pio_monitor_capture]
F --> B
π§° The 44 tools
Every tool returns ok, a one-paragraph summary written for the model, structured fields, and a log_path to the full output. Long output stays on disk under ~/.platformio-mcp/logs (newest 200 files kept).
The tools that go beyond the CLI
| What it does | Under the hood | |
|---|---|---|
π pio_flash_and_verify | Flash, open the port, read until expect matches (pass), a crash signature matches (fail, auto-decoded), or the timeout passes (timeout) | pio run -t upload + pyserial; fail_on defaults to Guru Meditation, HardFault, abort(), assert failed, watchdog, brownout, heap corruption |
π pio_test_plan_* | Run a declarative hardware test plan for N iterations with cancellation, per-iteration verdicts (pass/fail/timeout/infra_error), and evidence logs | Reuses upload + serial monitor tools, continues observing for a post-success window, and writes machine-readable reports with source+firmware identity |
π©Ί pio_decode_backtrace | Turn an ESP32 Backtrace: 0x400d... dump or a Cortex-M pc/lr dump into function, file, line, inlined frames, cause, reset reason | Toolchain located from pio project metadata, then <target>-addr2line -pfiaC on firmware.elf; fixes Xtensa A0 window bits |
π pio_size_report | Why is the firmware this big? Flash/RAM %, loaded sections, biggest symbols with file:line, per-file totals, regex filter | pio run -t checkprogsize (partition-aware) + GNU size -A + nm -S -C -l --size-sort |
πΎ pio_partition_table | Catch the silent ESP32 corruption where an app-only flash leaves an old partition table on the chip; alignment, overlap, OTA slot, and app-fit checks | Parses the env's partition CSV; read_device=true reads 0x8000 with esptool read_flash and diffs |
π§― pio_coredump | Pull the core dump from the coredump partition after a crash and decode task, registers, and backtrace | esptool read_flash + optional esp-coredump info_corefile (platformio.mcp[coredump]) |
π pio_memory_watch | Leak, fragmentation, and stack-headroom verdicts from what the firmware already prints | Parses Free heap:, heap_caps_print_heap_info, vTaskList, uxTaskGetStackHighWaterMark lines; least-squares slope |
π pio_power_profile | Average/min/max/p95 current, sleep vs active split, energy, battery-life estimate | A serial meter (INA219 sketch, USB meter log) or a Nordic PPK2 (platformio.mcp[power]) |
π pio_debug_* | Breakpoints, step, backtrace, and variable inspection through the debug probe | pio debug --interface=gdb driven over GDB/MI with parsed *stopped events |
π pio_upload_ota | Flash over Wi-Fi with failures mapped to the fix (wrong password, no ArduinoOTA.handle(), firewall, no OTA slot) | pio run -t upload --upload-port <ip> (espota auto-switch) or espota.py directly |
π pio_port_diagnose | Why the upload cannot open the port: our session, another process, permissions, or a board not in bootloader mode | lsof/fuser + pio device list; never kills anything |
π pio_deps_check | Library name collisions where lib_deps order silently picks the winner, unpinned specs, leftovers, cycles | Manifests in .pio/libdeps and lib/, plus the LDF dependency graph with build=true |
π Repeatable hardware test plans
Use declarative JSON plans when you need reproducible board validation over many iterations.
{
"plan_version": 1,
"name": "SPI loopback",
"project_dir": "/abs/project",
"environment": "ch32h4_loopback",
"device_identity": "bench-ch32h4-01",
"board": { "platformio_env": "ch32h4_loopback", "board_id": "genericCH32H4" },
"peripherals": ["SPI loopback"],
"pin_mapping": { "SPI_MOSI": "PB15", "SPI_MISO": "PB14" },
"fixture": { "required_wiring": ["PB15 to PB14"] },
"run": { "iterations": 100, "upload_each_iteration": true, "stop_open_sessions": true },
"serial_assertion": {
"success_pattern": "SPI_LOOPBACK_OK",
"failure_pattern": "panic|HardFault|assert failed|test-failure",
"timeout_s": 20,
"observation_window_s": 5,
"reconnect_attempts": 1
}
}
Workflow:
pio_test_plan_validate(plan_path=...)pio_test_plan_start(plan_path=...)- poll
pio_test_plan_status(run_id=...) - optional cancel:
pio_test_plan_cancel(run_id=...)
Reports include source revision + dirty state, firmware hash, selected env, and detected device identity. Every non-pass iteration stores evidence logs with references in the JSON report.
An end-to-end CH32H4 fixture + firmware example is in examples/ch32h4-spi-loopback.
π Safety policy
Set PLATFORMIO_MCP_POLICY in the server's env, or pass --policy to install:
| Policy | Can build | Can flash / erase / write serial | Use it for |
|---|---|---|---|
full (default) | β | β | Your own bench |
build_only | β | β | Shared labs, CI, "look but don't touch" |
read_only | β | β | Code review, onboarding, untrusted prompts |
MCP clients also prompt before each tool call. Policies are the second layer, not the only one.
βοΈ Settings
| Variable | Purpose | Default |
|---|---|---|
PLATFORMIO_MCP_POLICY | full, build_only, read_only | full |
PLATFORMIO_MCP_PROJECT_DIR | Project used when a tool is called without project_dir | server's cwd |
PLATFORMIO_MCP_PIO | Explicit path to the pio executable | auto-detect |
PLATFORMIO_MCP_LOG_DIR | Where full command logs go | ~/.platformio-mcp/logs |
PLATFORMIO_MCP_MAX_LOGS | How many log files to keep | 200 |
PLATFORMIO_MCP_ANALYTICS | on, local (log only, nothing sent), off (no log, nothing sent) | on |
DO_NOT_TRACK | 1 stops anonymous usage events to PostHog; the local log stays | unset |
PLATFORMIO_MCP_ANALYTICS_DIR | Where the usage log goes | ~/.platformio-mcp/analytics |
PLATFORMIO_MCP_ANALYTICS_URL | Also POST each usage event as JSON to this URL | unset |
π Usage analytics
Each tool call writes one JSON line to ~/.platformio-mcp/analytics/usage.jsonl on your machine. Releases also send the same event to PostHog EU Cloud (eu.i.posthog.com) as an anonymous event, so the maintainers can see which tools get used and which fail.
An event holds the tool name, ok, the error code, the duration, the names of the arguments passed, the server version, the policy, the OS, the Python version, the MCP client's name and version, and the MCP protocol version. When a tool receives a board or platform argument shaped like a PlatformIO registry id (esp32dev, espressif32), the event keeps that id. A path or URL in those arguments is dropped. The built-in recorder excludes other argument values, paths, ports, summaries, and serial output. The default PostHog SDK separately captures agent-supplied intent context and missing-capability reports, which can contain user content. PostHog sees a random install id from ~/.platformio-mcp/analytics/install_id and no person profile. The first run that sends events prints this notice on stderr.
By default, the built-in recorder sends three PostHog event types in batches of up to 20 events or every 5 seconds, and flushes them when the server shuts down:
| Event | When | Extra properties |
|---|---|---|
$mcp_tool_call | each tool call | $mcp_tool_name, $mcp_is_error, $mcp_error_type, $mcp_duration_ms, $mcp_input_keys, board, platform |
$mcp_initialize | once per process, when the client connects | none |
$mcp_tools_list | once per process, on the first tool list | $mcp_listed_tool_names |
Every PostHog event also carries $session_id (a random id per server process, which is one client connection over stdio), $mcp_source, $mcp_client_name, $mcp_client_version, $mcp_protocol_version, $mcp_server_name, $mcp_server_version, $lib, $lib_version, policy, os, and python. The local log and PLATFORMIO_MCP_ANALYTICS_URL get tool calls only, one POST per event.
| To | Set |
|---|---|
| Keep the local log, send nothing | DO_NOT_TRACK=1 or PLATFORMIO_MCP_ANALYTICS=local |
| Record nothing at all | PLATFORMIO_MCP_ANALYTICS=off |
| Also send raw events to your own endpoint | PLATFORMIO_MCP_ANALYTICS_URL=https://... |
Nothing is sent to PostHog when CI is set, so CI runs do not count as usage.
Version, model, effort, and optional prompts
Both analytics paths report the installed server release as $mcp_server_version and platformio_mcp_version. These identify the running Python package, which can differ from a cached plugin manifest version.
Tool events also record available model and reasoning-effort metadata. Codex supplies model and reasoning_effort in _meta["x-codex-turn-metadata"] (upstream implementation). Other hosts can supply the same fields in _meta["platformio.mcp"]. Unknown values are omitted; no model or effort is inferred from a client name. Model names are bounded identifiers; effort accepts none, minimal, low, medium, high, xhigh, max, or ultra.
| Property | Meaning |
|---|---|
$mcp_llm_model, $mcp_llm_model_source | Model identifier and client_metadata, SDK self_reported, or configuration source |
reasoning_effort, reasoning_effort_source | Supplied effort and client_metadata or configuration source |
user_prompt, user_prompt_source, user_prompt_truncated | Optional original prompt, host metadata source, and truncation flag |
If the host cannot send model or effort metadata, explicitly set PLATFORMIO_MCP_MODEL and PLATFORMIO_MCP_REASONING_EFFORT in the MCP server environment. Per-call metadata takes precedence. Keep configured values aligned with the client's selected model and effort; they are configuration claims, not automatic detection. With the SDK active, its optional llm_model argument remains available and takes precedence over configured model fallback.
Original prompt capture is off by default. A user must set PLATFORMIO_MCP_CAPTURE_PROMPTS=1 in their MCP server environment and their host must explicitly attach the prompt to each tool request:
{
"_meta": {
"platformio.mcp": {
"model": "actual-model-id",
"reasoning_effort": "high",
"prompt": "Build the firmware and inspect the serial output"
}
}
}
The server retains at most the first 2,000 characters of this prompt, including in local logs and custom endpoint events. Enabled remote delivery sends it to PostHog. Prompt text is user content and may contain secrets; no complete secret removal is promised. Only explicitly allowlisted metadata is captured, with no scraping of client chat files. Disabling this option stops future original prompt capture; it does not delete previously collected events. Existing off, local, DO_NOT_TRACK, and CI delivery controls still apply.
Ordinary Claude/Codex plugin installation does not automatically supply original prompts through this custom metadata contract. A host integration must provide it. SDK agent context remains separate intent text, not a copy of the original conversation. These prompt settings control only the new user_prompt field, not the existing SDK context capture described below.
When the SDK is active, report_missing=True exposes a get_more_tools tool. Server instructions ask agents to describe their goal in tool context and call get_more_tools when available tools cannot fulfill a request. Agents can use it to report unsupported capabilities; the SDK sends $mcp_missing_capability events containing the supplied context to PostHog.
The PostHog MCP SDK is enabled by default using the bundled public ingestion key, so ordinary installations advertise intent context and missing-capability reporting without extra configuration. Set POSTHOG_PROJECT_TOKEN to override the ingestion key for a different project; otherwise PLATFORMIO_MCP_POSTHOG_KEY overrides the bundled key. With no non-empty key, the SDK is disabled. The SDK starts only when analytics mode is on (the default), DO_NOT_TRACK and CI are unset or false (0, false, or no). POSTHOG_HOST, then PLATFORMIO_MCP_POSTHOG_HOST, overrides its default EU host. While the SDK runs, the built-in recorder keeps the local tool-call log and custom endpoint delivery but stops its own PostHog sending, preventing duplicate events. The SDK controls its event format and batching; the event table above describes the built-in recorder. Normal server shutdown flushes and closes the SDK client; cleanup failures do not prevent shutdown. Importing the server creates no PostHog client.
Context and missing-capability reports are agent-supplied text, separate from optional original prompt capture, and are sent remotely when analytics is enabled. Agents are instructed to omit secrets and private source code. Missing-capability capture depends on agents calling the reporting tool; the server cannot observe goals or gaps that clients never send. Restart the server and refresh cached client tool lists after upgrading to expose the instrumented schemas.
SDK intent capture is explicitly enabled with MCPAnalyticsOptions(context=True, intent_fallback=...). The SDK injects a context argument and records non-empty agent context as $mcp_intent with $mcp_intent_source=context_parameter. When context is omitted, empty, or whitespace, the fallback records Invoking <registered tool name> with source inferred. The fallback reads no argument values or transport metadata and makes no LLM calls. This identifies the tool operation, not the user's underlying goal. Agent-supplied context takes precedence and passes through the SDK's redaction pipeline. The built-in recorder does not capture intent. See PostHog's intent documentation.
uvx platformio.mcp stats # calls, ok rate, p50/p95 duration per tool, top errors, clients
uvx platformio.mcp stats --days 7 --json
The log rotates at 5 MB and keeps one previous file.
π Serial monitor notes
Sessions talk to the port with pyserial directly, because PlatformIO's own monitor needs an interactive terminal. PlatformIO monitor filters such as esp32_exception_decoder therefore do not apply; pio_decode_backtrace does that job. Baud and port default from monitor_speed / monitor_port in platformio.ini when project_dir is passed, otherwise the single detected dev board at 115200. Opening the port resets most dev boards, which is why pio_flash_and_verify sees the boot log from the top.
π οΈ Development
git clone https://github.com/powerdragonfire/platformio.mcp && cd platformio.mcp
uv sync
uv run pytest # unit tests, no hardware or network
uv run pytest -m integration # builds the bundled native fixture with your PlatformIO
uv run platformio-mcp doctor # what the agent's pio_system_info sees
npx @modelcontextprotocol/inspector uv run platformio-mcp # poke tools interactively
To use your checkout in Claude Code instead of the PyPI release:
claude mcp add platformio -- uv run --directory /path/to/platformio.mcp platformio-mcp
Changes are tracked in CHANGELOG.md.
π€ Contributing
Bug reports from real boards are the most useful thing you can send. Use the issue forms, ask questions in Discussions, and read CONTRIBUTING.md before opening a PR. Issues tagged good first issue are scoped for newcomers.
π Prior art
jl-codes/platformio-mcp is a TypeScript server with the same goal, a web dashboard, and a GPIO pin audit. This project exists for people who want a Python-only install through uvx, one that can bundle PlatformIO itself, and crash decoding, size budgeting, partition checks, core dumps, OTA, live GDB, and memory/power profiling built in.
License
MIT