Architecture
What Problem Does This Architecture Solve?
Section titled “What Problem Does This Architecture Solve?”Traditional microcontroller toolchains require hardcoded integrations — every new sensor, actuator, or custom capability on your MCU requires manual client-side code changes. If you add a temperature sensor to your ESP32, you must update your AI agent’s tool definitions. If you swap out the sensor for a different model with a different API, you update the client again. This creates a brittle, high-maintenance relationship between firmware and AI agents.
MCP/U solves this by making the firmware self-describing. The microcontroller advertises its capabilities at runtime — what pins it has, what tools it supports, what parameters each tool accepts. The client never hardcodes tool names. Adding a new tool to firmware is automatic; the AI agent discovers it without any client changes.
This architecture is designed for:
- Zero-config tool addition — add firmware code, restart client, tools appear
- Multi-device management — one client manages ESP32, Arduino, sensors, all unified
- Transport flexibility — works over USB serial or WiFi TCP
- AI-first design — tools are discovered, not declared; schemas are generated, not written
System Overview
Section titled “System Overview”graph TD A["AI / LLM Host<br/>(Claude Desktop, Claude Code, Gemini CLI, OpenCode...)"] B["mcpu-client (npm: mcpu-client)<br/><br/>index.ts — MCP Server, dynamic tool reg.<br/>transport.ts — Serial / TCP abstraction<br/>device_manager.ts — Multi-device connection + discovery<br/>schema_builder.ts — JSON Schema to Zod converter"] C["ESP32 #1<br/>MCP-U lib<br/>GPIO 2, 5, 34"] D["ESP32 #2 (WiFi)<br/>MCP-U lib<br/>GPIO 2, 13, 36"]
A <-->|"MCP Protocol (stdio)"| B B -->|"Serial (UART)"| C B -->|"TCP socket"| DComponent Responsibilities
Section titled “Component Responsibilities”Firmware (MCP-U library)
Section titled “Firmware (MCP-U library)”- Listens on any Arduino
Streamfor newline-delimited JSON-RPC requests - Maintains a pin registry (type, name, description, capabilities, sampling rates)
- Dispatches to built-in handlers (
gpio_write,gpio_read,get_pin_buffer, etc.) - Dispatches to user-registered custom tools
- Sends
list_toolsdiscovery response with full JSON Schema for all tools
transport.ts
Section titled “transport.ts”SerialTransport— wrapsserialportlibrary,ReadlineParserfor\nframingTcpTransport— wraps Node.jsnet.Socket, manual line bufferingMockTransport— simulates a connected device for testing without physical hardware- All share: pending promise map, request ID counter, 5s timeout, disconnect event
device_manager.ts
Section titled “device_manager.ts”- Instantiates correct transport per device config
- Calls
get_info+list_toolson connect (600ms boot delay for ESP32 DTR reset) - Caches:
info,pins[],tools[]per device - Routes
call(device_id, method, params)to correct transport
schema_builder.ts
Section titled “schema_builder.ts”- Converts firmware
inputSchema(JSON Schema) → Zod shape - Maps:
integer → z.number().int(),boolean → z.boolean(),string → z.string(),number → z.number() - Handles
required→ optional fields - Flat schemas only (matches MCU RAM constraint)
memory/ (Buffered Pull Memory Subsystem)
Section titled “memory/ (Buffered Pull Memory Subsystem)”sqlite_memory_store.ts— Manages the SQLite database connection, schema, batch inserts, tool calls, and data retention cleanup.buffer_drain_poller.ts— Identifies pins withbuffer: truecapabilities and dynamically calculates safe polling intervals to drain MCU ring buffers before they overflow.buffer_expander.ts— Parses bulk buffer arrays from the MCU and infers timestamps for each sample.sqlite_readonly_adapter.ts+sql_guard.ts— Provides a safe, restricted interface allowing the LLM to query historical observations using onlySELECTorWITHSQL commands.
index.ts
Section titled “index.ts”- Loads device config:
SERIAL_PORTenv →DEVICESenv - Boots
DeviceManagerandMemory Subsystem - Registers static meta-tools
list_devices,sql_readonly_query, andmemory_status - Loops over all devices × all tools →
server.registerTool()dynamically - Registers MCP Resources:
mcu://devices,mcu://{device_id}/pins,mcu://cache/*
Discovery Sequence
Section titled “Discovery Sequence”sequenceDiagram participant Client participant Transport participant MCU
Client->>Transport: DeviceManager.connect_all() Transport->>MCU: SerialTransport.connect() (open port) Note over Transport,MCU: wait 600ms (MCU boot / DTR reset) Transport->>MCU: rpc.call("get_info") MCU-->>Transport: { device, version, platform } Transport->>MCU: rpc.call("list_tools") MCU-->>Transport: { tools[], pins[] } Note over Client: For each device x each tool:<br/>server.registerTool(name, zodSchema, handler) Note over Client: server.connect(StdioTransport) (ready for Claude)Data Flow — Single Tool Call
Section titled “Data Flow — Single Tool Call”sequenceDiagram participant Claude participant Index as index.ts handler participant DM as DeviceManager participant Transport as SerialTransport participant ESP32
Claude->>Index: gpio_write({ pin: 2, value: true }) Note over Index: strips device_id (if multi-device) Index->>DM: call("esp32-01", "gpio_write", { pin: 2, value: true }) DM->>Transport: call("gpio_write", { pin: 2, value: true }, timeout=5000) Transport->>ESP32: {"jsonrpc":"2.0","id":42,"method":"gpio_write","params":{"pin":2,"value":true}} Note over ESP32: McpDevice._dispatch()<br/>_handle_gpio_write()<br/>digitalWrite(2, HIGH) ESP32-->>Transport: {"jsonrpc":"2.0","id":42,"result":{...}} Note over Transport: resolves pending[42] Transport-->>Claude: resultDesign Rationale
Section titled “Design Rationale”| Decision | Rationale |
|---|---|
| UART-first transport | Lowest latency, no network stack, works when WiFi is unavailable |
Arduino Stream abstraction | One firmware API works for Serial and WiFiClient |
| JSON-RPC 2.0 | Standard protocol with error codes, ID tracking, tooling support |
| Self-describing firmware | Client never hardcodes tool names — adding firmware tools is automatic |
| Zod schemas from JSON Schema | MCP SDK requires Zod; firmware returns JSON Schema — thin bridge needed |
device_id as Zod literal | Prevents Claude from calling wrong device, no runtime lookup needed |
SERIAL_PORT env var | Simplest config for single-device use — one line to connect |
Security Model
Section titled “Security Model”MCP/U is designed for local, trusted environments. The protocol does NOT provide:
-
Authentication — Any client that can open the serial port or connect to the TCP port can call any tool. There is no username/password, token, or key exchange.
-
Encryption — All data is transmitted in plaintext. Serial traffic is visible to any process with port access; TCP traffic is visible on the network.
-
Transport encryption — TLS/SSL is not implemented. WiFi connections are unencrypted by default.
-
Authorization — All discovered tools are available to all clients. There is no per-tool or per-pin access control.
-
Firmware verification — There is no code signing or integrity check. Any client can flash or modify firmware.
Recommended Security Practices
Section titled “Recommended Security Practices”- Run MCP/U on isolated networks (not exposed to the internet)
- Use USB serial for highest security (physical access required)
- If using WiFi, use a separate VLAN or firewall rules
- Keep the serial port accessible only to the user running the client
- Do not leave serial monitors or debug tools open that could interfere
Comparison with Alternatives
Section titled “Comparison with Alternatives”MQTT is a pub/sub protocol designed for IoT cloud connectivity. It requires a broker (e.g., Mosquitto, AWS IoT), adds ~3KB library overhead, and is overkill for single-device local control. MCP/U is simpler: no broker, no subscription management, direct device-to-client communication.
HTTP REST
Section titled “HTTP REST”REST APIs on microcontrollers require more RAM for parsing, lack standardized error codes (each implementation varies), and typically require manual documentation of endpoints. MCP/U’s JSON-RPC 2.0 is more compact and self-documenting via list_tools.
gRPC requires Protocol Buffers, adds significant overhead (~50KB+ for the stack), and is impractical for AVR-class devices with 2KB RAM. MCP/U’s JSON-RPC over UART works on Uno-class boards.
| Feature | MCP/U | MQTT | HTTP REST | gRPC |
|---|---|---|---|---|
| RAM footprint | ~20KB | ~3KB | ~5KB | ~50KB+ |
| Works on AVR | Yes | Borderline | No | No |
| Self-describing | Yes | No | No | Partial |
| Transport | Serial/TCP | TCP | TCP | TCP |
| Protocol | JSON-RPC 2.0 | Pub/Sub | REST | Protobuf |
| Transport | Serial/TCP | TCP | TCP | TCP |
| Protocol | JSON-RPC 2.0 | Pub/Sub | REST | Protobuf |