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?Design principlesArchitectureQuickstartAdd a feature in 5 stepsRecipes — five ready-to-flash device skeletonsWire format· fullSPEC.mdManifestRoadmapDesign rationale: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%)
, notwrite_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 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.
See 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.
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
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
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 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) - 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) - 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(v0.3.1). Source bundle and rendered PDF also mirrored on the v0.3.1release page. - 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)