Skip to main content
Glama

esp32-mcp

An MCP server that gives an AI agent control of an ESP32: relays, GPIO pins and analog sensors. The Python side has no dependencies and implements the MCP stdio protocol by hand. The board runs a single Arduino sketch and speaks JSON over HTTP.

AI agent  <-- stdio, JSON-RPC 2.0 -->  esp32-mcp  <-- HTTP, JSON -->  ESP32 firmware

I built this because most "AI controls hardware" demos hide a cloud service in the middle. Here the agent talks to a process on your machine, that process talks to a board on your LAN, and nothing leaves the network.

Status

v0.1.0. The whole path works end to end against a simulated board: handshake, tool discovery, relay switching, sensor reads and GPIO reads and writes. The test suite runs without hardware. The sketch compiles for esp32:esp32:esp32 with arduino-cli 1.4.1 and ESP32 core 3.3.12 (74% flash, 15% RAM). CI does not compile the sketch yet, see Deferred.

Related MCP server: ESP32 MCP Server

What is in the box

Path

What it is

src/esp32_mcp/

MCP server, JSON-RPC framing, HTTP device client, tool registry

src/esp32_mcp/fake_device.py

Simulated ESP32 used by the tests and the demo

firmware/esp32_mcp/esp32_mcp.ino

Complete Arduino sketch, pin map at the top

scripts/demo.py

Spawns the server, runs the handshake and calls real tools

docs/wiring.md

Pin tables, wiring diagram, power and safety notes

tests/

pytest suite, no hardware required

Install

git clone https://github.com/StefanYasin/esp32-mcp.git
cd esp32-mcp
python -m pip install -e .

Python 3.9 or newer. There are no runtime dependencies.

Try it without hardware

python scripts/demo.py --fake

That starts the built in simulator, spawns esp32-mcp as a real subprocess and prints the full JSON-RPC exchange: initialize, tools/list, then calls to esp32_health, esp32_read_sensors, esp32_set_relay, esp32_gpio_write and esp32_gpio_read.

Flash the firmware

  1. Install the ESP32 board package in the Arduino IDE (Boards Manager -> esp32 by Espressif).

  2. Open firmware/esp32_mcp/esp32_mcp.ino.

  3. Edit the CONFIG block at the top: WiFi credentials, API_TOKEN, the relay table, the analog sensor table and the GPIO allow list.

  4. Select ESP32 Dev Module, upload, then open the serial monitor at 115200 baud.

  5. Note the IP address it prints, or use the mDNS name http://esp32-mcp.local.

curl http://esp32-mcp.local/health
curl -X POST http://esp32-mcp.local/relay/1/on
curl "http://esp32-mcp.local/gpio?pin=2&value=1"

The same thing from the command line, which is the path I used to check the sketch:

arduino-cli core install esp32:esp32
arduino-cli compile --fqbn esp32:esp32:esp32 firmware/esp32_mcp
arduino-cli upload --fqbn esp32:esp32:esp32 -p COM5 firmware/esp32_mcp

Swap COM5 for your port. Wiring, pin tables and the safety notes are in docs/wiring.md.

Connect an MCP client

Point the client at the esp32-mcp executable and give it the board URL. This is the Claude Desktop shape; other clients use the same command and env fields.

{
  "mcpServers": {
    "esp32": {
      "command": "esp32-mcp",
      "env": {
        "ESP32_MCP_URL": "http://192.168.1.42",
        "ESP32_MCP_TOKEN": ""
      }
    }
  }
}

If you installed into a virtualenv, use the absolute path to the esp32-mcp executable in that venv.

Environment variables:

Variable

Default

Meaning

ESP32_MCP_URL

http://esp32-mcp.local

Base URL of the board

ESP32_MCP_TOKEN

empty

Sent as X-Api-Token when set

ESP32_MCP_TIMEOUT

5.0

HTTP timeout in seconds

Flags --url, --token, --timeout and --verbose override the environment. Logs go to stderr so they never corrupt the protocol stream.

Tools

Tool

Arguments

What it does

esp32_health

none

Firmware version, IP, uptime, free heap and the relay table

esp32_read_sensors

none

Chip temperature plus every configured analog channel

esp32_set_relay

