Bridge the gap between AI agents and physical hardware with intelligent, event-driven automation
Features • Hardware • Quick Start • Examples • Expand
- What is Arduino-MCP?
- Key Features
- What Makes This Project Unique
- Hardware Stack
- System Architecture
- Prerequisites
- Installation & Setup
- Quick Start
- Example Workflows
- API Reference
- Extensibility & Future Enhancements
- Project Structure
- Troubleshooting
- Contributing
- License
Arduino-MCP is an AI-controlled automation platform that bridges Python AI agents with Arduino-based hardware peripherals. It enables complex, multi-step scenarios without manual supervision by combining:
- Live Hardware Control: Real-time updates to displays, sensors, and actuators
- Event-Driven Automation: Background monitoring and conditional trigger execution
- AI Integration: High-level tools designed for natural language AI agent control
- Non-Blocking Architecture: Firmware runs multiple concurrent tasks seamlessly
Traditional Arduino projects require:
- Manual, continuous control or blocking code
- Complex state management for multi-device coordination
- Constant polling for time-based or sensor-triggered events
- Difficult integration with AI/automation systems
Arduino-MCP provides:
- ✅ High-level Python API for intuitive control
- ✅ Automatic background monitoring of timers, clocks, and sensors
- ✅ Conditional execution engine that chains actions without polling
- ✅ Live displays that update on-device without Python intervention
- ✅ FastMCP integration for seamless AI agent communication
| Display Type | Capabilities | Key Functions |
|---|---|---|
| LCD (16×2) | Live clock, stopwatch, custom text, backlight control | lcd_show_current_time(), lcd_start_stopwatch(), lcd_write_line1/2() |
| TM1637 7-Segment | Live clock, countdown timer, stopwatch, numbers | display_current_time(), display_timer(), display_number() |
| Component | Actions | Functions |
|---|---|---|
| LED | On/Off, Blink, Toggle | led_on(), led_off(), led_blink(interval_ms), led_toggle() |
| Buzzer | On/Off, Timed beep | buzzer_on(), buzzer_off(), buzzer_beep(duration_ms) |
| Ultrasonic Sensor | Single read, continuous monitoring, live LCD stream | ultrasonic_read(), ultrasonic_start(), show_live_ultrasonic_on_lcd() |
Background-triggered actions that execute automatically without polling:
| Trigger Type | When It Fires | Example Use Case |
|---|---|---|
when_timer_finishes() |
Countdown reaches 00:00 | Auto-beep when timer done, chain timers |
when_time_equals() |
Clock matches specific time | Start tasks at scheduled times |
when_distance_less_than() |
Object gets too close | Proximity alerts, collision prevention |
Available Actions:
buzzer_beep— Sound alertstart_timer— Begin countdownled_blink— Visual indicatordisplay_message— Show text on LCD
Unlike typical Arduino sketches that require constant Python calls, our firmware runs displays and timers autonomously in non-blocking loops:
- Clocks tick every second automatically
- Countdowns decrement without Python intervention
- Same strategy as LED blinking — set it once, it runs forever
Built specifically for AI agent control via FastMCP:
- High-level, intent-driven functions
- Natural language compatible tool descriptions
- Complex scenario parsing built-in (e.g., "start timer, then beep, then start another timer at 7:15 AM")
Python background monitor watches Arduino status messages and automatically executes queued actions:
display_timer(1, 30) # Start 90-second countdown
when_timer_finishes("buzzer_beep", "1000") # Auto-beep when done
when_timer_finishes("start_timer", "02:00") # Auto-start 2-min timer after first ends
# No polling loop needed - monitor handles everything!Unified control of LCD, 7-segment display, LED, buzzer, and ultrasonic sensor with synchronized state management.
Traditional approach: while True: check_status(); sleep(1)
Arduino-MCP approach: Set trigger → Background monitor → Action executes automatically
| Component | Model/Type | Connection | Notes |
|---|---|---|---|
| LCD Display | 16×2 I2C (LiquidCrystal_I2C) |
SCL→10, SDA→11 | Default address: 0x27 (try 0x3F if needed) |
| 7-Segment Display | 4-digit TM1637 | CLK→8, DIO→9 | Brightness adjustable (0-15) |
| Ultrasonic Sensor | HC-SR04 or similar | TRIG→7, ECHO→6 | Range: 2-400cm |
| Buzzer | Active buzzer | Pin 13 | Shares pin with LED |
| LED | Built-in | LED_BUILTIN |
Usually pin 13 |
Arduino Uno/Nano
├── LCD I2C ────────► SCL: Pin 10, SDA: Pin 11
├── TM1637 ─────────► CLK: Pin 8, DIO: Pin 9
├── Ultrasonic ─────► TRIG: Pin 7, ECHO: Pin 6
├── Buzzer ─────────► Pin 13
└── LED (built-in) ─► Pin 13 (LED_BUILTIN)
Install via Arduino Library Manager:
LiquidCrystal_I2Cby Frank de BrabanderTM1637Displayby Avishay Orpaz
┌─────────────────────────────────────────────────────────────┐
│ Python Layer (FastMCP) │
├─────────────────────────────────────────────────────────────┤
│ arduino_mcp_server.py │
│ ├── High-level Tools (led_on, display_timer, etc.) │
│ ├── Background Monitor Thread │
│ │ ├── Reads: TIMER:REMAINING, COUNTDOWN:FINISHED │
│ │ ├── Reads: CLOCK:LCD:HH:MM:SS, DISTANCE:<cm> │
│ │ └── Executes: Queued conditional actions │
│ └── Conditional Engine (when_timer_finishes, etc.) │
└─────────────────────────────────────────────────────────────┘
↕ Serial (9600 baud)
┌─────────────────────────────────────────────────────────────┐
│ Arduino Layer (Firmware) │
├─────────────────────────────────────────────────────────────┤
│ master_control.ino │
│ ├── Non-blocking loop() orchestrates all devices │
│ ├── Command Protocol: DEVICE:ACTION:VALUE │
│ │ Example: LED:BLINK:500, TM1637:COUNTDOWN:90 │
│ ├── Display Modes: │
│ │ ├── LCD: clock, stopwatch, static text │
│ │ └── TM1637: clock, countdown, stopwatch, numbers │
│ └── Status Broadcasting (auto-reports state) │
└─────────────────────────────────────────────────────────────┘
↕
┌───────────────┐
│ Hardware │
│ Peripherals │
└───────────────┘
| Command | Format | Example | Description |
|---|---|---|---|
| LED Control | LED:ACTION[:VALUE] |
LED:BLINK:500 |
Blink LED every 500ms |
| Buzzer | BUZZER:ACTION[:VALUE] |
BUZZER:BEEP:1000 |
Beep for 1 second |
| LCD | LCD:ACTION:VALUE |
LCD:LINE1:Hello World |
Write to LCD line 1 |
| TM1637 | TM1637:ACTION:VALUE |
TM1637:COUNTDOWN:90 |
Start 90-second countdown |
| Ultrasonic | ULTRA:ACTION |
ULTRA:START |
Begin continuous monitoring |
| Status | STATUS |
STATUS |
Get all device states |
- Arduino Uno, Nano, or compatible board
- USB cable for programming and serial communication
- Components listed in Hardware Stack
| Requirement | Version | Purpose |
|---|---|---|
| Python | 3.10+ | MCP server and scripts |
| Arduino IDE | 1.8+ or 2.x | Firmware upload |
| pyserial | Latest | Serial communication |
| fastmcp | Latest | AI agent integration |
- Windows (default COM port:
COM6) - macOS/Linux compatible with COM port adjustment
-
Open Arduino IDE
File → Open → master_control.ino -
Install Required Libraries
Sketch → Include Library → Manage Libraries Search: "LiquidCrystal_I2C" → Install Search: "TM1637Display" → Install -
Verify Wiring
- Double-check connections match Hardware Stack
- Test LCD I2C address (try
0x27, fallback0x3F)
-
Upload to Board
Tools → Board → Arduino Uno (or your model) Tools → Port → COM6 (or your port) Upload ✓
-
Install Dependencies
pip install pyserial fastmcp
-
Configure COM Port
- Edit
arduino_mcp_server.pyandmaster_control.py - Set
COM_PORT = "COM6"(adjust to your port) - Check Device Manager (Windows) or
ls /dev/tty*(Linux/Mac)
- Edit
-
Verify Connection
from arduino_mcp_server import test_connection print(test_connection()) # Expected: ✓ SUCCESS: Arduino is connected and ready on COM6
python arduino_mcp_server.pyThe server exposes tools that AI agents can call directly.
python master_control.pyTry these commands:
led blink 500
buzzer beep 200
lcd 1:Hello Arduino!
display clock
ultra start
from arduino_mcp_server import (
display_timer,
when_timer_finishes,
lcd_show_current_time
)
# Start a 30-second countdown on 7-segment display
display_timer(0, 30)
# When countdown finishes, beep 3 times (1 second each)
when_timer_finishes("buzzer_beep", "1000")
# Show live clock on LCD while timer runs
lcd_show_current_time()from arduino_mcp_server import display_timer, when_timer_finishes, lcd_display_message
# 5-minute cooking timer
display_timer(5, 0)
# Show message on LCD
lcd_display_message("Cooking Timer", "5:00 remaining")
# Beep 5 times when done
when_timer_finishes("buzzer_beep", "500")from arduino_mcp_server import lcd_show_current_time, when_time_equals, led_blink
# Display live clock
lcd_show_current_time()
# At 7:15 AM, start blinking LED (morning alarm)
when_time_equals("07:15:00", "led_blink", "1000")
# At 7:16 AM, show message
when_time_equals("07:16:00", "display_message", "Wake Up!|Time to Start")from arduino_mcp_server import (
ultrasonic_start,
when_distance_less_than,
show_live_ultrasonic_on_lcd
)
# Start monitoring distance and show on LCD
show_live_ultrasonic_on_lcd()
# If object closer than 15cm, trigger alarm
when_distance_less_than(15, "buzzer_beep", "2000")
when_distance_less_than(15, "led_blink", "100")from arduino_mcp_server import display_timer, when_timer_finishes
# Workout timer: 45 sec work → beep → 15 sec rest → beep → repeat
display_timer(0, 45)
when_timer_finishes("buzzer_beep", "500")
when_timer_finishes("start_timer", "00:15")
# Add more stages as needed!from arduino_mcp_server import (
get_current_status,
lcd_show_current_time,
display_current_time,
ultrasonic_start
)
# Show time on both displays
lcd_show_current_time()
display_current_time()
# Monitor distance
ultrasonic_start()
# Check system status
print(get_current_status())| Function | Parameters | Description |
|---|---|---|
lcd_show_current_time() |
— | Live PC time on LCD (updates every second) |
lcd_start_stopwatch() |
— | Count up from 00:00:00 on LCD |
lcd_write_line1(text) |
text: str |
Static text on LCD line 1 (max 16 chars) |
lcd_write_line2(text) |
text: str |
Static text on LCD line 2 |
lcd_display_message(line1, line2) |
line1, line2: str |
Write both lines at once |
lcd_clear() |
— | Clear entire LCD |
display_current_time() |
— | Live PC time on 7-segment (HH:MM) |
display_timer(minutes, seconds) |
minutes, seconds: int |
Live countdown to 00:00 with auto-beep |
display_start_stopwatch() |
— | Count up from 00:00 on 7-segment |
display_number(number) |
number: int |
Show static number (-999 to 9999) |
display_clear() |
— | Clear 7-segment display |
| Function | Parameters | Description |
|---|---|---|
led_on() |
— | Turn LED on (solid) |
led_off() |
— | Turn LED off |
led_blink(interval_ms) |
interval_ms: int (default: 500) |
Blink continuously |
led_toggle() |
— | Flip LED state once |
buzzer_on() |
— | Continuous buzzer (call buzzer_off() to stop) |
buzzer_off() |
— | Silence buzzer |
buzzer_beep(duration_ms) |
duration_ms: int (default: 100) |
Single beep, auto-stops |
| Function | Parameters | Description |
|---|---|---|
ultrasonic_read() |
— | Single distance measurement (cm) |
ultrasonic_start() |
— | Continuous monitoring (200ms intervals) |
ultrasonic_stop() |
— | Stop monitoring |
show_live_ultrasonic_on_lcd() |
— | Display + auto-update distance on LCD |
| Function | Parameters | Description |
|---|---|---|
when_timer_finishes(action, params) |
action: str, params: str |
Execute action when countdown → 0 |
when_time_equals(time, action, params) |
time: "HH:MM:SS", action: str, params: str |
Execute at specific clock time |
when_distance_less_than(cm, action, params) |
cm: float, action: str, params: str |
Execute when object too close |
Available Actions:
"buzzer_beep"— params: duration in ms (e.g.,"1000")"start_timer"— params:"MM:SS"format (e.g.,"02:30")"led_blink"— params: interval in ms (e.g.,"500")"display_message"— params:"Line1|Line2"(e.g.,"Alert!|Object Near")
| Function | Description |
|---|---|
get_current_status() |
Get timer, clock, distance, pending actions |
clear_all_pending_actions() |
Cancel all queued triggers |
all_off() |
Turn off/clear all devices (emergency stop) |
test_connection() |
Verify Arduino connection |
Arduino-MCP is designed for extensibility. Here's how you can enhance it:
Example: Adding a Servo Motor
Arduino Firmware (master_control.ino):
#include <Servo.h>
Servo myservo;
void setup() {
myservo.attach(5); // Servo on pin 5
}
void handleServo(String action, String value) {
if (action == "MOVE") {
int angle = value.toInt();
myservo.write(angle);
Serial.println("OK:SERVO:MOVE");
}
}
// Add to processCommand():
else if (device == "SERVO") {
handleServo(action, value);
}Python Server (arduino_mcp_server.py):
@mcp.tool()
def servo_move(angle: int) -> str:
"""Move servo to specified angle (0-180 degrees)"""
if angle < 0 or angle > 180:
return "Error: Angle must be 0-180"
response = send_command(f"SERVO:MOVE:{angle}")
return f"Servo moved to {angle}°"| Sensor Type | Integration Complexity | Potential Use Cases |
|---|---|---|
| Temperature/Humidity (DHT22) | Easy | Climate monitoring, auto-fan control |
| Light Sensor (LDR) | Easy | Auto-brightness, day/night detection |
| PIR Motion Sensor | Easy | Security alerts, auto-lighting |
| Gas/Smoke Detector | Medium | Safety alarms, air quality |
| GPS Module | Medium | Location tracking, geofencing |
Add more trigger types in arduino_mcp_server.py:
@mcp.tool()
def when_temperature_above(threshold: float, then_action: str, params: str) -> str:
"""Execute action when temperature exceeds threshold"""
action = {
"trigger": "temp_above",
"target_temp": threshold,
"action_type": then_action,
"params": parse_params(params)
}
pending_actions.append(action)
return f"✅ Will execute '{then_action}' when temp > {threshold}°C"Multi-Device Morning Routine:
def morning_routine():
# 6:30 AM: Start gentle alarm
when_time_equals("06:30:00", "led_blink", "2000")
# 6:45 AM: Louder alarm
when_time_equals("06:45:00", "buzzer_beep", "500")
# 7:00 AM: Show schedule on LCD
when_time_equals("07:00:00", "display_message", "Good Morning!|Check Schedule")| Enhancement | Difficulty | Impact |
|---|---|---|
| Web Dashboard | Medium | Real-time monitoring via browser |
| MQTT Integration | Medium | IoT device network compatibility |
| Voice Control | Medium-High | "Alexa, start the timer" |
| Data Logging | Low | Historical sensor data storage |
| Mobile App | High | Remote control via smartphone |
| Multi-Board Support | Medium | Control multiple Arduinos |
| Machine Learning | High | Predictive automation based on patterns |
| Relay Control | Easy | Control high-voltage appliances |
| RGB LED Strips | Easy-Medium | Advanced lighting effects |
| SD Card Logging | Medium | Offline data persistence |
Arduino-MCP/
│
├── arduino_mcp_server.py # FastMCP server with tools + conditional engine
│ ├── Tool definitions (@mcp.tool decorators)
│ ├── Background monitor thread
│ ├── Conditional execution engine
│ └── Serial communication layer
│
├── master_control.ino # Arduino firmware
│ ├── Non-blocking loop() architecture
│ ├── Device handlers (LED, buzzer, LCD, TM1637, ultrasonic)
│ ├── Command parser
│ └── Status broadcasting
│
├── master_control.py # Async CLI controller for manual testing
│ ├── Interactive command menu
│ ├── Human-friendly interface
│ └── Testing utilities
│
├── README.md # This file
├── LICENSE # MIT License
└── .gitignore # Git ignore rules
| Problem | Solution |
|---|---|
| LCD shows nothing | • Try I2C address 0x3F instead of 0x27 in firmware• Check wiring (SCL→10, SDA→11) • Verify backlight is on: lcd_backlight_on() |
| Port not found error | • Check Device Manager (Windows) or ls /dev/tty* (Linux)• Update COM_PORT in Python files• Ensure no other program is using the port |
| Clock shows wrong time | • Clock initializes from PC time, then ticks • Re-call lcd_show_current_time() to re-sync |
| Countdown doesn't beep | • Ensure buzzer is connected to pin 13 • Active buzzer required (not passive) • Check buzzer_beep() function works independently |
| Ultrasonic gives -1 | • Check wiring (TRIG→7, ECHO→6) • Ensure object is 2-400cm away • Sensor needs clear line of sight |
| Commands not responding | • Arduino may have reset (wait 2 seconds after connect) • Check baud rate is 9600 in firmware and Python • Call test_connection() to verify |
Enable verbose logging:
import logging
logging.basicConfig(level=logging.DEBUG)Check Arduino status:
from arduino_mcp_server import get_current_status
print(get_current_status())Contributions are welcome! Here's how you can help:
- 🔧 New hardware integrations (sensors, actuators)
- 📚 Documentation improvements
- 🐛 Bug fixes and testing
- 💡 Feature suggestions and implementations
- 🌍 Multi-language support
- 🎨 Web dashboard development
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Test thoroughly with actual hardware
- Commit changes:
git commit -m 'Add amazing feature' - Push to branch:
git push origin feature/amazing-feature - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- FastMCP by Marvin Halbach for the excellent MCP framework
- Arduino Community for hardware libraries and inspiration
- AI Agent Developers who inspire intelligent hardware control
Built with ❤️ for makers, tinkerers, and AI enthusiasts