Skip to content

powerdragonfire/platformio

v0.6.2MIT

Build, flash, monitor, test, and debug embedded firmware through PlatformIO: 44 MCP tools plus a skill that teaches the build-flash-verify loop.


⚑ 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 view env 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 before begin(). 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 doesUnder the hood
πŸš€ pio_flash_and_verifyFlash, 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 logsReuses upload + serial monitor tools, continues observing for a post-success window, and writes machine-readable reports with source+firmware identity
🩺 pio_decode_backtraceTurn an ESP32 Backtrace: 0x400d... dump or a Cortex-M pc/lr dump into function, file, line, inlined frames, cause, reset reasonToolchain located from pio project metadata, then <target>-addr2line -pfiaC on firmware.elf; fixes Xtensa A0 window bits
πŸ“Š pio_size_reportWhy is the firmware this big? Flash/RAM %, loaded sections, biggest symbols with file:line, per-file totals, regex filterpio run -t checkprogsize (partition-aware) + GNU size -A + nm -S -C -l --size-sort
πŸ’Ύ pio_partition_tableCatch the silent ESP32 corruption where an app-only flash leaves an old partition table on the chip; alignment, overlap, OTA slot, and app-fit checksParses the env's partition CSV; read_device=true reads 0x8000 with esptool read_flash and diffs
🧯 pio_coredumpPull the core dump from the coredump partition after a crash and decode task, registers, and backtraceesptool read_flash + optional esp-coredump info_corefile (platformio.mcp[coredump])
πŸ“ˆ pio_memory_watchLeak, fragmentation, and stack-headroom verdicts from what the firmware already printsParses Free heap:, heap_caps_print_heap_info, vTaskList, uxTaskGetStackHighWaterMark lines; least-squares slope
πŸ”‹ pio_power_profileAverage/min/max/p95 current, sleep vs active split, energy, battery-life estimateA 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 probepio debug --interface=gdb driven over GDB/MI with parsed *stopped events
🌐 pio_upload_otaFlash 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_diagnoseWhy the upload cannot open the port: our session, another process, permissions, or a board not in bootloader modelsof/fuser + pio device list; never kills anything
πŸ“š pio_deps_checkLibrary name collisions where lib_deps order silently picks the winner, unpinned specs, leftovers, cyclesManifests 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:

  1. pio_test_plan_validate(plan_path=...)
  2. pio_test_plan_start(plan_path=...)
  3. poll pio_test_plan_status(run_id=...)
  4. 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:

PolicyCan buildCan flash / erase / write serialUse 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

VariablePurposeDefault
PLATFORMIO_MCP_POLICYfull, build_only, read_onlyfull
PLATFORMIO_MCP_PROJECT_DIRProject used when a tool is called without project_dirserver's cwd
PLATFORMIO_MCP_PIOExplicit path to the pio executableauto-detect
PLATFORMIO_MCP_LOG_DIRWhere full command logs go~/.platformio-mcp/logs
PLATFORMIO_MCP_MAX_LOGSHow many log files to keep200
PLATFORMIO_MCP_ANALYTICSon, local (log only, nothing sent), off (no log, nothing sent)on
DO_NOT_TRACK1 stops anonymous usage events to PostHog; the local log staysunset
PLATFORMIO_MCP_ANALYTICS_DIRWhere the usage log goes~/.platformio-mcp/analytics
PLATFORMIO_MCP_ANALYTICS_URLAlso POST each usage event as JSON to this URLunset

πŸ“Š 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:

EventWhenExtra properties
$mcp_tool_calleach tool call$mcp_tool_name, $mcp_is_error, $mcp_error_type, $mcp_duration_ms, $mcp_input_keys, board, platform
$mcp_initializeonce per process, when the client connectsnone
$mcp_tools_listonce 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.

ToSet
Keep the local log, send nothingDO_NOT_TRACK=1 or PLATFORMIO_MCP_ANALYTICS=local
Record nothing at allPLATFORMIO_MCP_ANALYTICS=off
Also send raw events to your own endpointPLATFORMIO_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.

PropertyMeaning
$mcp_llm_model, $mcp_llm_model_sourceModel identifier and client_metadata, SDK self_reported, or configuration source
reasoning_effort, reasoning_effort_sourceSupplied effort and client_metadata or configuration source
user_prompt, user_prompt_source, user_prompt_truncatedOptional 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