-
Building an MCP Server on ESP32: Connecting AI Assistants to Real-World Devices
Connecting AI to the Physical World with Model Context Protocol
As detailed in StickyMCP: Notes That Stick, Even in the Cloud, MCP servers open the door for AI systems to interact with real-world tools far beyond their usual diet of static training data and existential boredom.
This project brings together two cutting-edge technologies: the Model Context Protocol (MCP) and Arduino Microcontroller. The result is an MCP server running directly on an Arduino Nano ESP32, allowing AI assistants control physical hardware in real-time.
(Some prompts were typed a bit wrong because I had the camera in the way, but Copilot Chat did not mind at all)
Since there is no MCP SDK for C++, I implemented the RPC protocol myself, kept it small and readable, and made it work to run simple tools like switching lights on and off.
What is it?
This project implements a fully-compliant JSON-RPC 2.0 MCP server on an Arduino Nano ESP32 microcontroller. It exposes hardware controls (LEDs in this case) as MCP "tools" that can be invoked by AI assistants through natural language commands.
Key Features
- MCP Protocol Support: Implements the MCP 2024-11-05 specification with proper initialization, tool listing, and tool execution
- JSON-RPC 2.0 Compliance: Standard protocol interface for reliable communication
- WiFi-Enabled: Runs a web server on port 8000, making it accessible over the network
- Server-Sent Events (SSE): Real-time notifications and logging stream for monitoring
- Multiple LED Controls: Manages built-in LED plus RGB LEDs (red, green, blue) independently
- Echo Tool: Simple text echo for testing and demonstration
Technical Implementation
The server uses the ESPAsyncWebServer library for handling HTTP requests and ArduinoJson for JSON parsing/serialization. It exposes two main endpoints:
POST /mcp- Main JSON-RPC 2.0 endpoint for all MCP methodsGET /sse- Server-Sent Events stream for real-time notifications
Each LED is implemented as an MCP tool with a simple boolean parameter to turn it on or off. The server handles all the MCP lifecycle methods including initialization, capability negotiation, and tool invocation.
Real-World Applications
This project demonstrates how AI assistants can seamlessly control physical devices. Instead of writing custom scripts or manual API calls, you can simply tell an AI assistant "turn on the red LED" and it happens. This opens up possibilities for:
- Smart home automation controlled by natural language
- Laboratory equipment control through AI assistants
- Educational demonstrations of AI-hardware integration
- Rapid prototyping of IoT devices with conversational interfaces
Technical Stack:
- Arduino Nano ESP32
- ESPAsyncWebServer
- ArduinoJson
- Model Context Protocol (MCP) 2024-11-05
- JSON-RPC 2.0
-
Arduino Darkroom Timer
Precise timing is critical in analog photography, especially during the development process in the darkroom. This project presents a versatile darkroom timer built around the Arduino Uno R4 and Gamepad Input Shield, designed to support key photographic development tasks with tactile control and visual feedback. It features an 8x8 LED matrix for ambient illumination and implements five distinct modes: LED brightness adjustment, manual and timed enlarger control, chemical bath sequencing, and a general-purpose timer. Navigation is intuitive via joystick or button input, making it a practical and user-friendly tool for analog photography workflows.
This project uses the Gamepad Input Shield and Arduino Uno R4. Wiring details are available in the source code. The joystick or up/down buttons are used to switch between tasks, while left/right buttons adjust the state or settings.
The first 8x8 LED block is used to illuminate the darkroom.The timer implements five tasks:
- LEDBrightnessTask: Adjusts LED brightness from 0 to 15.
- EnlargerOnOffTask: Manual control of the enlarger.
- EnlargerTimerTask: Timed control of the enlarger.
- DevelopStopFixTask: Three timers for develop, stop bath, and fix stages.
- GeneralTimerTask: A general-purpose timer starting from 0 seconds.
LEDBrightnessTask
Sets the brightness of the 8x8 darkroom illumination LEDs from min 0 to max 15.
EnlargerOnOffTask
Manual enlarger switch switched on.
EnlargerTimerTask
Timed enlarger switch.
DevelopStopFixTask
3 timers for develop, stop bath and fix.
GeneralTimerTask
General timer starting from 0s.
Arduino Darkroom Timer Github Project
This darkroom timer project combines modern microcontroller technology with the tactile simplicity of gamepad input to support essential analog photography tasks. Whether you are timing enlarger exposures or chemical baths, this tool offers consistent performance and flexibility for both amateur and professional darkroom enthusiasts. With five dedicated modes and intuitive controls, it provides a reliable and customizable solution for anyone working in a traditional darkroom. The open-source design invites further experimentation and adaptation. Whether you are refining your workflow or building your own version.
-
IOT: Arduino weather station, using the Arduino MKR 1010
The new Arduino MKR 1010 was released this year and supports wireless connection. It is similar to the M0 Pro, in that it also uses a SAMD21 32bit processor. It is much smaller but has the same amount of I/O pins available as the M0 Pro.
I had to try it out!
The previous setup was using an Ethernet shield that connected the controller by cable with the router.
I moved everything to an experimental board and switched out the Ethernet class and replaced it with the new Wifi class, along with a few other changes to get it working.
The bright spot in the middle of the board is actually a RGB LED of the wireless controller! I use each of the three colors to indicate HTTP requests, update of time and wind data and update of the air pressure, humidity and temperature.
Out of a brightness range from 1..255, 7 seems a good value to match the internal yellow LED.
It is also possible to use a digitalWrite on the LED port to switch it on or off. This is the same as using analogWrite with 255. This is very bright.const int GREEN_LED = 25; const int RED_LED = 26; const int BLUE_LED = 27; const int LED_Brightness = 7; // Configure the LED port of the wireless controller. WiFiDrv::pinMode(GREEN_LED, OUTPUT); WiFiDrv::pinMode(RED_LED, OUTPUT); WiFiDrv::pinMode(BLUE_LED, OUTPUT); // Set the LED. WiFiDrv::analogWrite(GREEN_LED, bGreenLed ? LED_Brightness : 0);The webserver on the board provides a full HTML page and the weather data in a json format for the AJAX requests that gets updated every second.
The matching App is using the AJAX data for display
and also provides access to the weather data for wind, temperature and air pressure stored in the 32Kb chip.

