trackmcp
Back to directory
navado

esp32mcpserver

View on GitHub

Allow AI models connect to ESP32 exposed interfaces. AI generated MCP server for ESP32.

25 stars C++AI & Machine Learning Updated Oct 3, 2025

Documentation

ESP32 MCP Server

Build & Test

A Model Context Protocol (MCP) server running on ESP32 (and Nordic nRF52840 in sensor-only mode), exposing real-world sensor data and bus protocols to AI assistants and automation tooling over WebSocket / JSON-RPC 2.0.

Connect an AI agent directly to your hardware: query I2C sensors, parse NMEA 0183 GPS/marine data, decode NMEA 2000 and OBD-II CAN frames, replay bus history — all through a standard MCP interface.

architecture

Features

CapabilityDetails
MCP ProtocolJSON-RPC 2.0 over WebSocket, resource discovery & subscriptions
I2C SensorsAuto-scan + drivers for BME280, MPU6050, ADS1115, SHT31, BH1750, INA219
NMEA 0183GGA, RMC, VTG, GSV, MWV, DBT, DPT, HDG, HDT with XOR checksum validation
NMEA 2000 / CAN29-bit PGNs: position, heading, speed, depth, wind, attitude, water temp
OBD-II / CANStandard 11-bit OBD-II service 01/09 PID decoding (60+ PIDs)
Bus HistoryPersistent ring buffers for CAN, NMEA, NMEA 2000, OBD-II — queryable via MCP
Bus ReadersTime-bounded serial and CAN reads, raw or parsed mode, JSON output
WiFiAP setup UI + STA mode; credentials stored in NVS
DiscoverymDNS (`_mcp._tcp`) + UDP capability broadcast
MetricsHeap, uptime, RSSI, histogram stats with boot persistence
Multi-boardESP32-S3, ESP32-C3, ESP32, Adafruit HUZZAH32, M5Stack, Nordic nRF52840-DK, Adafruit Feather nRF52840
TestingNative unit tests (Unity), mock I2C / CAN / Serial / LittleFS

Supported Boards

ESP32 Boards — Full MCP Server (WiFi + WebSocket + CAN)

BoardPlatformIO envMCUFlashRAMWiFiBLECANRGB LED
ESP32-S3-DevKitC-1`esp32-s3-devkitc-1`Xtensa LX7 × 2 @ 240 MHz8 MB512 KB + 8 MB PSRAM5.0GPIO48
ESP32-DevKitC V4`esp32-devkitc`Xtensa LX6 × 2 @ 240 MHz4 MB520 KB4.2
ESP32-C3-DevKitM-1`esp32-c3-devkitm-1`RISC-V @ 160 MHz4 MB400 KB5.0GPIO8
Adafruit HUZZAH32`adafruit-huzzah32`Xtensa LX6 × 2 @ 240 MHz4 MB520 KB4.2
M5Stack Core ESP32`m5stack-core`Xtensa LX6 × 2 @ 240 MHz16 MB520 KB4.2

nRF52840 Boards — Sensor Only (I2C + Serial, no WiFi)

BoardPlatformIO envMCUFlashRAMBLE
Nordic nRF52840-DK`nrf52840-dk`Cortex-M4F @ 64 MHz1 MB256 KB5.0 + 802.15.4
Adafruit Feather nRF52840`nrf52840-feather`Cortex-M4F @ 64 MHz1 MB256 KB5.0

> nRF52840 targets run a separate `main_nrf.cpp` entry point: sensors + I2C + Serial output only. No WiFi, no MCP WebSocket, no LittleFS, no bus history. For full MCP-over-WiFi, bridge via an ESP32-C3 in AT-command mode connected to UART1.

Board Feature Matrix

FeatureESP32-S3ESP32 V4ESP32-C3HUZZAH32M5StacknRF52840-DKnRF52840-Feather
WiFi
BLE
CAN / TWAI
I2C
UART (ext)
LittleFS
MCP Server
Bus History
mDNS / UDP discovery

Architecture

