Device Context Protocol – Bridge LLM Agents to Physical Devices A new open protocol, Device Context Protocol (DCP), enables large language model agents to safely control physical devices including low-cost microcontrollers. DCP uses a compact wire format and a Bridge process for safety enforcement, with reference firmware validated on ESP32 hardware. The protocol is designed to complement the Model Context Protocol (MCP) by translating DCP to MCP for zero-configuration use with existing LLM hosts. Status: Draft v0.3 — May 2026 · Hardware-validated on ESP32-WROOM-32 A protocol that lets LLM agents safely control physical devices, down to dollar-class microcontrollers. Intent-level, transport-agnostic, capability-scoped. Compact wire format sub-50-byte frames . Self-contained firmware: under 1 KB of RAM, ~28 KB of flash. Complementary to MCP — a reference Bridge translates DCP ↔ MCP so any MCP host Claude Desktop, Claude Code, IDE assistants works zero-config. Why DCP? why-dcp Design principles design-principles Architecture architecture Quickstart quickstart Add a feature in 5 steps /device-context-protocol/dcp/blob/main/docs/ADDING FEATURES.md Recipes — five ready-to-flash device skeletons /device-context-protocol/dcp/blob/main/docs/RECIPES.md Wire format wire-format · full SPEC.md /device-context-protocol/dcp/blob/main/SPEC.md Manifest manifest Roadmap roadmap Design rationale: docs/RATIONALE.md /device-context-protocol/dcp/blob/main/docs/RATIONALE.md — why not MCP-on-MCU, why not WoT, why not Matter. MCP is excellent for SaaS tools, but assumes JSON-RPC over WebSocket and runtime tool discovery. On an MCU with 32 KB of RAM, that's a non-starter. DCP keeps MCP's mental model manifest + tool calls but: - compiles to a compact CBOR wire format - uses a static intent table no runtime negotiation - moves safety enforcement to a Bridge process A reference Bridge translates DCP ↔ MCP , so any MCP-compatible LLM works out of the box. DCP is the last mile to physical hardware. Why this matters in one chart: the protocol's schema decides how many hallucinated or adversarial calls are stopped before any byte reaches a device. DCP catches all six categories at the wire layer; the others catch what their existing schema happens to cover. Intent, not register. set brightness 50% , not write pwm pin=5, duty=128 . Units in the protocol. Every number declares a unit. No ambiguity. Static intent table. Manifest known at compile time; runtime is pure binary. Safety lives in the Bridge. Devices trust the Bridge; LLMs never see raw GPIO. Idempotent by default. Non-idempotent intents must declare themselves. Transport-agnostic. UART, BLE, MQTT, USB-CDC, WebSocket — one frame. The Bridge is the sole trust boundary. On every call it issues and verifies capability tokens, enforces range/type/unit checks from the manifest, and supports dry-run as a wire-format primitive. Devices remain simple enough to fit on commodity microcontrollers; everything the LLM is allowed to do is enforced before any byte traverses the device boundary. As of v0.3 the reference firmware is measured-validated on two physical boards — an ESP32-WROOM-32 dev board over CH340 USB-Serial, and an ESP32-S3 LILYGO T-Panel S3 over the S3's native USB-Serial/JTAG — both at 115 200 baud: - 13/13 round-trip tests pass on each board tools/test uart roundtrip.py - 88/88 Python unit & conformance tests pass - Full lamp firmware: 295 KB flash, 22.7 KB globals on WROOM-32 — most of which is the Arduino-ESP32 runtime + FreeRTOS, not DCP The DCP layer itself measures 27.6 KB of flash and 0.6 KB of RAM over a baseline empty sketch — reproduce with docs/paper/figures/measure footprint.py . The flash figure is over the original <16 KB design target set before on-device HMAC was added ; the RAM figure is well under it.- The S3 run also exercises DCP over a native-USB CDC link rather than a USB-UART bridge chip — same firmware, no transport-specific code Static RAM is the scarce resource on an MCU. The DCP layer's measured 0.6 KB of RAM sits two orders of magnitude under IoT-MCP's reported 74 KB peak memory. DCP's flash cost 27.6 KB, measured is not plotted — IoT-MCP does not report a comparable flash figure. See docs/RATIONALE.md §7 /device-context-protocol/dcp/blob/main/docs/RATIONALE.md for what the hardware validation does and does not prove. The reference firmware is portable by design Arduino Stream + a software SHA-256, no SoC-specific code paths in DCP.{h,cpp} . It cross-compiles for every current ESP32 variant and for ESP8266; two of those targets are also runtime-validated on real boards, the rest are build-validated pending hardware on the bench: | Target | ISA | Flash lamp+blink | Globals | Status | |---|---|---|---|---| | ESP32-WROOM-32 | Xtensa LX6 baseline | 294 KB | 22.7 KB | runtime ✓ | | ESP32-S3 T-Panel | Xtensa LX7 | 322 KB | 22.7 KB | runtime ✓ native USB | | ESP32-C3 | RV32IMC | 289 KB | 13.4 KB | builds ✓ | | ESP32-C6 | RV32IMAC + HW-crypto | 266 KB | 14.0 KB | builds ✓ | | ESP32-H2 | RV32IMAC + 802.15.4 | 292 KB | 14.0 KB | builds ✓ | | ESP32-P4 | RV32IMAFC dual-core | 326 KB | 22.0 KB | builds ✓ | | ESP8266 NodeMCU | Xtensa LX106 legacy | 242 KB | 28.9 KB | builds ✓ | All builds use Arduino-ESP32 core 3.3.8 / Arduino-ESP8266 core 3.x - the same firmware/esp32/ library. The sketch picks PWM API at compile time ledcAttach / ledcWrite on ESP32, analogWrite on ESP8266 ; the protocol layer itself has no ifdef . Reproduce with: arduino-cli compile --clean --fqbn esp32:esp32:esp32c3 \ --library firmware/esp32 firmware/esp32/examples/lamp arduino-cli compile --clean --fqbn esp8266:esp8266:nodemcuv2 \ --library firmware/esp32 firmware/esp32/examples/lamp dcp: 0.3 device: id: lamp-kitchen-01 model: smart lamp v1 vendor: example.dev intents: - name: set brightness params: level: { type: float, unit: percent, range: 0, 100 } fade: { type: duration, unit: ms, default: 0 } capability: lamp.write idempotent: true dry run: true - name: read brightness returns: { type: float, unit: percent } capability: lamp.read events: - name: motion detected payload: confidence: { type: float, unit: ratio, range: 0, 1 } capability: lamp.read intent id = crc16 name — manifests and firmware stay in sync without coordination. A frame is a 6-byte fixed header + CBOR payload + an optional 16-byte truncated HMAC-SHA256. Header fields: | field | meaning | |---|---| ver | 1 in v0.3 | kind | 0x01 call · 0x02 reply · 0x03 event · 0x04 error · 0x81 dry-run | seq | client-chosen, echoed in reply | intent id | CRC-16/CCITT of intent name | cbor | CBOR map: params / return / event payload / error | Reply status codes: ok , denied , range , busy , unknown intent , capability required . A typical set brightness 50 call is 19 bytes on the wire; the MCP JSON-RPC equivalent is approximately 180 bytes. The full normative spec lives at SPEC.md /device-context-protocol/dcp/blob/main/SPEC.md . See docs/ADDING FEATURES.md /device-context-protocol/dcp/blob/main/docs/ADDING FEATURES.md for the full 5-step loop with a worked blink times, period example. The short version: edit the manifest, add a C++ handler + binding, recompile, flash, restart the MCP server — the LLM picks up the new tool automatically. The Bridge needs no code change. As a user — install from PyPI: pip install "pydcp mcp,serial " or mcp,serial,mqtt,ble for all transports dcp inspect examples/lamp manifest.yaml parsed manifest summary dcp serve examples/lamp manifest.yaml --simulator As a contributor — editable install from source: git clone https://github.com/device-context-protocol/dcp.git cd dcp pip install -e ". mcp,serial,mqtt,ble,dev " pytest all 88 tests python examples/lamp demo.py in-process bridge ↔ fake lamp The PyPI package is named pydcp the bare dcp is squatted by an unrelated package . The import name is dcp . The protocol name is DCP. The reference Bridge ships an MCP server that exposes each DCP intent as an MCP tool. With --simulator it spins up an in-process fake device, so you can demo with no hardware. dcp serve examples/lamp manifest.yaml --simulator no hardware dcp serve examples/lamp manifest.yaml --serial COM3 real ESP32 over UART dcp serve examples/lamp manifest.yaml --mqtt broker.lan:1883 \ MQTT --mqtt-prefix dcp/lamp-kitchen dcp serve examples/lamp manifest.yaml --ble AA:BB:CC:DD:EE:FF \ BLE --ble-service 12345678-1234-5678-1234-567812345678 For multi-tenant or scoped access, mint short-lived HMAC tokens and pass them to the Bridge: export DCP SECRET=$ dcp token keygen dcp token mint --caps lamp.write,lamp.read --ttl 3600 eyJjYXBzIjpb...sig Tokens are verified by the Bridge on every call. The device sees only already-authorized frames. Devices themselves do not verify signatures in v0.2 — that requires on-device HMAC, which is on the roadmap. To wire it into Claude Desktop , add this to your claude desktop config.json : { "mcpServers": { "smart-lamp": { "command": "dcp", "args": "serve", "C:/path/to/protocol/examples/lamp manifest.yaml", "--simulator" } } } Then ask Claude "set the lamp to 60% brightness" . The call flow: Claude ─MCP─▶ dcp serve ─Bridge─▶ Loopback ─DCP wire─▶ GenericSimulator For production use, replace GenericSimulator with a real transport UART / MQTT / BLE — coming next . - Multi-device atomic transactions - Firmware OTA - Mesh routing use Thread / Zigbee underneath if you need it - LLM-side authentication delegated to the MCP host's session model - Native CAN FD frames ESP32-S3 TWAI is classic CAN; v0.4 ESP32-P4 port enables true CAN FD If you use DCP in academic work, please cite the arXiv preprint: @misc{yang2026dcp, title = {Device Context Protocol: A Compact, Safety-First Architecture for LLM-Driven Control of Constrained Devices}, author = {Yang, Dongxu}, year = {2026}, eprint = {2605.26159}, archivePrefix= {arXiv}, primaryClass = {cs.NI}, url = {https://arxiv.org/abs/2605.26159}, } A machine-readable CITATION.cff /device-context-protocol/dcp/blob/main/CITATION.cff is also provided — GitHub renders a "Cite this repository" button in the sidebar. MIT. - Wire format + manifest parser - Reference Python Bridge with loopback transport - Lamp example - MCP server wrapper + CLI dcp serve - Generic in-process device simulator - UART transport COBS framing + CRC-16 - ESP32 reference firmware Arduino-compatible C++ - Design rationale docs/RATIONALE.md /device-context-protocol/dcp/blob/main/docs/RATIONALE.md - CI GitHub Actions, Linux + Windows, py 3.11–3.13 - MQTT transport - HMAC-SHA256 capability tokens Bridge-side enforcement - Manifest compiler: dcp codegen YAML → C header - Compile-time DCP ID name macro in firmware - BLE GATT transport bleak - Release prep: CONTRIBUTING / CHANGELOG / CoC / SECURITY / issue templates - On-device HMAC verification per-frame signatures, ESP32 firmware - ESP32 BLE peripheral example NimBLE-Arduino - Conformance test suite golden frames, language-neutral YAML - Codegen --stubs : emits handler signatures + binding table - Quickstart video script docs/QUICKSTART VIDEO.md /device-context-protocol/dcp/blob/main/docs/QUICKSTART VIDEO.md - Real-hardware validation on two boards ESP32-WROOM-32 over CH340, ESP32-S3 / T-Panel over native USB , 13/13 round-trips each - Cross-compile clean on ESP32 RISC-V family C3, C6, H2, P4 and ESP8266 - Public repo at device-context-protocol/dcp v0.3.0 released - PyPI release pip install pydcp , latest v0.3.1 - LLM-driven hallucination-rejection benchmark: 675 tool calls across 5 LLMs / 4 vendors, prompt-injection category instantiated from AgentDojo's attack templates. DCP catches 100% of capability- escalation and 78% of prompt-injection attempts vs 0–1% for MCP/ IoT-MCP. See tools/gen llm corpus.py + tools/bench hallucination empirical.py . - DCP vs IoT-MCP wire-latency A/B on identical ESP32-S3 hardware: 15.60 ms vs 15.59 ms median, within 5 µs. See firmware/esp32/examples/iotmcp echo/ + tools/bench latency iotmcp.py . - arXiv preprint published: arXiv:2605.26159 https://arxiv.org/abs/2605.26159 v0.3.1 . Source bundle and rendered PDF also mirrored on the v0.3.1 release page https://github.com/device-context-protocol/dcp/releases/tag/v0.3.1 . - T-Panel S3 + CAN bus demo firmware ready, awaiting hardware - ESP32-P4 port for native CAN FD - Multi-MCU footprint matrix nRF52840, Cortex-M0+, RP2040