relay (1 to 8), state (on or off)

Switches one relay, idempotent

esp32_gpio_read

pin (0 to 39)

Reads pin mode and digital level

esp32_gpio_write

pin, optional value (0 or 1), optional mode (output, input, input_pullup)

Changes a pin mode and optionally drives it

Agents call esp32_health first to learn which relays and pins exist. Bad arguments come back as a tool result with isError: true and a message that says what to fix. An unknown tool name is a JSON-RPC -32602 error, which is what the MCP spec asks for.

Only pins in the firmware allow list can be touched. The default list leaves out the flash pins and the strapping pins.

HTTP API on the board

The server and the firmware share this contract. It is small on purpose so the sketch stays readable.

Method

Path

Response

GET

/health

{"ok":true,"device":"esp32-mcp","firmware":"0.1.0",...,"relays":[{"index":1,"name":"relay1","pin":16,"state":"off"}]}

GET

/sensors

{"ok":true,"sensors":[{"id":"chip_temp","value":41.5,"unit":"c"},{"id":"soil_moisture","pin":34,"raw":1820,"millivolts":1466,"value":44.4,"unit":"percent"}]}

GET or POST

/relay/{n}/on

{"ok":true,"relay":1,"name":"relay1","pin":16,"state":"on"}

GET or POST

/relay/{n}/off

{"ok":true,"relay":1,"name":"relay1","pin":16,"state":"off"}

GET

/gpio?pin=N

{"ok":true,"pin":2,"mode":"output","value":1}

POST

/gpio?pin=N&value=0|1

Same shape, sets the level

POST

/gpio?pin=N&mode=output|input|input_pullup

Same shape, sets the mode

Errors use the HTTP status plus {"ok":false,"error":"..."}: 400 for bad parameters, 401 for a bad token, 403 for a pin outside the allow list, 404 for an unknown relay or route, 405 for the wrong method.

Protocol details

The MCP stdio transport is newline delimited JSON-RPC 2.0, one message per line, UTF-8. This server implements:

  • initialize with protocol version 2024-11-05, tools capability and server instructions

  • notifications/initialized

  • ping

  • tools/list

  • tools/call

Anything else returns -32601 Method not found. Malformed JSON returns -32700 and the stream stays open. Notifications never get a response.

Tests

python -m pytest

88 tests covering the JSON-RPC layer, tool schemas and dispatch, the HTTP client, the simulator contract and the real subprocess stdio transport. No hardware and no network access required; the simulator binds to 127.0.0.1 on a random port.

There is also a compatibility check against the official MCP Python SDK, which is a separate optional install:

python -m pip install mcp
python scripts/interop_check.py

It starts the simulator, spawns the server through the SDK stdio client and checks the handshake, the tool list, a successful relay call and a rejected pin. I ran it against mcp 2.3.0 and it passes. It is not part of CI because the SDK would be a heavy dev dependency.

Security

The board API is plain HTTP with an optional shared token. That is fine on a trusted LAN and wrong on the public internet. Do not port forward the ESP32, and do not put this behind a public reverse proxy without TLS and real authentication.

Relays can switch mains voltage. Use an opto-isolated relay module, keep the mains side away from the logic side, and read docs/wiring.md before connecting anything.

Deferred

  • TLS and per client credentials

  • PWM and analog writes, servo control

  • Sensor drivers (DHT22, DS18B20, I2C) beyond the internal temperature and raw ADC channels

  • OTA firmware updates

  • A sketch compile job in CI. The ESP32 core is about a 1 GB download, so for now the check is a local compile

  • Newer MCP protocol revisions, the server pins 2024-11-05 for now

  • An interop job in CI against the official MCP SDK, the check currently runs by hand

License

MIT. See LICENSE.

