Device Context Protocol MCP server
Exposes physical device intents as MCP tools so an AI client can control microcontrollers like ESP32 boards.
Also known as device-context-protocol/dcp
- Stars
- 62
- Tools
- —
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- May 27, 2026
- License
- MIT
What is Device Context Protocol MCP?
Device Context Protocol exposes physical device intents as MCP tools so an AI client can control microcontrollers like ESP32 boards.
Summary generated from the project's README
About Device Context Protocol MCP server
Controlling a lamp or an ESP32 board with plain instructions from an AI client is the job DCP's MCP server takes on. A reference Bridge translates DCP intents into MCP tools, so a host such as Claude Desktop or an IDE assistant can call something like set_brightness at 50 percent instead of writing raw registers. It suits developers building small hardware projects who want an agent to drive a microcontroller directly.
The server runs locally through the pydcp package (the import name is dcp, installed with the mcp and serial extras); the bare dcp name on PyPI belongs to an unrelated project. You start it with dcp serve against a manifest, pointing at a simulator for a hardware-free demo or at a serial port, an MQTT broker and prefix, or a BLE address and service UUID for a real device. Wiring it into Claude Desktop means adding a dcp serve entry to the client config. Tokens are minted with dcp token keygen and dcp token mint, carrying capabilities and a time-to-live, and the Bridge checks them on every call.
The difference is scale and safety. Intents are named, carry units, and are idempotent by default; a call is 19 bytes against roughly 180 for the JSON-RPC equivalent, and the measured DCP layer is 27.6 KB of flash and 0.6 KB of RAM. The Bridge enforces range, type and unit checks and supports dry-run. Validated boards include ESP32-WROOM-32 and ESP32-S3 at 115200 baud.
What you can do with it
- 1Ask Claude to dim a lampHave an AI client set a simulated lamp to 60 percent brightness without any hardware attached.
- 2Drive a real board over UARTPoint the Bridge at a COM port so an assistant's intent calls reach an ESP32 board.
- 3Scope access with minted tokensIssue a short-lived token limited to lamp read and write before letting a client send intent calls.
- 4Reach a device over BLEConnect the Bridge to a Bluetooth device by address and service UUID so intents route over BLE.
- 5Add a new intent end to endEdit the manifest and add a C++ handler, then reflash so the client sees the new tool on restart.
Written from the project's README. Every use case is checked against a quote from it.
Configuration
DCP_SECRET
| Name | Description | |
|---|---|---|
| DCP_SECRET | — |
README
View on GitHubDCP — Device Context Protocol
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.
Contents
- Why DCP?
- Design principles
- Architecture
- Quickstart
- Add a feature in 5 steps
- Recipes — five ready-to-flash device skeletons
- Wire format · full SPEC.md
- Manifest
- Roadmap
- Design rationale: docs/RATIONALE.md — why not MCP-on-MCU, why not WoT, why not Matter.
Why DCP?
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.
Design principles
- 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.
Architecture

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.
Validated on real hardware
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 KBdesign 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.
Cross-compile clean across the ESP family (Xtensa + RISC-V + ESP8266)
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/ledcWriteon ESP32,analogWriteon 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
Manifest
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.
Wire format

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.
Adding a feature
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.
Quickstart
# 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.
Run as an MCP server
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
Capability tokens (HMAC-SHA256)
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).
What's not in v0.3 (intentional)
- 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)
Cite
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.
License
MIT.
Roadmap
- 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.1 release 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)
Device Context Protocol MCP questions
How do I run DCP as an MCP server?
Install pydcp with the mcp and serial extras, then run 'dcp serve' against a manifest. Adding --simulator uses an in-process fake device so you can demo without hardware.
Can I connect DCP to Claude Desktop?
Yes. Add the dcp serve command to your claude_desktop_config.json mcpServers section, then ask Claude to set the lamp brightness.
What transports does DCP support?
The Bridge supports serial/UART, MQTT, and BLE, as well as an in-process loopback simulator. The protocol itself is transport-agnostic.
How much memory does DCP use on an ESP32?
The DCP layer itself measures 27.6 KB of flash and 0.6 KB of RAM over a baseline empty sketch on an ESP32-WROOM-32.
How is access controlled with capability tokens?
Mint short-lived HMAC-SHA256 tokens scoped to capabilities and a TTL, then pass them to the Bridge, which verifies them on every call.