Skip to content

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

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"| D

  • Listens on any Arduino Stream for 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_tools discovery response with full JSON Schema for all tools
  • SerialTransport — wraps serialport library, ReadlineParser for \n framing
  • TcpTransport — wraps Node.js net.Socket, manual line buffering
  • MockTransport — simulates a connected device for testing without physical hardware
  • All share: pending promise map, request ID counter, 5s timeout, disconnect event
  • Instantiates correct transport per device config
  • Calls get_info + list_tools on connect (600ms boot delay for ESP32 DTR reset)
  • Caches: info, pins[], tools[] per device
  • Routes call(device_id, method, params) to correct transport
  • 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)
  • sqlite_memory_store.ts — Manages the SQLite database connection, schema, batch inserts, tool calls, and data retention cleanup.
  • buffer_drain_poller.ts — Identifies pins with buffer: true capabilities 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 only SELECT or WITH SQL commands.
  • Loads device config: SERIAL_PORT env → DEVICES env
  • Boots DeviceManager and Memory Subsystem
  • Registers static meta-tools list_devices, sql_readonly_query, and memory_status
  • Loops over all devices × all tools → server.registerTool() dynamically
  • Registers MCP Resources: mcu://devices, mcu://{device_id}/pins, mcu://cache/*

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)

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: result

DecisionRationale
UART-first transportLowest latency, no network stack, works when WiFi is unavailable
Arduino Stream abstractionOne firmware API works for Serial and WiFiClient
JSON-RPC 2.0Standard protocol with error codes, ID tracking, tooling support
Self-describing firmwareClient never hardcodes tool names — adding firmware tools is automatic
Zod schemas from JSON SchemaMCP SDK requires Zod; firmware returns JSON Schema — thin bridge needed
device_id as Zod literalPrevents Claude from calling wrong device, no runtime lookup needed
SERIAL_PORT env varSimplest config for single-device use — one line to connect

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.

  • 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

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.

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.

FeatureMCP/UMQTTHTTP RESTgRPC
RAM footprint~20KB~3KB~5KB~50KB+
Works on AVRYesBorderlineNoNo
Self-describingYesNoNoPartial
TransportSerial/TCPTCPTCPTCP
ProtocolJSON-RPC 2.0Pub/SubRESTProtobuf
TransportSerial/TCPTCPTCPTCP
ProtocolJSON-RPC 2.0Pub/SubRESTProtobuf