Skip to content

Latest commit

 

History

History
336 lines (256 loc) · 13.9 KB

File metadata and controls

336 lines (256 loc) · 13.9 KB

AgentGATT — Public BLE GATT Bridge Protocol for MCP

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


1. Goals and Non-Goals

1.1 Goals

  • 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.

1.2 Non-Goals

  • 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 stdio and Streamable HTTP / SSE.

2. Architecture

+------------------+              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.

3. GATT Layout

3.1 Service

Item Value
Name AgentGATT Service
Type Primary Service
UUID E2C56DB5-DFFB-48D2-B060-D0F5A71096E0

3.2 Characteristics

# 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-D0F5A71096E0 with 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.

3.3 Client Characteristic Configuration Descriptor (CCCD)

The Response characteristic MUST expose a CCCD. The device sends responses only after the Central has subscribed (CCCD value 0x0001 notifications, or 0x0002 indications).


4. Byte Framing (Transport Layer)

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

4.1 Reassembly Rules

Receivers maintain a small reassembly buffer per direction:

  1. Accumulate incoming bytes until at least 2 bytes are present.
  2. Read L from the first two bytes (little-endian).
  3. Keep accumulating until L + 2 bytes are present.
  4. Extract the JSON payload, then reset for the next message.
  • A write to Request may arrive in one operation (ATT Write / Prepare-Write long) or several Write Without Response operations. The peripheral MUST accumulate across operations using the rules above.
  • A response/notification on Response may arrive in several ATT notifications. The central MUST accumulate across them.
  • If a length prefix is malformed (L exceeds a device-defined maximum), the receiver MAY drop the connection or emit a transport error (see §7).

4.2 MTU and Fragmentation

  • The device SHOULD negotiate MTU (BLE 4.2+ Data Length Extension) to reduce fragmentation; a good minimum target is ATT_MTU = 247.
  • The Meta characteristic advertises the device's preferred message size limit (maxMessageSize). The Central MUST NOT send a payload larger than this.

5. Message Layer (JSON-RPC 2.0 / MCP)

The framed payload is a single, complete JSON object conforming to JSON-RPC 2.0.

AgentGATT uses MCP's message shapes unchanged:

5.1 Device → Host

// capability manifest (sent after initialize, on request)
{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "...", "capabilities": {...}, "serverInfo": {...} } }

// tools/list response — tools may carry an optional `group` category.
// The name SHOULD be dotted (domain.verb) so agents can organize a large,
// otherwise-flat tool list. See §5.4 for the naming convention.
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [
    { "name": "sensor.temp.read", "group": "sensor", "description": "...", "inputSchema": {...} }
] } }

// tools/call response
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "..." } ], "isError": false } }

// unsolicited notification (device-initiated, e.g. sensor event)
{ "jsonrpc": "2.0", "method": "notifications/message", "params": { "level": "info", "data": {...} } }

5.2 Host → Device

{ "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 } }

5.3 Batching

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.

5.4 Tool naming & grouping (informative)

MCP's tools/list is a flat array — it has no native grouping. AgentGATT organizes large command sets through two conventions:

  1. 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.
  2. group field (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/call resolution 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 than tools, keeping the tool list focused on actions. See §5.5.
  • Long-running tools SHOULD honor notifications/cancelled (see §6).

5.5 Splitting tools vs. resources (informative)

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.

5.6 Asynchronous tools & cancellation

tools/call may need to run for seconds (a motor move, a network scan). The reference core supports deferred completion:

  1. The tool's invoke returns the sentinel AGENTGATT_TOOL_PENDING and the core records the request without replying.
  2. The device finishes its work and calls agentgatt_respond(request_id, text, is_error), which emits the framed JSON-RPC result (correlated by id).
  3. If the host abandons the call, it sends notifications/cancelled { "requestId": id }. The core looks up the pending request and invokes the tool's optional cancel hook (cooperative cancellation — the device stops its own background work and may respond with a -32800 cancellation 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, cancel and agentgatt_respond MUST all run on the same task context that drives the BLE host loop (i.e. where agentgatt_on_request executes). A tool that does its real work on a background thread returns AGENTGATT_TOOL_PENDING, signals the main context when finished, and the main context calls agentgatt_respond (or agentgatt_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.


6. Session Lifecycle

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                          |
  1. Discover & identify — read Meta; verify protocol == "agentgatt".
  2. Subscribe — enable notifications on Response.
  3. Initialize — send MCP initialize to learn capabilities and version.
  4. Use — call tools, read resources, handle device notifications.
  5. Close — disconnect; the device frees per-connection state.

7. Error Handling

Two independent error spaces exist.

7.1 AgentGATT application errors

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)

7.2 Standard MCP / JSON-RPC errors

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

8. Concurrency and Ordering

  • The device SHOULD process Request messages 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.

9. Security Recommendations (informative)

  • Use LE Secure Connections + encryption (authentication_required / encryption_required GATT 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.

10. Reference Implementations

  • 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.