mcbluetooth-esp32
ESP32 Bluetooth test harness MCP server — UART-controlled peripheral for automated E2E Bluetooth testing.
Overview
This project turns an ESP32 into a programmable Bluetooth peripheral that can be controlled via MCP (Model Context Protocol). Combined with mcbluetooth (Linux BlueZ MCP server), it enables fully automated end-to-end Bluetooth testing orchestrated by an LLM.
┌─────────────────────────────────────────────────────────────────┐
│ LLM (Claude, etc.) │
│ Orchestrates both MCP servers │
└─────────────────────────┬───────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
┌───────┴───────┐ ┌───────┴────────┐
│ mcbluetooth │ │mcbluetooth-esp32│
│ (bt_* tools)│ │ (esp32_* tools) │
└───────┬───────┘ └───────┬────────┘
│ │
D-Bus/BlueZ Serial/UART
│ │
┌───────┴───────┐ ┌───────┴────────┐
│ Linux Host │◄── Bluetooth ──►│ ESP32 │
│ (hci0/1) │ (over air) │ (peripheral) │
└───────────────┘ └────────────────┘
ESP32 Capabilities
The ESP32 can emulate various Bluetooth devices for testing:
| Capability |
Description |
| Classic BT Pairing |
All 4 SSP modes: Just Works, Numeric Comparison, Passkey Entry, Legacy PIN |
| SPP (Serial Port) |
Bidirectional data transfer over Classic Bluetooth virtual serial port |
| BLE GATT Server |
Dynamic service/characteristic creation at runtime |
| Device Personas |
Presets for headset, speaker, keyboard, sensor, phone |
| IO Capabilities |
Configurable: no_io, display_only, display_yesno, keyboard_only, keyboard_display |
| Event Reporting |
Real-time events: pair_request, pair_complete, spp_data, gatt_read, gatt_write, gatt_subscribe |
Hardware Requirements
- Original ESP32 (ESP32-D0WD or similar) — must have Classic Bluetooth support
- ESP32-S3, C3, H2, S2 will NOT work (no BR/EDR radio)
- USB cable for serial communication
Verified Hardware
| Property |
Value |
| Chip |
ESP32-D0WD-V3 (rev 3.1) |
| Features |
Wi-Fi, BT Classic + BLE, Dual Core 240MHz |
| Flash |
4MB |
| Protocol |
NDJSON over UART @ 115200 baud |
Quick Start
1. Flash the firmware
# Install ESP-IDF v5.x first (see docs/hardware-setup.md)
cd firmware
idf.py set-target esp32
idf.py -p /dev/ttyUSB0 flash
2. Run the MCP server
# Install and run
uvx mcbluetooth-esp32
# Or with explicit port
ESP32_SERIAL_PORT=/dev/ttyUSB0 uvx mcbluetooth-esp32
3. Add to Claude Code
claude mcp add mcbluetooth-esp32 -- uvx mcbluetooth-esp32
4. Verify connection
esp32_connect() # → connected=true, ready=true
esp32_ping() # → {pong: true}
esp32_get_info() # → chip, fw_version, bt_mac
MCP Tools
Connection & System
| Tool |
Description |
esp32_connect |
Open serial connection to ESP32 |
esp32_disconnect |
Close serial connection |
esp32_ping |
Verify UART link is alive |
esp32_status |
Get current BT/BLE state |
esp32_get_info |
Get chip model, firmware version, MAC |
esp32_reset |
Reboot the ESP32 |
Configuration
| Tool |
Description |
esp32_configure |
Set device name, IO capabilities, PIN |
esp32_set_ssp_mode |
Configure SSP pairing mode with optional auto-accept |
esp32_load_persona |
Load preset device profile (headset, speaker, etc.) |
Classic Bluetooth
| Tool |
Description |
esp32_classic_enable |
Enable BR/EDR radio |
esp32_classic_disable |
Disable BR/EDR radio |
esp32_classic_set_discoverable |
Make device visible for pairing |
esp32_classic_pair_respond |
Accept/reject incoming pairing |
SPP (Serial Port Profile)
| Tool |
Description |
esp32_spp_send |
Send data (text or hex) over SPP connection |
esp32_spp_disconnect |
Close the active SPP connection |
esp32_spp_status |
Get SPP connection status and remote address |
BLE / GATT
| Tool |
Description |
esp32_ble_enable |
Enable BLE radio |
esp32_ble_disable |
Disable BLE radio |
esp32_ble_advertise |
Start/stop BLE advertising |
esp32_ble_set_adv_data |
Configure advertisement data |
esp32_gatt_add_service |
Create GATT service |
esp32_gatt_add_characteristic |
Add characteristic to service |
esp32_gatt_set_value |
Set characteristic value |
esp32_gatt_notify |
Send notification to subscribers |
esp32_gatt_clear |
Remove all GATT services |
Events
| Tool |
Description |
esp32_get_events |
Retrieve event history |
esp32_wait_event |
Block until specific event occurs |
esp32_clear_events |
Clear event history |
E2E Testing
Run automated tests using Claude CLI in headless mode:
# Setup test environment
mkdir -p /tmp/bt-e2e-test && cd /tmp/bt-e2e-test
git init
# Create MCP config with both servers
cat > .mcp.json << 'EOF'
{
"mcpServers": {
"esp32": {
"type": "stdio",
"command": "uvx",
"args": ["mcbluetooth-esp32"],
"env": {"ESP32_SERIAL_PORT": "/dev/ttyUSB0"}
},
"bluez": {
"type": "stdio",
"command": "uvx",
"args": ["mcbluetooth"]
}
}
}
EOF
# Run test suite
PROMPT=$(cat tests/prompts/test-prompt-v5.md)
claude -p "$PROMPT" \
--mcp-config .mcp.json \
--allowedTools "mcp__esp32__*,mcp__bluez__*" \
--output-format json
Test Coverage (v5 - 76 tests)
| Phase |
Tests |
Coverage |
| ESP32 Connection |
1-4 |
connect, ping, info, status |
| BlueZ Adapter |
5-8 |
list, info, pairable, discoverable |
| Classic BT + SSP |
9-24 |
Full pairing workflow with auto-accept |
| BLE GATT Setup |
30-42 |
Battery Service, Environmental Sensing |
| HCI Capture |
43-55 |
btsnoop capture and analysis |
| GATT Operations |
56-63 |
read, write, notify, subscribe |
| Cleanup |
64-76 |
Adapter management, reset |
Recent Test Results
| Version |
Tests |
Result |
Duration |
| v4 |
71 |
66 PASS, 5 PARTIAL |
8.6 min |
| v5 |
76 |
76/76 PASS (100%) |
7.6 min |
Documentation
Development
# Install dev dependencies
uv sync --all-extras
# Run unit tests
uv run pytest tests/ -v --ignore=tests/integration
# Run integration tests (requires ESP32)
ESP32_SERIAL_PORT=/dev/ttyUSB0 uv run pytest tests/integration/ -v
# Lint
uv run ruff check src/
License
MIT