-
IOT: Arduino weather station, Overview and Installation
Using a Arduino M0 Pro to power a weather station to measure wind speed/direction, humidity and temperature inside and outside, barometric pressure and rainfall, support REST API for the weather app and HTTP Server:
The white sensor is a DHT22 to measure the humidity and temperature inside.
The 8 pin DIP on the experimental board is a 24LC256 (32KB) to hold weather data statistics.
The wireless module is already connected and will be used later to upload data to the Azure IOT hub.
First test with the weather station to check the directional sensor and the areometer signal.
The weather station is a very popular model, but the documentation has a wrong value for the resistance for one direction.
Here is the correct list:Direction (Degrees) Resistance (Ohms) Voltage (V=5v, R=10k) 0 33k 3.84v 22.5 6.57k 1.98v 45 8.2k 2.25v 67.5 891 0.41v 90 1k 0.45v 112.5 688 0.32v 135 2.2k 0.90v 157.5 1.41k 0.62v 180 3.9k 1.40v 202.5 3.14k 1.19v 225 16k 3.08v 247.5 14.12k 2.93v 270 120k 4.62v 292.5 42.12k 4.04v 315 64.9k 4.33v 337.5 21.88k 3.43v This translates to the following analog values using the recommended 10K resistor. The analog2index function returns the index (the wind direction) of the received value from the Arduino analog input port. Since the differences between the individual values is not constant, this mapping is the most precise mapping to get the wind direction.
const int analogwerte[] = { 787, 406, 461, 84, 93, 66, 185, 127, 287, 244, 631, 601, 947, 828, 887, 703 }; const int8_t richtungen = sizeof(analogwerte) / sizeof(int); int8_t analog2index(const int analogwert) { // nächsten Wert zu analogwert in analogwerte suchen int t = 20; // Starttoleranz: +-20 int index = -2; // return -1 für Fehler for(int8_t i = 0; i < richtungen; i++) { const int d(abs(analogwert - analogwerte[i])); if(d < t) { t = d; index = i; } } return index / 2; }
The installation:
Everything is packed and pulled up by rope up to the roof.
Bolted to the chimney.
The junction box hosting the outdoor sensor for humidity and (shadow) temperature.
The main junction box hosting the barometric pressure sensor (BMP180) including the second outdoor temperature.
The barometric pressure sensor is located on the lower left of the experimental board.