Version 1.0 (Draft)
AgentGATT is an open, vendor-neutral protocol that carries Model Context Protocol (MCP) messages over Bluetooth Low Energy (BLE) GATT. It lets a plain Bluetooth-capable embedded device (a Peripheral) expose its sensors, actuators and logic as MCP tools / resources / prompts to any AI agent through an MCP-capable host (a Central, typically a phone, PC, gateway or SBC running an AgentGATT-to-MCP bridge).
- HTTP-free. The device never speaks TCP/IP. Only BLE GATT is required.
- Zero-config discovery. An agent host discovers the device and its full capability manifest over GATT alone.
- MCP-native. Messages are MCP messages (JSON-RPC 2.0). No private procedure layer on top.
- Chip-agnostic. Any BLE stack (NimBLE, Zephyr, BlueZ, SoftDevice, Silicon Labs, …) can implement the peripheral side.
- Versioned and stable. UUIDs and byte framing are fixed; changes bump the protocol version.
- AgentGATT does not define security/encryption — use BLE pairing, LE Secure Connections, or application-layer auth.
- It does not define the MCP content of tools; it only transports them.
- It does not replace the MCP specification. AgentGATT is an alternative
transport for MCP, beside
stdioandStreamable HTTP/ SSE.
+------------------+ BLE GATT +-----------------------+
| Embedded device | <==============================> | Host / Gateway |
| (Peripheral) | AgentGATT Service + 3 chars | (Central) |
| | | |
| AgentGATT core | | AgentGATT-to-MCP |
| + tool callbacks| | bridge |
+------------------+ | | |
| v |
| MCP server transport|
| (stdio / HTTP) |
+-----------------------+
|
v
LLM / Agent application
- The Peripheral advertises and owns the AgentGATT service.
- The Central scans, connects, subscribes, and translates each MCP request into GATT writes and each GATT notification into an MCP response.
| Item | Value |
|---|---|
| Name | AgentGATT Service |
| Type | Primary Service |
| UUID | E2C56DB5-DFFB-48D2-B060-D0F5A71096E0 |
| # | Name | UUID | Properties | Direction | Purpose |
|---|---|---|---|---|---|
| 1 | Meta | E2C56DB5-DFFB-48D2-B060-D0F5A71096E1 |
Read, Notify | device → host | Protocol info, device identity (static, small) |
| 2 | Request | E2C56DB5-DFFB-48D2-B060-D0F5A71096E2 |
Write Without Response, Write | host → device | JSON-RPC requests + client notifications |
| 3 | Response | E2C56DB5-DFFB-48D2-B060-D0F5A71096E3 |
Notify, Read | device → host | JSON-RPC responses + device-initiated notifications |
The three UUIDs share the base
E2C56DB5-DFFB-48D2-B060-D0F5A71096E0with the last byte incremented (E1/E2/E3). These identifiers are part of the protocol. They MUST NOT be changed without bumping the protocol version.Authoritative source: the machine-readable identifier registry (
registry/entries/agentgatt-v1.json) is the single source of truth for these UUIDs, including their on-air (little-endian) byte order. The table above is informational; when in doubt, follow the registry.
The Response characteristic MUST expose a CCCD. The device sends responses
only after the Central has subscribed (CCCD value 0x0001 notifications, or
0x0002 indications).
All AgentGATT messages — in both directions — use the same framing. Because
an ATT notification/write carries only MTU - 3 payload bytes, a single MCP
message may span multiple GATT operations.
Message := LengthPrefix ++ Payload
LengthPrefix := uint16 little-endian, length of Payload in bytes (0..65535)
Payload := UTF-8 JSON (see §5)
Example (5-byte payload hello):
05 00 68 65 6C 6C 6F
Receivers maintain a small reassembly buffer per direction:
- Accumulate incoming bytes until at least 2 bytes are present.
- Read
Lfrom the first two bytes (little-endian). - Keep accumulating until
L + 2bytes are present. - Extract the JSON payload, then reset for the next message.
- A write to
Requestmay arrive in one operation (ATT Write / Prepare-Write long) or severalWrite Without Responseoperations. The peripheral MUST accumulate across operations using the rules above. - A response/notification on
Responsemay arrive in several ATT notifications. The central MUST accumulate across them. - If a length prefix is malformed (
Lexceeds a device-defined maximum), the receiver MAY drop the connection or emit a transport error (see §7).
- The device SHOULD negotiate MTU (BLE 4.2+ Data Length Extension) to reduce
fragmentation; a good minimum target is
ATT_MTU = 247. - The
Metacharacteristic advertises the device's preferred message size limit (maxMessageSize). The Central MUST NOT send a payload larger than this.
The framed payload is a single, complete JSON object conforming to JSON-RPC 2.0.
AgentGATT uses MCP's message shapes unchanged:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "...", "capabilities": {}, "clientInfo": {...} } }
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} }
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "read_sensor", "arguments": { "id": 0 } } }
{ "jsonrpc": "2.0", "method": "notifications/cancelled", "params": { "requestId": 3 } }Batch arrays are permitted but NOT recommended over GATT (large payloads, single notification). Implementations SHOULD send one JSON-RPC message per framed payload. Centers MAY still accept arrays.
MCP's tools/list is a flat array — it has no native grouping. AgentGATT
organizes large command sets through two conventions:
- Dotted tool names (
domain.subdomain.verb) carry the hierarchy in the name itself, e.g.sensor.temp.read,motor.step.move,sys.reboot. Any MCP client can group or fuzzy-match by prefix. groupfield (optional) explicitly tags each tool with a category (sensor,motor,sys, …) so clients can bucket it without parsing the name.
Implementation notes (reference core):
- The device core indexes tools in an FNV-1a open-addressing hash table
(built once at init) so
tools/callresolution is O(1) regardless of tool count. On allocation failure it falls back to a linear scan. - Read-only state access SHOULD be modeled as MCP resources
(
resources/list,resources/read) rather thantools, keeping the tool list focused on actions. See §5.5. - Long-running tools SHOULD honor
notifications/cancelled(see §6).
A large embedded API is usually a mix of actions (do something) and reads (observe state). MCP distinguishes these two; AgentGATT recommends modeling them accordingly to keep the tool list small and give clients cache/subscribe semantics:
| Intention | MCP mechanism | Message methods | AgentGATT example |
|---|---|---|---|
| Perform an action | tools |
tools/list, tools/call |
motor.step.move, sys.reboot |
| Read state | resources |
resources/list, resources/read |
agentgatt:///sensor/temperature, agentgatt:///device/info |
The reference core implements both tools and resources. The
initialize response advertises capabilities.resources.
tools/call may need to run for seconds (a motor move, a network scan). The
reference core supports deferred completion:
- The tool's
invokereturns the sentinelAGENTGATT_TOOL_PENDINGand the core records the request without replying. - The device finishes its work and calls
agentgatt_respond(request_id, text, is_error), which emits the framed JSON-RPC result (correlated byid). - If the host abandons the call, it sends
notifications/cancelled { "requestId": id }. The core looks up the pending request and invokes the tool's optionalcancelhook (cooperative cancellation — the device stops its own background work and may respond with a-32800cancellation error).
Cancellation is cooperative: the core never preempts a running tool; it only delivers a "please stop" signal. A device that cannot cancel still completes the call normally.
Threading (informative). The reference core is single-threaded and lock-free.
invoke,cancelandagentgatt_respondMUST all run on the same task context that drives the BLE host loop (i.e. whereagentgatt_on_requestexecutes). A tool that does its real work on a background thread returnsAGENTGATT_TOOL_PENDING, signals the main context when finished, and the main context callsagentgatt_respond(oragentgatt_notify). This contract keeps the core mutex-free and small enough for RTOS/bare-metal targets. Implementations with a heavier concurrency model may add their own serialization around these entry points.
Central Peripheral
| scan / connect |
| read Meta (Char#1) |
|<--------- {protocol, version ...} -|
| subscribe CCCD (Char#3) |
| write "initialize" (Char#2) --->|
|<--------- "initialize" result (Char#3)
| write "tools/list" (Char#2) --->|
|<--------- "tools list" (Char#3) |
| write "tools/call" (Char#2) --->|
|<--------- "tools/call" result (Char#3)
| ... |
| disconnect |
- Discover & identify — read
Meta; verifyprotocol == "agentgatt". - Subscribe — enable notifications on
Response. - Initialize — send MCP
initializeto learn capabilities and version. - Use — call tools, read resources, handle device notifications.
- Close — disconnect; the device frees per-connection state.
Two independent error spaces exist.
AgentGATT reserves the JSON-RPC 2.0 application-error range -32000..-32099.
The authoritative list is the registry (registry/entries/agentgatt-v1.json,
kind: "error-code"); the table below is informational.
| Code | Constant | Meaning |
|---|---|---|
-32000 |
AGENTGATT_ERR_FRAME_TOO_LARGE |
Length prefix out of range |
-32001 |
AGENTGATT_ERR_INVALID_JSON |
Payload is not valid UTF-8 JSON |
-32002 |
AGENTGATT_ERR_TOOL_FAILED |
Tool/action execution failed |
-32003 |
AGENTGATT_ERR_RESOURCE_NOT_FOUND |
Resource URI not found |
-32004 |
AGENTGATT_ERR_PENDING_FULL |
Too many pending async requests |
-32800 |
AGENTGATT_ERR_CANCELLED |
Request cancelled (MCP standard) |
Standard codes pass through unchanged and are not redefined by AgentGATT:
| Code | Meaning |
|---|---|
-32700 |
Parse error |
-32600 |
Invalid request |
-32601 |
Method not found |
-32602 |
Invalid params |
-32603 |
Internal error |
- The device SHOULD process
Requestmessages in order of receipt. - Responses MAY be sent out of order for slow tool calls; each response carries
the original
id. - The device MAY emit unsolicited notifications (
notifications/message) at any time while subscribed.
- Use LE Secure Connections + encryption (
authentication_required/encryption_requiredGATT permissions on Request/Response in production). - Add application-level pairing/PIN if the link must be user-confirmed.
- The
maxMessageSize/ manifest SHOULD NOT be trusted blindly; the Central MUST bound its own buffers.
- Peripheral (C) — device-side library + NimBLE example (this repository).
- Central / Bridge (C) — MCP stdio bridge over BlueZ/NimBLE (this repository).
- Bindings for other BLE stacks (Zephyr, Arduino, ESP-IDF) are welcome.