Concepts

March 27, 2026 · View on GitHub

How the BLE MCP server works, and how the pieces fit together.


How the agent interacts with devices

The server gives an AI agent (like Claude) a set of BLE tools over the MCP protocol. The agent uses these tools to talk to real hardware — scanning, connecting, reading, writing, and streaming notifications.

Everything is stateful: connections and subscriptions persist across tool calls. The agent doesn't have to re-connect between each operation.

┌─────────────┐    stdio/SSE/HTTP      ┌─────────────────┐       BLE        ┌──────────┐
│  AI Agent   │ ◄────────────────────► │  BLE MCP Server │ ◄──────────────► │  Device  │
│ (Claude etc)│   structured JSON      │  (this project) │   bleak/GATT     │          │
└─────────────┘                        └─────────────────┘                  └──────────┘

The server supports stdio (default), SSE, and Streamable HTTP transports. Each session gets isolated BLE state. The agent sees tools like ble_connect, ble_read, ble_subscribe. It calls them, gets structured JSON back, and reasons about what to do next.


Security model

Because this server controls real hardware and can execute code via plugins, all risky capabilities are opt-in and enforced by the server.

                          ┌──────────────────────┐
                          │   Default: read-only │
                          └──────────┬───────────┘

                    BLE_MCP_ALLOW_WRITES=true

                          ┌──────────▼───────────┐
                          │   Writes enabled     │
                          │   (all chars)        │
                          └──────────┬───────────┘

                    BLE_MCP_WRITE_ALLOWLIST=uuid1,uuid2

                          ┌──────────▼───────────┐
                          │   Writes restricted  │
                          │   (allowlisted UUIDs)│
                          └──────────────────────┘

Plugins follow the same pattern:

BLE_MCP_PLUGINSEffect
(unset)Plugins disabled — no loading, no discovery
allAll plugins in .ble_mcp/plugins/ are loaded
name1,name2Only named plugins are loaded

The agent cannot bypass these flags. It can only use the tools the server exposes, and the server enforces the policy.

Path containment is enforced for all filesystem operations:

  • Plugins must be inside .ble_mcp/plugins/
  • Specs must be inside the project directory (parent of .ble_mcp/)
  • Traces always write to .ble_mcp/traces/trace.jsonl (not configurable)

Protocol specs — teaching the agent about your device

Specs are markdown files that describe a BLE device's protocol: services, characteristics, commands, and multi-step flows.

.ble_mcp/
  specs/
    my-device.md      # protocol documentation

The agent reads specs to understand what a device can do. Without a spec, the agent can still discover services and read characteristics, but it won't know what the values mean or what commands to send.

How specs help the agent

Without spec:                         With spec:
  "I see service 0xAA00               "This is the SensorTag IR
   with characteristic 0xAA01.          temperature service. Char 0xAA01
   I don't know what it does."          returns 4 bytes: [objTemp, ambTemp]
                                        in 0.03125 °C units. Write 0x01
                                        to 0xAA02 to enable the sensor."

Creating a spec

Tell the agent about your device's protocol — paste a datasheet, a link to docs, or just describe the services and commands in chat. The agent will create the spec file, register it, and use it in future sessions.

You can also write specs by hand if you prefer. They're just markdown files with a small YAML header.

How the agent uses specs

After connecting to a device, the agent checks for registered specs, attaches a matching one, and references it throughout the session — looking up characteristic UUIDs, command formats, and multi-step flows as needed.

Specs are freeform markdown. The agent reads and reasons about them — there's no rigid schema to fight, so specs can evolve naturally with your protocol.

Beyond the agent

Specs aren't just for the agent — they're structured protocol documentation that lives in your repo. If you're designing a new BLE protocol, specs created during agent sessions become the foundation for official protocol docs. They capture what was discovered, tested, and verified through real device interaction.


Plugins — giving the agent shortcut tools

Plugins add device-specific tools to the server. Instead of the agent manually composing read/write sequences, a plugin provides high-level operations like sensortag.read_temp or ota.upload_firmware.

.ble_mcp/
  plugins/
    sensortag.py      # adds sensortag.* tools
    ota_dfu.py        # adds ota.* tools (works with any device supporting DFU)