mermaid
flowchart TD
    Client[AI Agent / MCP Client] -->|WebSocket :9000 JSON-RPC 2.0| MCPServer

    subgraph ESP32
        MCPServer -->|resources/list| Resources
        MCPServer -->|sensors/i2c/scan| SensorMgr
        MCPServer -->|sensors/read| SensorMgr
        MCPServer -->|buses/serial/read| BusReader
        MCPServer -->|buses/can/read| BusReader
        MCPServer -->|bus/history/read| BusHistory
        MCPServer -->|bus/history/config/*| BusHistory

        SensorMgr -->|Wire I2C| I2CSensors[BME280 · MPU6050 · ADS1115\nSHT31 · BH1750 · INA219]
        BusReader -->|UART| NMEAParser[NMEA 0183 Parser]
        BusReader -->|CAN TWAI| CANParser[NMEA 2000 / OBD-II Parser]
        BusReader -->|feed| BusHistory[Bus History\nRing Buffers]
        BusHistory --> LittleFS

        NetworkMgr[Network Manager] --> WiFi
        NetworkMgr --> WebSetup[Setup UI :80]
        DiscoveryMgr[Discovery Manager] -->|mDNS + UDP| Network
        Metrics[Metrics System] --> LittleFS
        uLogger --> LittleFS
    end

    MCPServer --> Metrics
    MCPServer --> uLogger

Hardware

Pin Assignments (ESP32-S3-DevKitC-1)

GPIOFunctionConnected to
IO8I2C SDAAll I2C sensors
IO9I2C SCLAll I2C sensors
IO16UART1 RXGPS / chart plotter TX
IO17UART1 TXGPS / chart plotter RX (optional)
IO4CAN RXTJA1050 / SN65HVD230 CRXD
IO5CAN TXTJA1050 / SN65HVD230 CTXD

nRF52840-DK Pin Assignments

PinFunction
P0.26 / D6I2C SDA
P0.27 / D7I2C SCL
P0.06 / TXUART1 TX
P0.08 / RXUART1 RX

Bill of Materials

#PartPurpose
1ESP32-S3-DevKitC-1Main MCU
1BME280 breakoutTemperature / Humidity / Pressure
1MPU6050 breakout6-axis IMU (optional)
1TJA1050 or SN65HVD230CAN bus transceiver (optional)
1GPS module (3.3 V UART)NMEA 0183 position/time (optional)
24.7 kΩ resistorsI2C pull-ups
Breadboard + jumpers

ESP32-S3-DevKitC-1 Wiring

code
┌──────────────────────────────────────┐
                    │         ESP32-S3-DevKitC-1            │
                    │                                       │
             3V3 ───┤ 3V3                          GND ├─── GND
             GND ───┤ GND                          IO0 ├
                    │                                   │
  I2C SDA ── IO8 ───┤ IO8  ┌──────────┐       IO17 ├─── UART1 TX → GPS
  I2C SCL ── IO9 ───┤ IO9  │  ESP32   │       IO16 ├─── UART1 RX ← GPS
                    │ IO10 │    S3    │       IO15 ├
                    │ IO11 └──────────┘       IO14 ├
                    │ IO12                    IO13 ├
                    │ IO3                      IO4 ├─── CAN RX ← TJA1050
                    │ IO46                     IO5 ├─── CAN TX → TJA1050
                    │                               │
                    │    USB (flash / serial log)   │
                    └───────────────────────────────┘

I2C Sensor Wiring

All sensors share one bus. Add 4.7 kΩ pull-ups from SDA and SCL to 3V3.

code
ESP32-S3      BME280       MPU6050      ADS1115      INA219
─────────     ───────      ───────      ───────      ───────
IO8 (SDA)─────SDA──────────SDA──────────SDA──────────SDA
IO9 (SCL)─────SCL──────────SCL──────────SCL──────────SCL
3V3  ──────────VCC──────────VCC──────────VCC──────────VCC
GND  ──────────GND──────────GND──────────GND──────────GND
               SDO→GND      AD0→GND      ADDR→GND
               (0x76)       (0x68)       (0x48)       (0x40)

Other supported sensors (same bus wiring): SHT31 (0x44), BH1750 (0x23).

NMEA 0183 Wiring (GPS / Chart Plotter)

code
GPS / chart plotter              ESP32-S3
───────────────────              ────────
TX  (3.3 V TTL NMEA out) ─────── IO16 (UART1 RX)
RX  (optional NMEA in)   ─────── IO17 (UART1 TX)
GND                      ─────── GND
VCC (3.3 V)              ─────── 3V3

> If the device outputs RS-232 levels, use a MAX3232 level shifter. Never connect RS-232 directly to ESP32 GPIO.

CAN Bus Wiring (NMEA 2000 / OBD-II)

code
TJA1050 / SN65HVD230             ESP32-S3
────────────────────             ────────
CTXD                  ─────────── IO5 (CAN TX)
CRXD                  ─────────── IO4 (CAN RX)
VCC (5 V for TJA1050) ─────────── VIN / external 5 V
GND                   ─────────── GND
CANH ──────────────── NMEA 2000 backbone / OBD-II CANH
CANL ──────────────── NMEA 2000 backbone / OBD-II CANL

> NMEA 2000 runs at 250 kbps. OBD-II runs at 500 kbps. Set the baud rate in your `CANBusReader` configuration accordingly.


Software Prerequisites

bash
pip install platformio

Build & Flash

bash
# 1. Clone
git clone https://github.com/navado/ESP32MCPServer.git
cd ESP32MCPServer

# 2. Install library dependencies
pio pkg install

# 3. Upload web assets to LittleFS (optional but recommended)
pio run -t uploadfs -e esp32-s3-devkitc-1

# 4. Build firmware
pio run -e esp32-s3-devkitc-1

# 5. Flash
pio run -t upload -e esp32-s3-devkitc-1

# 6. Monitor
pio device monitor --baud 115200

nRF52840

bash
pio run -e nrf52840-dk
pio run -t upload -e nrf52840-dk

First Run

1. Power on the board. It broadcasts a WiFi AP: `ESP32_XXXXXX` (last 6 hex digits of the MAC address).

2. Connect to that AP and open `http://192.168.4.1`.

3. Enter your WiFi credentials and click Save.

4. The board joins your network and prints its IP address on the serial console.

5. Connect an MCP client to `ws://:9000`.


Web Dashboard

A browser-based dashboard is served from flash at `http:///app`.

TabWhat it does
SensorsScan I²C bus, display detected devices, read values, auto-refresh
MetricsList all metrics, live values, sparkline history chart
LogsQuery and clear metric log entries
MCP ConsoleInteractive JSON-RPC console with preset requests and live message log

HTTP Endpoints (port 80)

MethodPathDescription
GET`/`WiFi setup page
POST`/save`Save WiFi credentials (`ssid`, `password` form params)
GET`/status`Network status JSON
WS`/ws`Real-time network status WebSocket
GET`/app`Browser dashboard
GET`/bus-history`Read bus history config JSON
POST`/bus-history`Update bus history config JSON

MCP API Reference (WebSocket port 9000)

All methods use JSON-RPC 2.0 over WebSocket.

Protocol

MethodParamsDescription
`initialize`Server info, protocol version, capabilities
`resources/list`List registered MCP resources
`resources/read``uri`Read a resource by URI
`resources/subscribe``uri`Subscribe to push updates for a resource
`resources/unsubscribe``uri`Cancel a subscription

Sensors

MethodParamsDescription
`sensors/i2c/scan`Scan I²C bus; auto-detect and initialise drivers
`sensors/read``sensorId?`Read all channels of one sensor (omit ID to read all)

`sensors/i2c/scan`

json
{"jsonrpc":"2.0","method":"sensors/i2c/scan","id":1}
json
{
  "jsonrpc":"2.0","id":1,
  "result":{"devices":[
    {"address":118,"name":"BME280","supported":true,"parameters":["temperature","pressure","humidity"]},
    {"address":104,"name":"MPU6050","supported":true,"parameters":["accel_x","accel_y","accel_z","gyro_x","gyro_y","gyro_z","temperature"]}
  ]}
}

`sensors/read`

json
{"jsonrpc":"2.0","method":"sensors/read","id":2,"params":{"sensorId":"bme280_0x76"}}
json
{
  "jsonrpc":"2.0","id":2,
  "result":{"readings":[
    {"sensorId":"bme280_0x76","parameter":"temperature","value":22.4,"unit":"°C","timestamp":1234567890,"valid":true},
    {"sensorId":"bme280_0x76","parameter":"humidity","value":58.1,"unit":"%RH","timestamp":1234567890,"valid":true},
    {"sensorId":"bme280_0x76","parameter":"pressure","value":101320.0,"unit":"Pa","timestamp":1234567890,"valid":true}
  ]}
}

Bus History

Bus history accumulates CAN frames, NMEA lines, decoded NMEA 2000 messages, and OBD-II records in circular RAM buffers. The buffers are sized from a configurable RAM budget and their configuration is persisted to LittleFS.

MethodParamsDescription
`bus/history/read``type`, `limit?`Read accumulated messages from history buffers
`bus/history/config/get`Read current buffer sizing configuration
`bus/history/config/set`config fieldsResize buffers and persist configuration

`bus/history/read`

`type` must be one of: `can`, `nmea`, `nmea2000`, `obdii`. `limit` defaults to all stored entries.

json
{"jsonrpc":"2.0","method":"bus/history/read","id":3,"params":{"type":"can","limit":10}}
json
{
  "jsonrpc":"2.0","id":3,
  "result":{
    "type":"can",
    "frames":["7E8#0441050300000000","7E8#04410D0000000000"],
    "count":2,
    "capacity":500
  }
}
json
{"jsonrpc":"2.0","method":"bus/history/read","id":4,"params":{"type":"nmea2000"}}
json
{
  "jsonrpc":"2.0","id":4,
  "result":{
    "type":"nmea2000",
    "messages":[
      {"pgn":129025,"priority":2,"source":0,"name":"Position Rapid Update",
       "valid":true,"hasPosition":true,"latitude":47.6097,"longitude":-122.3331}
    ],
    "count":1,"capacity":200
  }
}
json
{"jsonrpc":"2.0","method":"bus/history/read","id":5,"params":{"type":"obdii"}}
json
{
  "jsonrpc":"2.0","id":5,
  "result":{
    "type":"obdii",
    "records":[
      {"service":1,"pid":12,"name":"Engine RPM","value":1500,"unit":"rpm","valid":true},
      {"service":1,"pid":13,"name":"Vehicle Speed","value":65,"unit":"km/h","valid":true}
    ],
    "count":2,"capacity":300
  }
}

`bus/history/config/get`

json
{"jsonrpc":"2.0","method":"bus/history/config/get","id":6}
json
{
  "jsonrpc":"2.0","id":6,
  "result":{
    "canFrameCount":500,
    "nmeaLineCount":1000,
    "nmea2000Count":200,
    "obdiiCount":300,
    "ramBudgetBytes":1048576,
    "safetyMarginBytes":65536,
    "freeHeapBytes":2097152
  }
}

`bus/history/config/set`

All fields are optional; omitted fields retain their current values.

json
{
  "jsonrpc":"2.0","method":"bus/history/config/set","id":7,
  "params":{"canFrameCount":1000,"ramBudgetBytes":2097152}
}
json
{"jsonrpc":"2.0","id":7,"result":{"ok":true,"canFrameCount":1000,"nmeaLineCount":1000,"nmea2000Count":200,"obdiiCount":300,"ramBudgetBytes":2097152,"safetyMarginBytes":65536}}

> Set `canFrameCount`, `nmeaLineCount`, `nmea2000Count`, `obdiiCount` all to `0` to enable auto-sizing: the server splits `ramBudgetBytes` equally among the four buffer types based on the size of each record type.

Metrics

MethodParamsDescription
`metrics/list``category?`List all registered metrics (name, type, unit, category)
`metrics/get``name`Current value of one metric
`metrics/history``name`, `seconds?`Historical samples for one metric (default: last 300 s)

`metrics/list`

json
{"jsonrpc":"2.0","method":"metrics/list","id":8,"params":{"category":"system"}}
json
{
  "jsonrpc":"2.0","id":8,
  "result":{"metrics":[
    {"name":"system.heap.free","type":"gauge","description":"Free heap","unit":"bytes","category":"system"},
    {"name":"system.uptime","type":"counter","description":"Uptime","unit":"s","category":"system"},
    {"name":"system.wifi.rssi","type":"gauge","description":"WiFi signal strength","unit":"dBm","category":"system"}
  ]}
}

Metric types: `counter` (cumulative integer), `gauge` (current value), `histogram` (min/max/mean/count object).

`metrics/get`

json
{"jsonrpc":"2.0","method":"metrics/get","id":9,"params":{"name":"system.heap.free"}}
json
{
  "jsonrpc":"2.0","id":9,
  "result":{"name":"system.heap.free","type":"gauge","value":214032,"unit":"bytes","category":"system","timestamp":1234567890}
}

For histogram metrics, `value` is an object: `{"mean":…,"min":…,"max":…,"count":…}`.

`metrics/history`

json
{"jsonrpc":"2.0","method":"metrics/history","id":10,"params":{"name":"system.heap.free","seconds":60}}
json
{
  "jsonrpc":"2.0","id":10,
  "result":{"name":"system.heap.free","entries":[
    {"ts":1234567800,"val":214100},
    {"ts":1234567830,"val":214032}
  ]}
}

Logs

MethodParamsDescription
`logs/query``name?`, `seconds?`Query binary metric log (default: last 3600 s)
`logs/clear`Delete all log entries from LittleFS
json
{"jsonrpc":"2.0","method":"logs/query","id":11,"params":{"name":"system.heap.free","seconds":120}}
json
{
  "jsonrpc":"2.0","id":11,
  "result":{"name":"system.heap.free","count":4,"entries":[
    {"ts":1234567810,"val":214100},
    {"ts":1234567840,"val":214032}
  ]}
}
json
{"jsonrpc":"2.0","method":"logs/clear","id":12}
json
{"jsonrpc":"2.0","id":12,"result":{"ok":true}}

Subscriptions

json
{"jsonrpc":"2.0","method":"resources/subscribe","id":13,"params":{"uri":"sensor://i2c/bme280_0x76"}}

After subscribing the server pushes unsolicited `notifications/resources/updated` on every value change:

json
{"jsonrpc":"2.0","method":"notifications/resources/updated","params":{"uri":"sensor://i2c/bme280_0x76"}}

TypeScript client example

See `examples/client/client.ts` for a complete typed client.


I2C Sensor Reference

SensorAddressesParametersUnits
BME280 / BMP2800x76, 0x77`temperature`, `pressure`, `humidity`¹°C, Pa, %RH
MPU60500x68, 0x69`accel_x/y/z`, `gyro_x/y/z`, `temperature`g, °/s, °C
ADS11150x48–0x4B`ch0_V`, `ch1_V`, `ch2_V`, `ch3_V`V (±4.096 range)
SHT310x44, 0x45`temperature`, `humidity`°C, %RH
BH17500x23, 0x5C`light_lux`lux
INA2190x40–0x4F`bus_voltage_V`, `shunt_mV`, `current_mA`, `power_mW`V, mV, mA, mW

¹ `humidity` is not available on BMP280.


NMEA 0183 Sentences Supported

SentenceDescriptionFields Decoded
GGAGPS Fix Datatime, lat, lon, fix quality, satellites, HDOP, altitude, geoid separation
RMCRecommended Minimum Navigationtime, active, lat, lon, speed (kn & km/h), course, magnetic variation
VTGTrack Made Good / Ground Speedcourse (true & magnetic), speed (kn & km/h)
GSVSatellites in Viewtotal satellites, per-satellite PRN, elevation, azimuth, SNR
MWVWind Speed and Angleangle, relative/true, speed, unit (K/M/N)
DBTDepth Below Transducerdepth (feet / metres / fathoms)
DPTDepth of Waterdepth (metres)
HDGHeading – Deviation & Variationheading, deviation, variation (E/W)
HDTHeading – Trueheading (true north)

Parsing features: XOR checksum validation, DDMM.MMMM → signed decimal degrees, HHMMSS.SS time, talker ID extraction, graceful empty-field handling.


NMEA 2000 PGNs Supported

PGNNameKey Fields
127250Vessel Heading`heading`, `variation`
127257Attitude`pitch`, `roll`, `yaw`
128259Speed Through Water`speedKnots`
128267Water Depth`depthMetres`, `offset`
129025Position Rapid Update`latitude`, `longitude`
129026COG & SOG`courseOverGround`, `speedOverGround`
130306Wind Data`speedKnots`, `angle`, apparent/true flag
130310Water Temperature`tempCelsius`

OBD-II PIDs Supported (Service 01)

Selected PIDs decoded; full list in `src/CANParser.cpp`:

CategoryPIDs
EngineRPM, load, coolant temp, intake air temp, MAP, MAF, timing advance
FuelFuel pressure, trim (short/long), rail pressure, injection timing, fuel rate
EmissionsO2 sensor voltages, EGR, evap system, DPF pressure
MotionVehicle speed, throttle position, accelerator pedal position
TemperatureOil temp, intake air temp, EGR temp, engine exhaust gas temps (EGT)
ElectricalControl module voltage, battery voltage
HybridHybrid battery remaining life
TorqueDriver demand, actual torque, reference torque
Vehicle infoVIN, calibration IDs (service 09)

Discovery

On network connect the server registers two mDNS services and periodically sends a UDP capability beacon:

DiscoveryDetails
mDNS`_mcp._tcp` and `_http._tcp` with TXT records: version, capabilities, detected sensors
UDP broadcastJSON beacon to `255.255.255.255:5354` every 30 s (configurable); includes sensor manifest

mDNS and UDP are triggered automatically on network join, I2C scan completion, MCP server ready, and bus history config changes.


Testing

Tests compile and run natively on Linux — no hardware required.

bash
# All tests
pio test -e native

# Specific suites
pio test -e native -f test_nmea_parser
pio test -e native -f test_can_parser
pio test -e native -f test_i2c_sensors
pio test -e native -f test_bus_reader
pio test -e native -f test_mcp_server
pio test -e native -f test_metrics_system

# With coverage (requires lcov)
pio test -e native --coverage
SuiteWhat is covered
`test_nmea_parser`GGA, RMC, VTG, GSV, MWV, DBT, DPT, HDG, HDT, checksum validation
`test_can_parser`OBD-II PIDs, NMEA 2000 PGNs 127250/127257/128259/128267/129025/129026/130306/130310
`test_i2c_sensors`BME280, MPU6050, ADS1115, SHT31, BH1750, INA219 via mock I2C
`test_bus_reader`Timed reads, raw/parsed modes, timeout handling
`test_mcp_server`JSON-RPC dispatch, resource management, method registration
`test_metrics_system`Counters, gauges, histograms, LittleFS persistence

Extending with Custom MCP Methods

Register additional JSON-RPC handlers at runtime:

cpp
mcpServer.registerMethodHandler("myns/doSomething",
    [](uint8_t clientId, uint32_t reqId, const JsonObject& params) {
        StaticJsonDocument doc;
        doc["result"]["ok"] = true;
        std::string out;
        serializeJson(doc, out);
        return out;
    });

// Remove when no longer needed
mcpServer.unregisterMethodHandler("myns/doSomething");

Contributing

1. Fork the repository

2. Create a feature branch: `git checkout -b feature/my-feature`

3. Add tests for new functionality (`pio test -e native` must pass)

4. Open a pull request

See CONTRIBUTING.md for coding style and guidelines.


License

MIT — see LICENSE.

Frequently asked questions

What is esp32mcpserver?

esp32mcpserver is Allow AI models connect to ESP32 exposed interfaces. AI generated MCP server for ESP32.

How do I install esp32mcpserver?

Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.

Is esp32mcpserver open source?

Yes — it is hosted on GitHub at https://github.com/navado/ESP32MCPServer and has 25 stars.

Related MCP tools

Run your own MCP server? See who uses it and what to fix.

Measure it with TrackMCP