An open, vendor-neutral protocol that carries MCP (Model Context Protocol) messages over BLE GATT — so traditional Bluetooth-only embedded devices (ESP32, nRF52, …) can be used directly by AI agents.
AgentGATT turns a BLE Peripheral (the embedded device) into an MCP server, and a BLE Central (phone / PC / SBC gateway) into an MCP client bridge.
[ embedded device ] -- BLE GATT --> [ host/gateway ] --> [ LLM / Agent ]
(Peripheral) AgentGATT Service (Central) MCP client
This repository contains the public protocol specification and a reference implementation in C (chip-agnostic, with a NimBLE peripheral example and a BlueZ host bridge). It is intentionally small and portable.
- ✅ Protocol spec (v1.0 draft) —
docs/PROTOCOL.md - ✅ Identifier registry (UUIDs + error codes + validator) —
registry/ - ✅ C core library (framing + MCP dispatch) —
src/agentgatt_core.c - ✅ HAL abstraction —
include/agentgatt/hal.h - ✅ NimBLE peripheral HAL —
src/nimble_hal.c,examples/example_peripheral.c - ✅ Zephyr native BT peripheral HAL —
src/zephyr_hal.c - ✅ RT-Thread (NimBLE package) boot glue —
examples/example_rtt_startup.c - ✅ Host-side Central bridge —
src/central_bridge.c - ✅ BlueZ GDBus Central backend (discovery/connect/GATT/subscribe) —
src/host/bluez_hal.c - ✅ Smoke tests + conformance golden-byte vectors —
tests/ - ✅ Device-side HAL porting guide + template —
docs/HAL_PORTING.md,src/hal_template.c
| MCP method | Direction | Status |
|---|---|---|
initialize |
host → device | ✅ (advertises tools + resources) |
tools/list / tools/call |
host → device | ✅ (dotted names + group + O(1) lookup) |
resources/list / resources/read |
host → device | ✅ (read-only state) |
notifications/cancelled |
host → device | ✅ (cooperative async cancel) |
notifications/message |
device → host | ✅ (unsolicited) |
notifications/initialized |
host → device | ✅ (no response) |
Read docs/PROTOCOL.md. The essentials:
| Item | Value |
|---|---|
| Service | E2C56DB5-DFFB-48D2-B060-D0F5A71096E0 |
| Meta (read+notify) | …96E1 — protocol info + limits |
| Request (write) | …96E2 — host → device (JSON-RPC) |
| Response (notify) | …96E3 — device → host (JSON-RPC) |
| Framing | uint16 LE length + UTF-8 JSON |
The portable core compiles without any BLE stack (useful for CI/sanity checks):
cmake -S . -B build -DAGENTGATT_BUILD_TESTS=ON
cmake --build build
ctest --test-dir buildAdd these sources to your NimBLE target:
src/agentgatt_core.csrc/nimble_hal.cexamples/example_peripheral.c
call agentgatt_example_init() after nimble_port_init() + controller init,
then start the NimBLE host task loop.
cmake -S . -B build -DAGENTGATT_BUILD_BRIDGE=ON
cmake --build build
# MCP stdio <-> GATT bridge over BlueZ (Linux)
./build/agentgatt-bridgeThe bridge speaks MCP on stdin/stdout (MCP stdio transport) and GATT over BLE:
it scans for the AgentGATT service UUID, connects, subscribes to notifications,
and forwards JSON-RPC in both directions. Requires a Linux system with BlueZ
and pkg-config for gio-2.0. It can be plugged directly into an MCP client
by registering it as an MCP stdio server.
python3 tools/validate_registry.py # validates identifier registry
cmake -S . -B build -DAGENTGATT_BUILD_TESTS=ON
cmake --build build
ctest --test-dir build # smoke + conformance tests.
├── docs/
│ ├── PROTOCOL.md # public protocol specification
│ └── HAL_PORTING.md # device-side HAL porting guide
├── registry/ # identifier registry (UUIDs + validator)
│ ├── README.md
│ ├── schema/registry-schema.json
│ └── entries/agentgatt-v1.json
├── tools/
│ └── validate_registry.py # registry JSON-Schema + byte-order check
├── include/agentgatt/
│ ├── protocol.h # UUIDs, version, framing, limits
│ ├── hal.h # BLE HAL abstraction (library boundary)
│ ├── core.h # device-side service core API
│ └── central_hal.h # host-side bridge HAL
├── src/
│ ├── agentgatt_core.c # framed JSON + MCP method dispatch
│ ├── nimble_hal.c # NimBLE peripheral HAL (NimBLE/ESP-IDF/RTT)
│ ├── zephyr_hal.c # Zephyr native BT peripheral HAL
│ ├── hal_template.c # copy-to-start porting template (any stack)
│ ├── central_bridge.c # host-side MCP stdio <-> GATT bridge
│ └── host/
│ └── bluez_hal.c # BlueZ Central HAL (GDBus, complete)
├── examples/
│ ├── example_peripheral.c # full device-side example
│ └── example_rtt_startup.c # RT-Thread NimBLE boot glue
├── tests/
│ ├── smoke.c # framing + dispatch + async + escape tests
│ ├── conformance.c # golden-byte wire-format vectors
│ └── conformance/README.md # vector format + usage
└── CMakeLists.txt
- Framing — every message is
uint16 LE length+ UTF-8 JSON; a frame is assembled atomically before the HAL sends it, so a torn frame (prefix without payload) can never reach the wire. - Tool lookup — the core indexes tools in an FNV-1a open-addressing hash
table, so
tools/callresolution is O(1) regardless of tool count. - Grouping — tools use dotted names (
sensor.temp.read) and an optionalgroupfield; read-only state is modeled as MCPresources, not tools. - Async + cancel — a tool may return
AGENTGATT_TOOL_PENDINGand finish later viaagentgatt_respond();notifications/cancelledtriggers the tool's optionalcancelhook (cooperative, single-threaded). - Threading — the core is single-threaded and lock-free.
invoke/cancel/agentgatt_respondmust run on the same context asagentgatt_on_request.
Start from docs/HAL_PORTING.md (step-by-step guide) and src/hal_template.c
(copy-to-start skeleton). Implement the two interfaces:
agentgatt_hal_api+agentgatt_hal_callbacks(device side) —include/agentgatt/hal.hcentral_hal_*(host side) —include/agentgatt/central_hal.h
Then wire GATT events into agentgatt_on_request / agentgatt_on_subscribed /
agentgatt_on_unsubscribed / agentgatt_on_disconnected (device), and drive
central_hal_write_request / notification-forwarding (host).
MIT. See LICENSE.
The protocol itself is an open specification: implement it freely in any product, commercial or otherwise, without licensing fees.