* asdf * Implement SphereOfInfluenceModule for traffic management and eviction tracking * Implement Sphere of Influence module for dynamic hop limit adjustment and role-based floor * Update SAMPLING_DENOMINATOR to improve mesh size estimation accuracy * Add debug logging for scale factor estimation and per-hop node counts in SphereOfInfluenceModule * Enable variable hop limits and role-based hop floors in Sphere of Influence module * Respond to copilot review * Disable variable hop limits and role-based hop floors in Sphere of Influence module * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Implement adaptive sampling for unique node ID tracking in Sphere of Influence module * Add state persistence for Sphere of Influence module * Enhance Sphere of Influence module with state management and adaptive sampling adjustments * Refactor hop scaling functionality: remove SphereOfInfluenceModule and introduce HopScalingModule - Deleted SphereOfInfluenceModule.h, consolidating its responsibilities into the new HopScalingModule. - Added HopScalingModule.h and HopScalingModule.cpp to manage hop scaling logic, including eviction tracking and sampling-based mesh size estimation. - Implemented methods for recording evictions and packet senders, estimating scale factors, and computing required hops based on node activity. - Introduced state persistence for hop scaling parameters to maintain continuity across reboots. - Enhanced thread safety and modularity by utilizing concurrency features. * Guard out STM32. Sowwy. * Refactor HopScalingModule: enhance sampling logic and improve state management * Add unit tests for HopScalingModule: implement mock database and various test scenarios * Refactor test output in HopScalingModule tests: replace printf with TEST_MESSAGE for better integration with Unity * Refactor HopScalingModule logging: replace lastStatusMode with descriptive mode names for improved readability * Refactor test_main.cpp: change NodeNum variable to static and improve comments for clarity * Remove unnecessary delay in setup function for improved test performance * Add missing include for MeshTypes in test_main.cpp * Refactor HopScalingModule tests: enhance mesh topology scenarios and improve test clarity * Update HopScalingModule tests: flesh out node scenarios and improve clarity for dense and sparse mesh cases * Fix politeness factor calculations in HopScalingModule and update related test scenarios for clarity. Remove outdated design doc. * Enhance HopScalingModule: add sampled estimate for scaling decisions and refactor initial run state management * Add sample traffic injection for HopScaling tests to enhance sampledEst visibility * Enhance HopScalingModule: adjust windowFraction calculation for early triggers and improve test output formatting * Enhance HopScalingModule: add jitter functionality to sampling denominator and update tests for consistent behavior * Enhance HopScalingModule: implement adaptive sampling denominator adjustment and add reset functionality for tests * Enhance HopScalingTestShim: add test-only clock and window helpers, update injectSampleTraffic for adaptive sampling, and improve scenario summary output * Enhance HopScalingModule: add detailed documentation for functions, improve clarity of jitter and sampling logic, and reset functionality in tests * Enhance HopScalingModule: add evictionEstimate parameter to estimateScaleFactor and update related logging for improved mesh size estimation * Enhance HopScalingModule: adjust effective rolls calculation for improved accuracy, add eviction estimate logic, responding to all copilot review points * Implement CompactHistogram for parallel hop scaling sampling - Added CompactHistogram class to track node hop distances with bitwise sampling. - Integrated CompactHistogram into HopScalingModule for independent packet sampling. - Updated NodeDB to feed both the hop scaling module and the new histogram sampler. - Enhanced HopScalingModule with methods to sample packets for the histogram and retrieve hop distribution statistics. - Implemented tests for CompactHistogram functionality, including sampling, window rolling, and adaptive denominator scaling. - Updated existing tests to validate the integration of the new histogram sampling mechanism. * Enhance CompactHistogram and HopScalingModule: add per-hop distribution functionality, improve time handling for unit tests, and refine test setup for deterministic behavior * CompactHistogram: add mesh size estimation, improve entry replacement logic, and update logging for per-hop distribution * Refactor HopScalingModule and CompactHistogram integration - Removed the suggestedHopFromCompactHistogram function to streamline hop suggestion logic. - Updated HopScalingModule to directly utilize CompactHistogram's internal methods for hop suggestions and sampling. - Enhanced logging in HopScalingModule to provide detailed histogram statistics. - Modified test cases to ensure comprehensive coverage of new histogram behaviors and sampling logic. - Improved node ID distribution in tests to better exercise sampling mechanisms. - Ensured that filtering denominators are held for 12 hours before dropping, enhancing stability in sampling. * Refactor CompactHistogram to support 13-hour activity tracking and introduce politeness regimes - Updated the bitfield structure to accommodate 13-hour seen tracking. - Changed the logic in rollHour() to analyze activity over the last 0-2 hours vs. 1-3 hours for politeness factor calculation. - Introduced three politeness levels: GENEROUS, DEFAULT, and STRICT based on recent activity ratios. - Adjusted filtering and sampling logic to reflect the new 13-hour tracking period. - Updated unit tests to validate new behavior and ensure proper functionality of politeness regimes. * Enhance CompactHistogram and HopScalingModule for improved sampling and decision-making - Introduced a session-specific hash seed in CompactHistogram to reduce bias in node ID sampling. - Updated sampling logic to use hashed node IDs instead of raw IDs for filtering and entry management. - Added histogram rollover tracking in HopScalingModule to ensure proper decision-making after initial data collection. - Adjusted logging to reflect the active state of the histogram and its comparison with NodeDB advisory hops. - Enhanced unit tests to validate new sampling logic and memory layout changes. * Expose hashNodeId for testing in CompactHistogram * Add mesh trend statistics to CompactHistogram for enhanced activity tracking * Implement histogram state persistence in CompactHistogram with save and load functions * Refactor CompactHistogram to improve entry management and enhance rollHour logging * feat: add HopScalingModule for adaptive hop limit recommendations Introduces HopScalingModule, a sampled hop-distance histogram that recommends the minimum hop limit needed to reach ~40 nodes, and automatically reducing the hops as the mesh grows. Key design: - 512-byte packed histogram (128 × 4-byte Record entries) embedded in a new HopScalingModule. - Each Record: 16-bit node hash, 3-bit hop distance, 13-bit seen bitmap - Sampling filter: only nodes where (hash & (denom-1)) == 0 are kept; denominator doubles on overflow and halves when utilisation is low - Hourly rollHour(): tallies per-hop counts, walks scaled buckets to find the minimum hop satisfying TARGET_AFFECTED_NODES (40), applies a politeness extension based on recent/older activity ratio, shifts all seen bitmaps, and persists state to /prefs/hopScalingState.bin - Hop recommendation gated by bootstrap (requires >=1 rollHour before overriding HOP_MAX) - NodeDB calls samplePacketForHistogram() on every non-MQTT rx packet - Module also estimates total mesh size and logs useful information about mesh characteristics. Changes: - src/modules/HopScalingModule.h/.cpp: new module - src/mesh/NodeDB.cpp: wire up samplePacketForHistogram - src/mesh/Router.cpp: consume getLastRequiredHop() - test/test_hop_scaling/: 12-test suite covering all mesh topologies and anticipated operational requirements * test: increase run iterations in sparse to dense transition test * feat: refactor HopScalingModule to use RUNS_PER_HOUR constant and improve logging * feat: enhance HopScalingModule with filtering denominator management and add tests for state transitions * refactor: remove CompactHistogram module and related files * address copilot review comments * Tweak: packet sampling only lora * ove role-based hop floor logic and related definitions into the module - keep it in one place. * Refactor MockNodeDB to use nodeInfoLiteSetBit for MQTT flag setting * Refactor hop scaling parameters and logic to integer maths and put default values in defaults.h. Small flash size reduction, no functional impact. * Update unit test preprocessor directives to PIO_UNIT_TESTING for consistency * refactor: improve test output organization and clarity in hop scaling tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Meshtastic MCP Server
An MCP server for working with the Meshtastic firmware repo and connected devices. Lets Claude Code / Claude Desktop:
- Discover USB-connected Meshtastic devices
- Enumerate PlatformIO board variants (166+) with Meshtastic metadata
- Build, clean, flash, erase-and-flash (factory), and OTA-update firmware
- Read serial logs via
pio device monitor(with board-specific exception decoders) - Trigger 1200bps touch-reset for bootloader entry (nRF52, ESP32-S3, RP2040)
- Query and administer a running node via the
meshtasticPython API: owner name, config (LocalConfig + ModuleConfig), channels, messaging, reboot/shutdown/factory-reset - Call
esptool,nrfutil,picotooldirectly when PlatformIO doesn't cover the operation
Design principle
PlatformIO first. Its pio run -t upload knows the correct protocol, offsets, and post-build chain for every variant in variants/. Direct vendor-tool wrappers (esptool_*, nrfutil_*, picotool_*) exist as escape hatches for operations pio doesn't cover (blank-chip erase, DFU .zip packages, BOOTSEL-mode inspection).
Prerequisites
- Python ≥ 3.11
- PlatformIO Core —
pioon$PATHor at~/.platformio/penv/bin/pio - The Meshtastic firmware repo checked out somewhere (set via
MESHTASTIC_FIRMWARE_ROOT) - Optional:
esptool,nrfutil,picotoolon$PATH(or under the firmware venv at.venv/bin/) if you want to use the direct-tool wrappers
Install
cd <firmware-repo>/mcp-server
python3 -m venv .venv
.venv/bin/pip install -e .
Verify:
MESHTASTIC_FIRMWARE_ROOT=<firmware-repo> .venv/bin/python -m meshtastic_mcp
The server blocks on stdin (that's correct — it speaks MCP over stdio). Ctrl-C to exit.
Register with Claude Code
Edit ~/.claude/settings.json (global) or <firmware-repo>/.claude/settings.local.json (project-only):
{
"mcpServers": {
"meshtastic": {
"command": "<firmware-repo>/mcp-server/.venv/bin/python",
"args": ["-m", "meshtastic_mcp"],
"env": {
"MESHTASTIC_FIRMWARE_ROOT": "<firmware-repo>"
}
}
}
}
Replace <firmware-repo> with the absolute path, e.g. /Users/you/GitHub/firmware. Restart Claude Code after editing.
Register with Claude Desktop
Same mcpServers block, but in ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows).
Tools (43)
Discovery & metadata
| Tool | What it does |
|---|---|
list_devices |
USB/serial port listing, flags likely-Meshtastic candidates |
list_boards |
PlatformIO envs with custom_meshtastic_* metadata; filters by arch/supported/query/level |
get_board |
Full env dict incl. raw pio config |
Build & flash
| Tool | What it does |
|---|---|
build |
pio run -e <env> (+ mtjson target) |
clean |
pio run -e <env> -t clean |
pio_flash |
pio run -e <env> -t upload --upload-port <port> — any architecture |
erase_and_flash |
ESP32 full factory flash via bin/device-install.sh |
update_flash |
ESP32 OTA app-partition update via bin/device-update.sh |
touch_1200bps |
1200-baud open/close to trigger USB CDC bootloader entry |
Serial log sessions
Backed by long-running pio device monitor subprocesses with a 10k-line ring buffer per session and board-specific filters (esp32_exception_decoder auto-selected when you pass env=).
| Tool | What it does |
|---|---|
serial_open |
Start a monitor session; returns session_id |
serial_read |
Cursor-based pull; reports dropped if lines aged out of the ring |
serial_list |
All active sessions |
serial_close |
Terminate a session |
Device reads
| Tool | What it does |
|---|---|
device_info |
my_node_num, long/short name, firmware version, region, channel, node count |
list_nodes |
Full node database with position, SNR, RSSI, last_heard, battery |
The tool tables below document 38 currently registered MCP server tools.
Device writes
| Tool | What it does |
|---|---|
set_owner |
Long name + optional short name (≤4 chars) |
get_config |
One section or all (LocalConfig + ModuleConfig) |
set_config |
Dot-path field write: lora.region="US", device.role="ROUTER", etc. |
get_channel_url |
Primary-only or include_all=admin URL |
set_channel_url |
Import channels from a Meshtastic URL |
set_debug_log_api |
Enable or disable debug logging for the Meshtastic Python API client |
send_text |
Broadcast or direct text message |
reboot |
localNode.reboot(secs) — requires confirm=True |
shutdown |
localNode.shutdown(secs) — requires confirm=True |
factory_reset |
localNode.factoryReset(full?) — requires confirm=True |
Direct hardware tools (escape hatches)
| Tool | What it does |
|---|---|
esptool_chip_info |
Read chip, MAC, crystal, flash size |
esptool_erase_flash |
Full-chip erase (destructive) |
esptool_raw |
Pass-through; confirm=True required for write/erase/merge |
nrfutil_dfu |
DFU-flash a .zip package |
nrfutil_raw |
Pass-through |
picotool_info |
Read Pico BOOTSEL-mode info |
picotool_load |
Load a UF2 |
picotool_raw |
Pass-through |
USB power control (uhubctl)
| Tool | What it does |
|---|---|
uhubctl_list |
Enumerate USB hubs + attached-device VID/PID (read-only) |
uhubctl_power |
Drive a hub port on or off; off requires confirm=True |
uhubctl_cycle |
Off → wait delay_s → on; confirm=True required |
Target a port by explicit (location, port) (raw uhubctl syntax like
location="1-1.3", port=2) or by role ("nrf52", "esp32s3"). Role
lookup checks MESHTASTIC_UHUBCTL_LOCATION_<ROLE> +
MESHTASTIC_UHUBCTL_PORT_<ROLE> env vars first, then auto-detects via VID
against uhubctl's output.
Requires uhubctl on PATH:
brew install uhubctl # macOS
apt install uhubctl # Debian/Ubuntu
Modern macOS + PPPS-capable hubs generally work without root. On Linux
without udev rules, or on old macOS with driver quirks, you may need
sudo. If uhubctl returns a permission error the MCP tool raises a
clear UhubctlError pointing at the
udev-rules / sudo fallback
rather than auto-sudo'ing mid-run.
Safety
- All destructive flash/admin tools require
confirm=Trueas a tool-level gate, on top of any permission prompt from Claude. - Serial port is exclusive. If a
serial_*session is active on a port,device_info/admin tools on the same port will fail fast with a pointer at the activesession_id. Close the session first. - Flash confirmation by architecture:
erase_and_flash/update_flasherror if the env's architecture isn't ESP32 — usepio_flashfor nRF52/RP2040/STM32.
Environment variables
| Var | Default | Purpose |
|---|---|---|
MESHTASTIC_FIRMWARE_ROOT |
walks up from cwd for platformio.ini |
Pin the firmware repo |
MESHTASTIC_PIO_BIN |
~/.platformio/penv/bin/pio → $PATH pio → platformio |
Override pio location |
MESHTASTIC_ESPTOOL_BIN |
<firmware>/.venv/bin/esptool → $PATH |
Override esptool |
MESHTASTIC_NRFUTIL_BIN |
$PATH |
Override nrfutil |
MESHTASTIC_PICOTOOL_BIN |
$PATH |
Override picotool |
MESHTASTIC_MCP_SEED |
mcp-<user>-<host> |
PSK seed for test-harness session (CI override) |
MESHTASTIC_MCP_FLASH_LOG |
<mcp-server>/tests/flash.log |
Tee target for pio/esptool/nrfutil subprocess output (TUI tails it) |
MESHTASTIC_MCP_TCP_HOST |
unset | host or host:port of a meshtasticd daemon to surface as a TCP device (see "TCP / native-host nodes" below) |
TCP / native-host nodes
The native-macos and native PlatformIO envs build a headless meshtasticd
binary that runs on the host (Apple Silicon / Intel macOS, or Linux Portduino).
The daemon exposes the meshtastic TCP API on port 4403 rather than a USB
serial endpoint — point the MCP server at it via MESHTASTIC_MCP_TCP_HOST:
# 1. Build + run a daemon on this host (see variants/native/portduino/platformio.ini
# for full Homebrew prereqs and CH341 LoRa-adapter setup).
pio run -e native-macos
~/.meshtasticd/meshtasticd
# 2. Point the MCP server at it.
export MESHTASTIC_MCP_TCP_HOST=localhost # or host:port, default port 4403
First-run gotcha — MAC address. meshtasticd derives its MAC from the
USB adapter's serial-number / product strings. Many cheap CH341 dongles
(MeshStick included — VID 0x1A86 / PID 0x5512) ship with iSerialNumber=0
and iProduct=0, so the daemon aborts on boot with *** Blank MAC Address not allowed!. Set the MAC explicitly in config.yaml:
# Under General:
MACAddress: 02:CA:FE:BA:BE:01
Use a locally-administered address (first byte's second-LSB set, e.g.
02:* / 06:* / 0A:* / 0E:*) to avoid colliding with a real OUI.
There is also a --hwid AA:BB:CC:DD:EE:FF CLI flag visible in
meshtasticd --help, but it is currently broken in
MAC_from_string() (src/platform/portduino/PortduinoGlue.cpp): the
function strips colons from its parameter but then reads bytes from the
global portduino_config.mac_address, so --hwid is silently overridden
when MACAddress: is also set, and crashes the daemon (uncaught
std::invalid_argument: stoi: no conversion) when it isn't. Use the YAML
form until that's fixed upstream.
list_devices will surface the daemon as tcp://localhost:4403 with
likely_meshtastic=True, so device_info, list_nodes, get_config,
set_config, set_owner, send_text, userprefs_*, and the admin RPCs
auto-select it when no port is passed. Pass port="tcp://other-host:9999"
explicitly to target a different daemon.
Tools that don't apply to a TCP/native node (no USB hardware to operate
on) raise a clear ConnectionError rather than failing mysteriously:
pio_flash, erase_and_flash, update_flash, touch_1200bps,
serial_open (use info/admin tools directly), and the vendor escape hatches
esptool_*, nrfutil_*, picotool_*. pio_flash against a native* env
similarly raises — there's no upload step; use build and run the binary
directly.
The pytest harness in tests/ still assumes USB-attached devices per role —
TCP-aware fixtures are not part of this surface yet.
Hardware Test Suite
mcp-server/tests/ holds a pytest-based integration suite that exercises
real USB-connected Meshtastic devices against the MCP server surface. Separate
from the native C++ unit tests in the firmware repo's top-level test/
directory — this one validates the device-facing behavior end-to-end.
Invocation
./mcp-server/run-tests.sh # full suite (auto-detect + auto-bake-if-needed)
./mcp-server/run-tests.sh --force-bake # reflash devices before testing
./mcp-server/run-tests.sh --assume-baked # skip the bake step (caller vouches for state)
./mcp-server/run-tests.sh tests/mesh # one tier
./mcp-server/run-tests.sh tests/mesh/test_traceroute.py # one file
./mcp-server/run-tests.sh -k telemetry # pytest name filter
The wrapper auto-detects connected devices (VID 0x239A → nrf52 → env
rak4631; 0x303A or 0x10C4 → esp32s3 → env heltec-v3), exports
MESHTASTIC_MCP_ENV_<ROLE> env vars, and invokes pytest. Overrides via
per-role env vars: MESHTASTIC_MCP_ENV_NRF52=heltec-mesh-node-t114 ./run-tests.sh.
No hardware connected? The wrapper narrows to tests/unit/ only and says so
in the pre-flight header.
Tiers (run in this order)
bake(tests/test_00_bake.py) — flashes both hub roles with the session's test profile. Has a skip-if-already-baked check (region + channel match);--force-bakeoverrides.unit— pure Python, no hardware. boards / PIO wrapper / userPrefs-parse / testing-profile fixtures.mesh— 2-device mesh: formation, broadcast delivery, direct+ACK, traceroute, bidirectional. Parametrized over both directions. Includestest_peer_offline_recoverywhich uses uhubctl to power-cycle one peer mid-conversation and verifies the mesh recovers (skips without uhubctl).telemetry— periodic telemetry broadcast + on-demand request/reply (TELEMETRY_APPwithwantResponse=True).monitor— boot log has no panic markers within 60 s of reboot.recovery—uhubctlpower-cycle round-trip: verifies the hub port can be toggled off/on, the device re-enumerates with the samemy_node_num, and NVS-resident config (region, channel, modem preset) survives a hard reset. Requiresuhubctlon PATH; skips cleanly otherwise.ui— input-broker-driven screen navigation (AdminMessage.send_input_eventinjection →Screen::handleInputEvent→ frame transition). Parametrized on the screen-bearing role (heltec-v3 OLED). Captures images via USB webcam + OCRs them for HTML-report evidence. Requirespip install -e '.[ui]'andMESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=<index>; tier is auto-deselected ifcv2isn't importable.fleet— PSK-seed isolation: two labs with different seeds never overlap.admin— owner persistence across reboot, channel URL round-trip,lora.hop_limitpersistence.provisioning— region/channel baking, userPrefs survivefactory_reset(full=False).
UI tier setup
The tests/ui/ tier drives the on-device OLED via the firmware's existing
AdminMessage.send_input_event RPC (no firmware changes required) and
verifies transitions via a macro-gated log line + camera + OCR. Summary:
- Install extras:
pip install -e 'mcp-server/.[ui]'— pulls inopencv-python-headless,numpy,easyocr,Pillow. First easyocr run downloads ~100 MB of models to~/.EasyOCR/; an autouse session fixture pre-warms the reader so per-test OCR is <100 ms after that. - Point a USB webcam at the heltec-v3 OLED. Discover its index:
.venv/bin/python -c "import cv2; [print(i, cv2.VideoCapture(i).read()[0]) for i in range(5)]" - Export the per-role device env var:
export MESHTASTIC_UI_CAMERA_DEVICE_ESP32S3=0 - Run:
Captures land under
./run-tests.sh tests/ui -vtests/ui_captures/<session_seed>/<test_id>/, one PNG +.ocr.txtperframe_capture()call, with a per-testtranscript.mdstepping through event → frame → OCR. The HTML report embeds the full image strip inline (pass or fail).
On macOS, cv2.VideoCapture(0) triggers the TCC Camera permission prompt
on first use. Pre-grant Terminal (or your IDE's terminal) before running.
The OpenCVBackend fails fast on 10 consecutive black frames so a silent
permission denial surfaces as a clear error, not an empty PNG strip.
No camera? Set MESHTASTIC_UI_CAMERA_BACKEND=null (or leave the device var
unset). Tests still exercise the event-injection path and log assertions;
captures just become 1×1 black PNGs.
Artifacts (regenerated every run, under tests/)
report.html— self-contained pytest-html report. Each test gets a Meshtastic debug section attached on failure with a 200-line firmware log tail + device-state dump. Open this first on failures.junit.xml— CI-parseable.reportlog.jsonl—pytest-reportlogevent stream; consumed by the TUI.fwlog.jsonl— firmware log mirror (meshtastic.log.linepubsub → JSONL).flash.log— tee of all pio / esptool / nrfutil / picotool subprocess output during the run (driven byMESHTASTIC_MCP_FLASH_LOG).
Live TUI
.venv/bin/meshtastic-mcp-test-tui
.venv/bin/meshtastic-mcp-test-tui tests/mesh # pytest args pass through
Textual-based wrapper over run-tests.sh with a live test tree, tier
counters, pytest output pane, firmware-log pane, and a device-status strip.
Key bindings: r re-run focused, f filter, d failure detail, g open
report.html, x export reproducer bundle, l cycle fw-log filter, q
quit (SIGINT → SIGTERM → SIGKILL escalation).
Set MESHTASTIC_UI_TUI_CAMERA=1 to mount a bottom-of-screen UI camera
panel. Left side: the latest capture PNG rendered as Unicode half-blocks
(via rich-pixels, works in any terminal — no kitty/sixel required).
Right side: live transcript tail ("step 3 — frame 4/8 name=nodelist_nodes
— OCR: Nodes 2/2") so you can see every event-injection and its result
as each UI test runs. Requires the [ui] extras for image rendering; the
transcript alone works without them.
Slash commands
Three AI-assisted workflows are wired up for Claude Code operators
(.claude/commands/) and Copilot operators (.github/prompts/):
/test (run + interpret), /diagnose (read-only health report), /repro
(flake triage, N-times re-run with log diff).
House rules (for human + agent contributors)
- Session-scoped fixtures in
tests/conftest.pysnapshot + restoreuserPrefs.jsonc; never edituserPrefs.jsoncfrom inside a test. Use thetest_profile/no_region_profilefixtures for ephemeral overrides. SerialInterfaceholds an exclusive port lock; sequence calls open → mutate → close, then next device. No parallel calls to the same port.- Directed PKI-encrypted sends need bilateral NodeInfo warmup —
both sides must hold the other's current pubkey. See
tests/mesh/_receive.py::nudge_nodeinfo_portand the three directed- send tests (test_direct_with_ack,test_traceroute,test_telemetry_request_reply) for the canonical pattern.
Layout
mcp-server/
├── pyproject.toml
├── README.md
└── src/meshtastic_mcp/
├── __main__.py # entry: python -m meshtastic_mcp
├── server.py # FastMCP app + @app.tool() registrations (thin)
├── config.py # firmware_root, pio_bin, esptool_bin, etc.
├── pio.py # subprocess wrapper (timeouts, JSON, tail_lines)
├── devices.py # list_devices (findPorts + comports)
├── boards.py # list_boards / get_board (pio project config parse + cache)
├── flash.py # build, clean, flash, erase_and_flash, update_flash, touch_1200bps
├── serial_session.py # SerialSession + reader thread + ring buffer
├── registry.py # session registry + per-port locks
├── connection.py # connect(port) ctx mgr — SerialInterface + port lock
├── info.py # device_info, list_nodes
├── admin.py # set_owner, get/set_config, channels, send_text, reboot/shutdown/factory_reset
└── hw_tools.py # esptool / nrfutil / picotool wrappers
Troubleshooting
- "Could not locate Meshtastic firmware root" — set
MESHTASTIC_FIRMWARE_ROOT. - "Could not find
pio" — install PlatformIO or setMESHTASTIC_PIO_BIN. - "Port is held by serial session ..." — call
serial_close(session_id)orserial_listto find it. factory.binnot found after build — the env may not be ESP32; only ESP32 envs produce a.factory.bin.touch_1200bpsreportednew_port: null— the device may not have 1200bps-reset stdio, or the bootloader re-uses the same port name. Checklist_devicesmanually.