What a plugin provides

TOOLS = [...]       # Tool definitions the agent can call
HANDLERS = {...}    # Implementation for each tool
META = {...}        # Optional: matching hints (service UUIDs, device name patterns)

How the agent uses plugins

After connecting to a device, the agent checks ble.plugin.list. Each plugin includes metadata that helps the agent decide if it fits:

{
  "name": "ota_dfu",
  "tools": ["ota.start", "ota.upload", "ota.status"],
  "meta": {
    "description": "OTA DFU over BLE",
    "service_uuids": ["adc710df-5a73-4810-9d11-63ae660a448b"]
  }
}

The agent reasons: "This device has service 1d14d6ee..., and the ota_dfu plugin matches that service. I'll use its tools."

AI-authored plugins

The agent can also create plugins. Using ble.plugin.template, it generates a skeleton, fills in the implementation based on the device spec, and saves it to .ble_mcp/plugins/. After a server restart (or hot-reload), the new tools are available. Review generated plugins before enabling them in sensitive environments.

This is the core loop: the agent explores a device, writes a plugin for it, and future sessions get shortcut tools.

Background tasks

Plugins can start background asyncio tasks for continuous operations — periodic scans, data collection loops, monitoring. The plugin template includes examples.

Tasks are registered with state.register_task(name, task) and visible via ble.tasks.list. The agent (or user) can cancel any task with ble.tasks.cancel. All tasks are automatically cancelled on server shutdown.

Plugin notifications

Plugins can send MCP log notifications to the client via state.on_log_cb(level, message):

if state.on_log_cb:
    import asyncio
    asyncio.get_running_loop().create_task(
        state.on_log_cb("info", "Temperature threshold exceeded: 31.5C")
    )

This allows background tasks to proactively alert the client about events. Whether the client acts on these depends on the MCP client implementation — MCP Inspector shows them in real time; Claude Code and Claude Desktop currently ignore them; custom MCP clients (like an edge agent) can receive and act on them.

Beyond the agent

Plugin code is real Python that talks to real hardware. It can serve as a starting point for standalone test scripts, CLI tools, or production libraries. The agent writes the first draft based on the device spec, and you refine it into whatever you need.


How specs and plugins connect

Specs and plugins serve different roles:

SpecPlugin
WhatDocumentationCode
PurposeTeach the agent what the device can doGive the agent shortcut tools
FormatFreeform markdownPython module
Required?No — agent can still discover and exploreNo — agent can use raw BLE tools
Bound toA connection (via ble.spec.attach)Global (all connections)

They work together:

                    ┌──────────────────┐
                    │  Protocol Spec   │──── "What can this device do?"
                    │  (markdown)      │     Agent reads and reasons
                    └────────┬─────────┘

                     agent reasons about
                     the spec, or creates

                    ┌────────▼─────────┐
                    │     Plugin       │──── "Shortcut tools for this device"
                    │  (Python module) │     Agent calls directly
                    └──────────────────┘

A plugin doesn't require a spec, and a spec doesn't require a plugin. But when both exist for a device, the agent gets the best of both: deep protocol knowledge from the spec, and fast operations from the plugin.

One plugin, many devices

A plugin doesn't have to be device-specific. A DFU plugin can work with any device that implements a particular service. The META dict advertises this:

META = {
    "service_uuids": ["1d14d6ee-..."],  # matches any device with this service
}

The agent matches by service UUID, not by device name.


The agent's decision flow

After connecting to a device, the agent follows this flow:

Connect to device


Check ble.spec.list ──── matching spec? ──── yes ──► ble.spec.attach
       │                                                    │
       │ no                                                 │
       ▼                                                    ▼
Check ble.plugin.list ◄─────────────────────── Check ble.plugin.list
       │                                                    │
       │                                                    ▼
       ▼                                          Present options:
  matching plugin? ─── yes ──► use plugin tools    • use plugin tools
       │                                           • follow spec manually
       │ no                                        • extend plugin
       ▼                                           • create new plugin
  Ask user / explore
  with raw BLE tools

The agent handles this automatically. The tool descriptions guide it through each step.