# Device Context Protocol – Bridge LLM Agents to Physical Devices

> Source: <https://github.com/device-context-protocol/dcp>
> Published: 2026-07-13 08:35:55+00:00

**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)