Built by Stefan Yasin (https://stefanyasin.github.io/)

Available Tools

5 tools
esp32_gpio_readA

Read the mode and digital level of one GPIO pin. Only pins in the firmware allow list can be read.

ParametersJSON Schema
NameRequiredDescriptionDefault
pinYesGPIO number, for example 2.

TDQS

A3.5/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description must carry the behavioral load: it does disclose the read-only nature and the allow-list restriction, both of which are non-obvious operational facts. It does not say what error surfaces for a disallowed pin, whether reading has side effects, or anything about rate/latency, so meaningful gaps remain.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two tight sentences with no filler, and the core action is front-loaded before the constraint. Nothing wastes space, though there is room to fold in a routing hint without bloating it.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter read with no output schema, the description usefully states what comes back (mode and digital level) and flags the allow-list precondition. The remaining gap is error behavior for blocked pins, which is minor for a tool this simple.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, and the single 'pin' parameter is fully documented in the schema with type, range, and an example. The description adds no further semantics about pin selection or numbering beyond what the schema already provides, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

Names a specific verb and resource ('Read the mode and digital level of one GPIO pin'), which clearly separates it from the write-oriented siblings like esp32_gpio_write and esp32_set_relay. It stops short of naming those alternatives explicitly, so sibling differentiation is implied rather than stated.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The only guidance is a constraint — 'Only pins in the firmware allow list can be read' — which tells the agent when the call will fail but not when to prefer this tool over esp32_gpio_write or esp32_read_sensors. Usage context is inferable from the read verb but never spelled out.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

esp32_gpio_writeA

Set a GPIO pin mode (output, input, input_pullup) and optionally drive it high or low. Pass value only, mode only, or both. Only pins in the firmware allow list can be changed.

ParametersJSON Schema
NameRequiredDescriptionDefault
pinYesGPIO number, for example 2.
modeNoPin mode to apply before the value.
valueNoDigital level: 1 high, 0 low.

TDQS

A3.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden. It discloses the allow-list restriction and that mode is applied before value, but does not explain error behavior when a pin is disallowed, whether changes persist, or hardware side effects of driving a pin.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three tightly scoped sentences with no filler. The core operation, optional parameter combinations, and allow-list constraint are all front-loaded and each sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool is simple and the schema is rich, so most invocation details are covered. Yet as an unannotated hardware mutation, the description lacks important context about failure modes, persistence, and side effects, which keeps it at a minimum-viable level.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the schema already documents each parameter. The description adds useful combination semantics by stating that value only, mode only, or both are valid, which goes beyond the individual field descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: setting a GPIO pin mode and optionally driving it high or low. It does not explicitly distinguish this from the sibling esp32_gpio_read, but the write/drive framing makes the purpose clear.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives useful usage constraints: you may pass value only, mode only, or both, and only firmware-allow-listed pins can be changed. However, it does not explicitly say when to use this tool versus esp32_gpio_read or other siblings, leaving alternative selection to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

esp32_healthA

Read board health: firmware version, IP address, hostname, uptime, free heap and the relay table (index, name, pin, state). Call this first to learn what the board exposes.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations exist, so the description carries the load: 'Read' establishes a non-mutating operation and the enumerated fields tell the agent exactly what comes back with no side effects. It stops short of saying whether the data is cached, whether the board can be unreachable, or anything about failure modes.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences: the first delivers the complete return payload, the second delivers the workflow cue. Front-loaded, zero filler, and the relay-table column list is compactly parenthesized rather than spelled out at length.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description must enumerate return values and it does so completely, including the shape of the nested relay table (index, name, pin, state). For a zero-parameter read tool with no annotations, nothing an agent needs before calling is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so there is nothing for the description to disambiguate and the baseline applies. The schema is closed (additionalProperties false, empty properties), matching the no-argument call described.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb ('Read') and resource ('board health') and then enumerates the exact payload: firmware version, IP, hostname, uptime, free heap and the relay table with its columns. The closing instruction 'Call this first' separates it from the sensor/GPIO/relay siblings without needing to name them.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly prescribes ordering — 'Call this first to learn what the board exposes' — which tells the agent this is the discovery step before esp32_read_sensors or esp32_set_relay. It doesn't state exclusions or name the alternatives outright, so it stops short of a full when/when-not map.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

esp32_read_sensorsA

Read every sensor configured in the firmware: the internal chip temperature and each analog channel, with raw ADC counts, millivolts and a scaled value.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. 'Read' makes the non-mutating nature clear and the sentence usefully discloses the shape of the returned readings (raw counts, mV, scaled), but says nothing about behavior when no sensors are configured, error responses, or sampling timing.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single well-formed sentence that front-loads the verb and scope, then lists the outputs. No filler, nothing repeated from the schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter read tool with no annotations and no output schema, the description covers what the call does and what comes back, which is most of what an agent needs. It falls short only on edge cases like an empty sensor configuration or read failures.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters and the schema is empty, so there are no parameter semantics to explain. Baseline 4 applies; the description correctly implies a parameterless bulk read.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a concrete verb+resource (read every sensor) and enumerates the specific data returned: chip temperature, each analog channel, with raw ADC counts, millivolts and a scaled value. It implicitly distinguishes itself from esp32_gpio_read by scoping to analog channels, but never names a sibling explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: 'read every sensor configured in the firmware' suggests a bulk snapshot call, and the absence of parameters reinforces that. It does not state when to prefer esp32_health or esp32_gpio_read instead, nor any prerequisites.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

esp32_set_relayA

Switch one relay on or off. Relay indexes are 1 based and are listed by esp32_health. The call is idempotent.

ParametersJSON Schema
NameRequiredDescriptionDefault
relayYes1 based relay index.
stateYesTarget state.

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full behavioral burden and does disclose one genuinely important trait: the call is idempotent. It still does not say whether state persists after power loss, what happens on an out-of-range or unavailable relay, or whether any state is returned, which are material gaps for a hardware mutation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short sentences, front-loaded with the action, followed by index discovery and the idempotency note. There is no filler and every sentence carries information an agent needs.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a two-parameter mutation with no annotations and no output schema, the description covers purpose, valid index sourcing, and idempotency, which is close to sufficient. What remains thin is failure behavior and return semantics, though with a fully documented schema the gap is modest.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3, and the description adds real value on top by explaining that relay indexes are 1-based and can be enumerated via esp32_health. It does not mention the upper bound of 8 or restate the on/off values in any additional way, so it stops short of full marks.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource pair ("Switch one relay on or off") with clear single-unit scope, so an agent knows exactly what the tool does. It does not explicitly distinguish itself from the sibling esp32_gpio_write, which an agent could plausibly confuse with relay switching, so it falls short of a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description points to esp32_health as the way to discover valid relay indexes, which is useful routing guidance for setup. However, it never says when to prefer this over esp32_gpio_write or esp32_gpio_read, nor any precondition such as device being online, so usage is only implied.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 5 tool updatesv0.1.0
    • First observedesp32_gpio_read
    • First observedesp32_gpio_write
    • First observedesp32_health
    • First observedesp32_read_sensors
    • First observedesp32_set_relay

TDQS

A3.9/5.0

Scored across 5 tools

Disambiguation4/5

Each tool has a distinct action: health (board identity/diagnostics), read_sensors (sensor values), set_relay (relay control), gpio_read, gpio_write. The only mild overlap is that esp32_health also reports the relay table and health metrics while esp32_read_sensors reports chip temperature, but the descriptions make the boundary clear enough.

Naming Consistency4/5

All tools share the esp32_ prefix and a consistent snake_case form. Minor deviation: read_sensors/set_relay use verb_noun ordering while gpio_read/gpio_write use noun_verb, though the gpio pair is internally consistent and readable.

Tool Count5/5

Five tools is a tight, well-scoped surface for a microcontroller: one discovery, one sensor read, one relay control, and a GPIO read/write pair. Nothing feels redundant or padded.

Completeness4/5

Core lifecycle is covered: discovery, sensor reading, relay actuation, and GPIO read/write. Gaps exist for reset/reboot, OTA/firmware update, and analog/PWM output, but these are minor and can be worked around for typical use.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Bridges the Model Context Protocol (MCP) with ESP32 devices running Tasmota firmware, enabling LLMs to send structured commands like toggling relays or reading sensors via HTTP.
    1
    ISC
  • A
    license
    Not graded
    quality
    A
    maintenance
    ESP32-based Streamable HTTP MCP server with embedded Lua scripting engine. Bridges Claude AI to physical devices — telescopes (INDI protocol), sensors, BLE peripherals, and TCP devices. Flash-and-run binary distribution, no build required.
    13
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    MCP server that gives AI agents direct access to IoT devices via the jettyd platform, enabling control and monitoring of ESP32 devices through natural language.
    5
    17 npm
    MIT