Skip to content

Firmware Guide

The library lives in firmware/lib/McpIot/ and is auto-detected by PlatformIO.

For standalone use, add to platformio.ini:

lib_deps =
bblanchon/ArduinoJson @ ^7
; path to McpIot (local or GitHub)
  1. Download as ZIP
  2. Sketch → Include Library → Add .ZIP Library
  3. Select the ZIP
  4. #include <McpIot.h> in your sketch

Constructor. Declare once at file scope.

McpDevice mcp("my-robot", "2.1.0");

Register a hardware pin. Call before begin().

ParamTypeDescription
pinuint8_tGPIO pin number
nameconst char*Short identifier (e.g. "led")
typeMcpPinTypeSee pin types below
descriptionconst char*Human-readable description
mcp.add_pin(2, "led", MCP_DIGITAL_OUTPUT, "Status LED");
mcp.add_pin(34, "sensor", MCP_ADC_INPUT, "Light Sensor");

Register a custom RPC tool. Call before begin().

void my_handler(int id, JsonObject params) {
int value = params["value"].as<int>();
JsonDocument res;
res["result"]["ok"] = true;
mcp.send_result(id, res);
// On error:
// mcp.send_error(id, -32602, "Invalid parameter");
}
mcp.add_tool("my_action", "Does something custom", my_handler);

Start the MCP device on any Arduino Stream.

mcp.begin(Serial, 115200); // USB Serial
mcp.begin(Serial2, 9600); // Hardware Serial 2
mcp.begin(wifi_client); // WiFiClient (connect first)
mcp.begin(bt); // BluetoothSerial

Call from Arduino loop(). Reads and dispatches one RPC request per call.

void loop() {
mcp.loop();
}

Send a success response. Set doc["result"] before calling.

JsonDocument res;
res["result"]["temperature"] = 25.4;
mcp.send_result(id, res);

Send an error response.

mcp.send_error(id, -32602, "Invalid pin number");

Standard codes: -32700 parse error · -32600 invalid request · -32601 not found · -32602 bad params


ConstantDirectionAvailable built-in tools
MCP_DIGITAL_OUTPUTOutputgpio_write, gpio_read
MCP_DIGITAL_INPUTInputgpio_read
MCP_PWM_OUTPUTOutputpwm_write
MCP_ADC_INPUTInputadc_read

Override via build flags in platformio.ini:

build_flags =
-DMCP_MAX_PINS=32
-DMCP_MAX_TOOLS=32
-DMCP_SERIAL_BUFFER=1024
ConstantDefaultDescription
MCP_MAX_PINS16Max registered pins
MCP_MAX_TOOLS24Max registered tools
MCP_SERIAL_BUFFER512Serial read buffer (bytes)

The simplest transport — plug in a USB cable and go. Works on every Arduino-compatible board.

#include <McpIot.h>
McpDevice mcp("esp32-demo", "1.0.0");
void handle_hello(int id, JsonObject params) {
JsonDocument res;
res["result"]["message"] = "Hello from ESP32!";
mcp.send_result(id, res);
}
void setup() {
mcp.add_pin(2, "led", MCP_DIGITAL_OUTPUT, "Onboard LED");
mcp.add_pin(5, "buzzer", MCP_DIGITAL_OUTPUT, "Piezo Buzzer");
mcp.add_pin(34, "sensor", MCP_ADC_INPUT, "Analog Sensor");
mcp.add_tool("hello", "Returns a greeting", handle_hello);
mcp.begin(Serial, 115200);
}
void loop() { mcp.loop(); }

No USB cable required — the MCU acts as a TCP server on your local network. The client connects by IP address and port.

#include <WiFi.h>
#include <McpIot.h>
static const char* WIFI_SSID = "YOUR_SSID";
static const char* WIFI_PASSWORD = "YOUR_PASSWORD";
static const uint16_t TCP_PORT = 3000;
McpDevice mcp("esp32-wifi", "1.0.0");
WiFiServer server(TCP_PORT);
WiFiClient client;
void setup() {
Serial.begin(115200);
mcp.add_pin(2, "led", MCP_DIGITAL_OUTPUT, "Onboard LED");
mcp.add_pin(5, "buzzer", MCP_DIGITAL_OUTPUT, "Piezo Buzzer");
mcp.add_pin(34, "sensor", MCP_ADC_INPUT, "Analog Sensor");
Serial.print("Connecting to WiFi");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("IP: ");
Serial.println(WiFi.localIP()); // note this IP for the client
server.begin();
}
void loop() {
// Accept new client if none is connected
if (!client || !client.connected()) {
WiFiClient incoming = server.accept();
if (incoming) {
client = incoming;
mcp.begin(client); // swap the MCP stream to the TCP connection
}
}
mcp.loop();
}

