Skip to content

Architecture

import { Aside } from ‘@astrojs/starlight/components’;

Understanding mcbluetooth’s architecture helps you troubleshoot issues and extend functionality.

┌─────────────────────────────────────────────────────────────┐
│ MCP Client (Claude, etc.) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ mcbluetooth │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ MCP Tools │ │ MCP Resources│ │ Pairing Agent │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ D-Bus Clients (async) ││
│ │ ┌──────────────┐ ┌──────────────────────┐ ││
│ │ │ BlueZClient │ │ ObexClient │ ││
│ │ │ (system bus) │ │ (session bus) │ ││
│ │ └──────────────┘ └──────────────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ BlueZ │ │ obexd │ │ PipeWire/ │
│ (bluetoothd) │ │ (session) │ │ PulseAudio │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────┼───────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Linux Kernel │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Bluetooth │ │ btusb │ │ HCI driver │ │
│ │ subsystem │ │ module │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Bluetooth Hardware │
│ (USB dongle, built-in) │
└─────────────────────────────────────────────────────────────┘

The MCP server layer that exposes Bluetooth functionality:

ComponentPurpose
MCP Tools69 tools for Bluetooth operations
MCP ResourcesLive state queries via URIs
Pairing AgentHandles PIN/passkey negotiation
D-Bus ClientsAsync communication with BlueZ/obexd

The official Linux Bluetooth stack daemon:

  • Manages adapter hardware
  • Handles device discovery and pairing
  • Implements Bluetooth profiles (A2DP, HFP, etc.)
  • Manages custom profile registrations (HFP AG via ProfileManager1)
  • Exposes D-Bus API on system bus

OBEX protocol daemon for file transfer:

  • Runs per-user (session daemon)
  • Implements OPP, FTP, PBAP, MAP profiles
  • Exposes D-Bus API on session bus

Audio server integration:

  • Receives audio streams from BlueZ
  • Provides volume, mute, routing controls
  • Handles codec negotiation

mcbluetooth communicates with two D-Bus buses:

Service: org.bluez
Paths: /org/bluez
/org/bluez/hci0
/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF
Interfaces:
org.bluez.Adapter1 - Adapter control
org.bluez.Device1 - Device management
org.bluez.GattService1 - BLE services
org.bluez.GattCharacteristic1 - BLE characteristics
org.bluez.AgentManager1 - Pairing agent registration
org.bluez.ProfileManager1 - Custom profile registration (HFP AG)
Service: org.bluez.obex
Paths: /org/bluez/obex
/org/bluez/obex/client/session0
/org/bluez/obex/client/session0/transfer0
Interfaces:
org.bluez.obex.Client1 - Session management
org.bluez.obex.Session1 - Session properties
org.bluez.obex.ObjectPush1 - OPP file sending
org.bluez.obex.FileTransfer1 - FTP operations
org.bluez.obex.PhonebookAccess1 - PBAP
org.bluez.obex.MessageAccess1 - MAP

mcbluetooth uses dbus-fast for non-blocking D-Bus communication:

# Singleton pattern ensures one connection
class BlueZClient:
_instance = None
@classmethod
async def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
await cls._instance._connect()
return cls._instance

Benefits:

  • Multiple concurrent operations
  • No blocking the MCP event loop
  • Efficient resource usage

Tools are organized by functional area:

server.py
from mcbluetooth.tools import adapter, audio, ble, device, hfp, monitor, obex
def create_server():
mcp = FastMCP("mcbluetooth")
adapter.register_tools(mcp)
device.register_tools(mcp)
audio.register_tools(mcp)
hfp.register_tools(mcp)
ble.register_tools(mcp)
monitor.register_tools(mcp)
obex.register_tools(mcp)
return mcp

Each module follows the pattern:

tools/device.py
def register_tools(mcp: FastMCP) -> None:
@mcp.tool()
async def bt_connect(adapter: str, address: str) -> dict:
"""Connect to a paired device."""
client = await BlueZClient.get_instance()
return await client.connect_device(adapter, address)

D-Bus errors are translated to user-friendly messages:

D-Bus Errormcbluetooth Response
org.bluez.Error.Failed{"error": "Operation failed", ...}
org.bluez.Error.NotReady{"error": "Device not ready", ...}
org.bluez.Error.AuthenticationFailed{"error": "Authentication failed", ...}
org.freedesktop.DBus.Error.ServiceUnknown{"error": "BlueZ not running", ...}

mcbluetooth doesn’t cache BlueZ state — each query goes to D-Bus:

  • Ensures fresh data
  • Avoids stale state issues
  • BlueZ handles the caching

OBEX sessions are tracked locally:

_active_sessions: dict[str, dict] = {}
# session_id -> {path, address, target, created}
def generate_session_id(address: str, target: str) -> str:
"""Generate friendly ID: ftp_C87B235568E8"""
clean_addr = address.replace(":", "")
return f"{target}_{clean_addr}"

The pairing agent registers with BlueZ and handles callbacks:

1. Agent registers with AgentManager1
2. BlueZ calls agent methods for pairing events
3. Agent handles PIN/passkey based on pairing_mode
4. Results returned to BlueZ

Each tool call involves:

  1. MCP request parsing
  2. D-Bus method call (async)
  3. Response serialization

Typical latency: 1-10ms per call.

Discovery is resource-intensive:

  • bt_scan starts/stops discovery explicitly
  • Avoids continuous scanning
  • Timeout prevents runaway scans

OBEX transfers use polling for progress:

while True:
status = await get_transfer_status(transfer_path)
if status["status"] in ("complete", "error"):
return status
await asyncio.sleep(0.5)
LayerSecurity
MCPTrust boundary at MCP client
D-BusPolicyKit for privileged ops
BlueZPairing provides encryption
OBEXSession-based access