The recommended pattern for any sensor or peripheral is to wrap it in a custom tool. Your firmware handles all the hardware complexity — Claude sees only clean, named results.

The built-in pwm_write tool uses analogWrite(pin, duty) with the default 5 kHz frequency — enough for LED dimming and most use cases.

If you need a custom frequency (e.g. motor control, buzzer tone, servo), use the ESP32 LEDC API in a custom tool instead:

#include <McpIot.h>
McpDevice mcp("esp32-demo", "1.0.0");
void handle_buzzer(int id, JsonObject params) {
int freq = params["freq"] | 1000; // Hz
int duty = params["duty"] | 128; // 0–255
ledcSetup(0, freq, 8); // channel 0, 8-bit resolution
ledcAttachPin(5, 0); // GPIO 5 → channel 0
ledcWrite(0, duty);
JsonDocument res;
res["result"]["freq"] = freq;
res["result"]["duty"] = duty;
mcp.send_result(id, res);
}
void setup() {
mcp.add_tool("buzzer_tone", "Play a tone on the buzzer (freq Hz, duty 0-255)", handle_buzzer);
mcp.begin(Serial, 115200);
}

BME280 (Temperature / Humidity / Pressure)

Section titled “BME280 (Temperature / Humidity / Pressure)”
#include <McpIot.h>
#include <Adafruit_BME280.h>
McpDevice mcp("weather-node", "1.0.0");
Adafruit_BME280 bme;
void handle_read_bme280(int id, JsonObject params) {
JsonDocument res;
res["result"]["temperature"] = bme.readTemperature();
res["result"]["humidity"] = bme.readHumidity();
res["result"]["pressure"] = bme.readPressure() / 100.0F;
mcp.send_result(id, res);
}
void setup() {
bme.begin(0x76);
mcp.add_tool("read_bme280", "Read temperature (°C), humidity (%), pressure (hPa)", handle_read_bme280);
mcp.begin(Serial, 115200);
}
#include <McpIot.h>
#include <Adafruit_SSD1306.h>
McpDevice mcp("display-node", "1.0.0");
Adafruit_SSD1306 display(128, 64, &Wire);
void handle_show_text(int id, JsonObject params) {
const char* text = params["text"] | "";
display.clearDisplay();
display.setCursor(0, 0);
display.println(text);
display.display();
JsonDocument res;
res["result"]["ok"] = true;
mcp.send_result(id, res);
}
void setup() {
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
mcp.add_tool("show_text", "Display text on OLED screen", handle_show_text);
mcp.begin(Serial, 115200);
}
#include <McpIot.h>
#include <MPU6050_light.h>
McpDevice mcp("motion-node", "1.0.0");
MPU6050 mpu(Wire);
void handle_read_motion(int id, JsonObject params) {
mpu.update();
JsonDocument res;
res["result"]["angle_x"] = mpu.getAngleX();
res["result"]["angle_y"] = mpu.getAngleY();
res["result"]["angle_z"] = mpu.getAngleZ();
mcp.send_result(id, res);
}
void setup() {
Wire.begin(21, 22);
mpu.begin();
mcp.add_tool("read_motion", "Read gyroscope angles (degrees)", handle_read_motion);
mcp.begin(Serial, 115200);
}
#include <McpIot.h>
#include <LiquidCrystal_I2C.h>
McpDevice mcp("lcd-node", "1.0.0");
LiquidCrystal_I2C lcd(0x27, 16, 4);
void handle_show_text(int id, JsonObject params) {
int row = params["row"] | 0;
int col = params["col"] | 0;
lcd.setCursor(col, row);
lcd.print(params["text"].as<const char*>());
JsonDocument res;
res["result"]["ok"] = true;
mcp.send_result(id, res);
}
void setup() {
Wire.begin(21, 22);
lcd.init();
lcd.backlight();
mcp.add_tool("show_text", "Write text to LCD (params: text, row=0, col=0)", handle_show_text);
mcp.begin(Serial, 115200);
}

PlatformSerialWiFi TCPBluetoothPWM freq
ESP32✅ BLE Serial
ESP8266⚠️ fixed
AVR (Uno/Mega)⚠️ limited pins
RP2040 (Pico)⚠️ Pico W