Add MCP resources, logging, and progress reporting
Resources (dynamic state queries):
- bluetooth://adapters - All Bluetooth adapters
- bluetooth://devices/{paired,connected,visible,trusted}
- bluetooth://adapter/{name} - Adapter details
- bluetooth://device/{address} - Device details
Logging throughout device operations using ctx.info/debug/warning/error
Progress reporting during bt_scan (reports completion percentage)
This commit is contained in:
parent
013cd0eb2f
commit
e9e5b0b4e6
14
README.md
14
README.md
@ -65,6 +65,20 @@ sudo usermod -aG bluetooth $USER
|
|||||||
# Or configure polkit for BlueZ D-Bus access
|
# Or configure polkit for BlueZ D-Bus access
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## MCP Resources
|
||||||
|
|
||||||
|
The server exposes dynamic resources for live state queries:
|
||||||
|
|
||||||
|
| Resource URI | Description |
|
||||||
|
|--------------|-------------|
|
||||||
|
| `bluetooth://adapters` | All Bluetooth adapters |
|
||||||
|
| `bluetooth://devices/paired` | Paired devices |
|
||||||
|
| `bluetooth://devices/connected` | Connected devices |
|
||||||
|
| `bluetooth://devices/visible` | All known devices |
|
||||||
|
| `bluetooth://devices/trusted` | Trusted devices |
|
||||||
|
| `bluetooth://adapter/{name}` | Specific adapter details |
|
||||||
|
| `bluetooth://device/{address}` | Specific device details |
|
||||||
|
|
||||||
## MCP Tools
|
## MCP Tools
|
||||||
|
|
||||||
### Adapter Tools
|
### Adapter Tools
|
||||||
|
|||||||
209
src/mcbluetooth/resources.py
Normal file
209
src/mcbluetooth/resources.py
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
"""MCP Resources for Bluetooth state exposure.
|
||||||
|
|
||||||
|
Resources provide a live, queryable view of Bluetooth state:
|
||||||
|
- Adapters: Available Bluetooth controllers
|
||||||
|
- Devices: Filtered by state (visible, paired, connected)
|
||||||
|
|
||||||
|
Unlike tools which perform actions, resources are read-only snapshots
|
||||||
|
that clients can poll or subscribe to for state changes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from dataclasses import asdict
|
||||||
|
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
|
from mcbluetooth.dbus_client import get_client
|
||||||
|
|
||||||
|
|
||||||
|
def register_resources(mcp: FastMCP) -> None:
|
||||||
|
"""Register Bluetooth state resources with the MCP server."""
|
||||||
|
|
||||||
|
@mcp.resource(
|
||||||
|
"bluetooth://adapters",
|
||||||
|
name="Bluetooth Adapters",
|
||||||
|
description="List of all Bluetooth adapters on the system with their current state",
|
||||||
|
mime_type="application/json",
|
||||||
|
)
|
||||||
|
async def resource_adapters() -> str:
|
||||||
|
"""Get all Bluetooth adapters."""
|
||||||
|
client = await get_client()
|
||||||
|
adapters = await client.list_adapters()
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"adapters": [
|
||||||
|
{
|
||||||
|
"name": a.name,
|
||||||
|
"address": a.address,
|
||||||
|
"alias": a.alias,
|
||||||
|
"powered": a.powered,
|
||||||
|
"discoverable": a.discoverable,
|
||||||
|
"discovering": a.discovering,
|
||||||
|
"pairable": a.pairable,
|
||||||
|
}
|
||||||
|
for a in adapters
|
||||||
|
]
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.resource(
|
||||||
|
"bluetooth://devices/paired",
|
||||||
|
name="Paired Devices",
|
||||||
|
description="Bluetooth devices that have been paired with this system",
|
||||||
|
mime_type="application/json",
|
||||||
|
)
|
||||||
|
async def resource_devices_paired() -> str:
|
||||||
|
"""Get all paired Bluetooth devices."""
|
||||||
|
client = await get_client()
|
||||||
|
devices = await client.list_devices(filter_type="paired")
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"filter": "paired",
|
||||||
|
"count": len(devices),
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"address": d.address,
|
||||||
|
"name": d.name or d.alias or "(unnamed)",
|
||||||
|
"connected": d.connected,
|
||||||
|
"trusted": d.trusted,
|
||||||
|
"adapter": d.adapter,
|
||||||
|
"uuids": d.uuids[:5], # First 5 UUIDs
|
||||||
|
}
|
||||||
|
for d in devices
|
||||||
|
],
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.resource(
|
||||||
|
"bluetooth://devices/connected",
|
||||||
|
name="Connected Devices",
|
||||||
|
description="Bluetooth devices currently connected to this system",
|
||||||
|
mime_type="application/json",
|
||||||
|
)
|
||||||
|
async def resource_devices_connected() -> str:
|
||||||
|
"""Get all connected Bluetooth devices."""
|
||||||
|
client = await get_client()
|
||||||
|
devices = await client.list_devices(filter_type="connected")
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"filter": "connected",
|
||||||
|
"count": len(devices),
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"address": d.address,
|
||||||
|
"name": d.name or d.alias or "(unnamed)",
|
||||||
|
"paired": d.paired,
|
||||||
|
"trusted": d.trusted,
|
||||||
|
"adapter": d.adapter,
|
||||||
|
"services_resolved": d.services_resolved,
|
||||||
|
}
|
||||||
|
for d in devices
|
||||||
|
],
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.resource(
|
||||||
|
"bluetooth://devices/visible",
|
||||||
|
name="Visible Devices",
|
||||||
|
description="All Bluetooth devices visible to this system (discovered or known)",
|
||||||
|
mime_type="application/json",
|
||||||
|
)
|
||||||
|
async def resource_devices_visible() -> str:
|
||||||
|
"""Get all visible/known Bluetooth devices."""
|
||||||
|
client = await get_client()
|
||||||
|
devices = await client.list_devices(filter_type="all")
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"filter": "all",
|
||||||
|
"count": len(devices),
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"address": d.address,
|
||||||
|
"name": d.name or d.alias or "(unnamed)",
|
||||||
|
"rssi": d.rssi,
|
||||||
|
"paired": d.paired,
|
||||||
|
"connected": d.connected,
|
||||||
|
"adapter": d.adapter,
|
||||||
|
}
|
||||||
|
for d in devices
|
||||||
|
],
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.resource(
|
||||||
|
"bluetooth://devices/trusted",
|
||||||
|
name="Trusted Devices",
|
||||||
|
description="Bluetooth devices marked as trusted (auto-connect enabled)",
|
||||||
|
mime_type="application/json",
|
||||||
|
)
|
||||||
|
async def resource_devices_trusted() -> str:
|
||||||
|
"""Get all trusted Bluetooth devices."""
|
||||||
|
client = await get_client()
|
||||||
|
devices = await client.list_devices(filter_type="trusted")
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"filter": "trusted",
|
||||||
|
"count": len(devices),
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"address": d.address,
|
||||||
|
"name": d.name or d.alias or "(unnamed)",
|
||||||
|
"paired": d.paired,
|
||||||
|
"connected": d.connected,
|
||||||
|
"adapter": d.adapter,
|
||||||
|
}
|
||||||
|
for d in devices
|
||||||
|
],
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dynamic resource template for per-adapter info
|
||||||
|
@mcp.resource(
|
||||||
|
"bluetooth://adapter/{adapter_name}",
|
||||||
|
name="Adapter Details",
|
||||||
|
description="Detailed information about a specific Bluetooth adapter",
|
||||||
|
mime_type="application/json",
|
||||||
|
)
|
||||||
|
async def resource_adapter_info(adapter_name: str) -> str:
|
||||||
|
"""Get detailed info for a specific adapter."""
|
||||||
|
client = await get_client()
|
||||||
|
adapter = await client.get_adapter(adapter_name)
|
||||||
|
if not adapter:
|
||||||
|
return json.dumps({"error": f"Adapter '{adapter_name}' not found"})
|
||||||
|
return json.dumps(asdict(adapter), indent=2)
|
||||||
|
|
||||||
|
# Dynamic resource for per-device info
|
||||||
|
@mcp.resource(
|
||||||
|
"bluetooth://device/{address}",
|
||||||
|
name="Device Details",
|
||||||
|
description="Detailed information about a specific Bluetooth device by address",
|
||||||
|
mime_type="application/json",
|
||||||
|
)
|
||||||
|
async def resource_device_info(address: str) -> str:
|
||||||
|
"""Get detailed info for a specific device."""
|
||||||
|
client = await get_client()
|
||||||
|
# Search across all adapters
|
||||||
|
devices = await client.list_devices()
|
||||||
|
for d in devices:
|
||||||
|
if d.address.upper() == address.upper():
|
||||||
|
data = asdict(d)
|
||||||
|
# Convert bytes to hex strings for JSON serialization
|
||||||
|
if data.get("manufacturer_data"):
|
||||||
|
data["manufacturer_data"] = {
|
||||||
|
k: v.hex() if isinstance(v, bytes) else str(v)
|
||||||
|
for k, v in data["manufacturer_data"].items()
|
||||||
|
}
|
||||||
|
if data.get("service_data"):
|
||||||
|
data["service_data"] = {
|
||||||
|
k: v.hex() if isinstance(v, bytes) else str(v)
|
||||||
|
for k, v in data["service_data"].items()
|
||||||
|
}
|
||||||
|
return json.dumps(data, indent=2)
|
||||||
|
return json.dumps({"error": f"Device '{address}' not found"})
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
|
from mcbluetooth import resources
|
||||||
from mcbluetooth.tools import adapter, audio, ble, device
|
from mcbluetooth.tools import adapter, audio, ble, device
|
||||||
|
|
||||||
mcp = FastMCP(
|
mcp = FastMCP(
|
||||||
@ -14,6 +15,16 @@ This server provides comprehensive control over the Linux Bluetooth stack:
|
|||||||
- Audio profiles (A2DP, HFP) with PipeWire/PulseAudio integration
|
- Audio profiles (A2DP, HFP) with PipeWire/PulseAudio integration
|
||||||
- BLE/GATT services (read/write characteristics, notifications)
|
- BLE/GATT services (read/write characteristics, notifications)
|
||||||
|
|
||||||
|
## Resources (live state queries)
|
||||||
|
- bluetooth://adapters - All Bluetooth adapters
|
||||||
|
- bluetooth://devices/paired - Paired devices
|
||||||
|
- bluetooth://devices/connected - Connected devices
|
||||||
|
- bluetooth://devices/visible - All known devices
|
||||||
|
- bluetooth://devices/trusted - Trusted devices
|
||||||
|
- bluetooth://adapter/{name} - Specific adapter details
|
||||||
|
- bluetooth://device/{address} - Specific device details
|
||||||
|
|
||||||
|
## Tools
|
||||||
All tools require an explicit 'adapter' parameter (e.g., "hci0").
|
All tools require an explicit 'adapter' parameter (e.g., "hci0").
|
||||||
Use bt_list_adapters() to discover available adapters.
|
Use bt_list_adapters() to discover available adapters.
|
||||||
|
|
||||||
@ -24,6 +35,9 @@ For pairing, use pairing_mode parameter:
|
|||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Register resources (live state)
|
||||||
|
resources.register_resources(mcp)
|
||||||
|
|
||||||
# Register tool modules
|
# Register tool modules
|
||||||
adapter.register_tools(mcp)
|
adapter.register_tools(mcp)
|
||||||
device.register_tools(mcp)
|
device.register_tools(mcp)
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
"""Device discovery and management tools for BlueZ."""
|
"""Device discovery and management tools for BlueZ."""
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import Context, FastMCP
|
||||||
|
|
||||||
from mcbluetooth.dbus_client import get_client
|
from mcbluetooth.dbus_client import get_client
|
||||||
|
|
||||||
|
# Type aliases for MCP tool parameters
|
||||||
|
ScanMode = Literal["classic", "ble", "both"]
|
||||||
|
DeviceFilter = Literal["all", "paired", "connected", "trusted"]
|
||||||
|
PairingMode = Literal["elicit", "interactive", "auto"]
|
||||||
|
|
||||||
|
|
||||||
def register_tools(mcp: FastMCP) -> None:
|
def register_tools(mcp: FastMCP) -> None:
|
||||||
"""Register device management tools with the MCP server."""
|
"""Register device management tools with the MCP server."""
|
||||||
@ -18,7 +21,8 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
async def bt_scan(
|
async def bt_scan(
|
||||||
adapter: str,
|
adapter: str,
|
||||||
timeout: int = 10,
|
timeout: int = 10,
|
||||||
mode: Literal["classic", "ble", "both"] = "both",
|
mode: ScanMode = "both",
|
||||||
|
ctx: Context | None = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Scan for nearby Bluetooth devices.
|
"""Scan for nearby Bluetooth devices.
|
||||||
|
|
||||||
@ -35,29 +39,48 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
"""
|
"""
|
||||||
client = await get_client()
|
client = await get_client()
|
||||||
|
|
||||||
|
# Log scan start
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Starting {mode} scan on {adapter} for {timeout}s")
|
||||||
|
|
||||||
# Set discovery filter based on mode
|
# Set discovery filter based on mode
|
||||||
transport = {"classic": "bredr", "ble": "le", "both": "auto"}[mode]
|
transport = {"classic": "bredr", "ble": "le", "both": "auto"}[mode]
|
||||||
await client.set_discovery_filter(adapter, transport=transport, duplicate_data=True)
|
await client.set_discovery_filter(adapter, transport=transport, duplicate_data=True)
|
||||||
|
|
||||||
# Start discovery
|
# Start discovery
|
||||||
await client.start_discovery(adapter)
|
await client.start_discovery(adapter)
|
||||||
|
if ctx:
|
||||||
|
await ctx.debug(f"Discovery started on {adapter}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Wait for scan duration
|
# Report progress during scan with periodic device count updates
|
||||||
await asyncio.sleep(timeout)
|
for elapsed in range(timeout):
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
if ctx:
|
||||||
|
# Report progress
|
||||||
|
await ctx.report_progress(progress=elapsed + 1, total=timeout)
|
||||||
|
# Periodically log device count
|
||||||
|
if (elapsed + 1) % 3 == 0 or elapsed == 0:
|
||||||
|
devices = await client.list_devices(adapter=adapter)
|
||||||
|
await ctx.debug(f"Scan progress: {len(devices)} devices found")
|
||||||
finally:
|
finally:
|
||||||
# Always stop discovery
|
# Always stop discovery
|
||||||
await client.stop_discovery(adapter)
|
await client.stop_discovery(adapter)
|
||||||
await client.remove_discovery_filter(adapter)
|
await client.remove_discovery_filter(adapter)
|
||||||
|
if ctx:
|
||||||
|
await ctx.debug("Discovery stopped")
|
||||||
|
|
||||||
# Return discovered devices
|
# Return discovered devices
|
||||||
devices = await client.list_devices(adapter=adapter)
|
devices = await client.list_devices(adapter=adapter)
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Scan complete: found {len(devices)} devices")
|
||||||
return [asdict(d) for d in devices]
|
return [asdict(d) for d in devices]
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def bt_list_devices(
|
async def bt_list_devices(
|
||||||
adapter: str,
|
adapter: str,
|
||||||
filter: Literal["all", "paired", "connected", "trusted"] = "all",
|
filter: DeviceFilter = "all",
|
||||||
|
ctx: Context | None = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""List known Bluetooth devices.
|
"""List known Bluetooth devices.
|
||||||
|
|
||||||
@ -72,11 +95,19 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
List of devices matching the filter
|
List of devices matching the filter
|
||||||
"""
|
"""
|
||||||
client = await get_client()
|
client = await get_client()
|
||||||
|
if ctx:
|
||||||
|
await ctx.debug(f"Listing {filter} devices on {adapter}")
|
||||||
devices = await client.list_devices(adapter=adapter, filter_type=filter)
|
devices = await client.list_devices(adapter=adapter, filter_type=filter)
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Found {len(devices)} {filter} devices")
|
||||||
return [asdict(d) for d in devices]
|
return [asdict(d) for d in devices]
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def bt_device_info(adapter: str, address: str) -> dict[str, Any] | None:
|
async def bt_device_info(
|
||||||
|
adapter: str,
|
||||||
|
address: str,
|
||||||
|
ctx: Context | None = None,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
"""Get detailed information about a specific device.
|
"""Get detailed information about a specific device.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -87,14 +118,19 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Device properties or None if not found
|
Device properties or None if not found
|
||||||
"""
|
"""
|
||||||
client = await get_client()
|
client = await get_client()
|
||||||
|
if ctx:
|
||||||
|
await ctx.debug(f"Getting info for {address}")
|
||||||
info = await client.get_device(adapter, address)
|
info = await client.get_device(adapter, address)
|
||||||
|
if info and ctx:
|
||||||
|
await ctx.info(f"Device: {info.name or info.alias or 'unnamed'} ({address})")
|
||||||
return asdict(info) if info else None
|
return asdict(info) if info else None
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def bt_pair(
|
async def bt_pair(
|
||||||
adapter: str,
|
adapter: str,
|
||||||
address: str,
|
address: str,
|
||||||
pairing_mode: Literal["elicit", "interactive", "auto"] = "interactive",
|
pairing_mode: PairingMode = "interactive",
|
||||||
|
ctx: Context | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Initiate pairing with a device.
|
"""Initiate pairing with a device.
|
||||||
|
|
||||||
@ -114,9 +150,14 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
"""
|
"""
|
||||||
client = await get_client()
|
client = await get_client()
|
||||||
|
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Initiating pairing with {address} (mode: {pairing_mode})")
|
||||||
|
|
||||||
# Check if already paired
|
# Check if already paired
|
||||||
device = await client.get_device(adapter, address)
|
device = await client.get_device(adapter, address)
|
||||||
if device and device.paired:
|
if device and device.paired:
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Device {address} is already paired")
|
||||||
return {"status": "already_paired", "device": asdict(device)}
|
return {"status": "already_paired", "device": asdict(device)}
|
||||||
|
|
||||||
if pairing_mode == "auto":
|
if pairing_mode == "auto":
|
||||||
@ -124,26 +165,33 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
try:
|
try:
|
||||||
await client.pair_device(adapter, address)
|
await client.pair_device(adapter, address)
|
||||||
device = await client.get_device(adapter, address)
|
device = await client.get_device(adapter, address)
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Successfully paired with {address}")
|
||||||
return {"status": "paired", "device": asdict(device) if device else None}
|
return {"status": "paired", "device": asdict(device) if device else None}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
await ctx.error(f"Pairing failed: {e}")
|
||||||
return {"status": "error", "error": str(e)}
|
return {"status": "error", "error": str(e)}
|
||||||
else:
|
else:
|
||||||
# For interactive/elicit modes, we need an agent
|
# For interactive/elicit modes, we need an agent
|
||||||
# Return status indicating pairing initiated
|
|
||||||
try:
|
try:
|
||||||
# Start pairing (this is async and may block for confirmation)
|
|
||||||
# In a real implementation, this would register an agent
|
|
||||||
await client.pair_device(adapter, address)
|
await client.pair_device(adapter, address)
|
||||||
device = await client.get_device(adapter, address)
|
device = await client.get_device(adapter, address)
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Successfully paired with {address}")
|
||||||
return {"status": "paired", "device": asdict(device) if device else None}
|
return {"status": "paired", "device": asdict(device) if device else None}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
if "AuthenticationFailed" in error_msg or "AuthenticationCanceled" in error_msg:
|
if "AuthenticationFailed" in error_msg or "AuthenticationCanceled" in error_msg:
|
||||||
|
if ctx:
|
||||||
|
await ctx.warning("Pairing requires user confirmation or PIN")
|
||||||
return {
|
return {
|
||||||
"status": "awaiting_confirmation",
|
"status": "awaiting_confirmation",
|
||||||
"message": "Pairing requires user confirmation or PIN entry",
|
"message": "Pairing requires user confirmation or PIN entry",
|
||||||
"pairing_mode": pairing_mode,
|
"pairing_mode": pairing_mode,
|
||||||
}
|
}
|
||||||
|
if ctx:
|
||||||
|
await ctx.error(f"Pairing failed: {error_msg}")
|
||||||
return {"status": "error", "error": error_msg}
|
return {"status": "error", "error": error_msg}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@ -152,6 +200,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
address: str,
|
address: str,
|
||||||
pin: str | None = None,
|
pin: str | None = None,
|
||||||
accept: bool = True,
|
accept: bool = True,
|
||||||
|
ctx: Context | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Confirm or reject a pairing request.
|
"""Confirm or reject a pairing request.
|
||||||
|
|
||||||
@ -169,23 +218,36 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
client = await get_client()
|
client = await get_client()
|
||||||
|
|
||||||
if not accept:
|
if not accept:
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Rejecting pairing with {address}")
|
||||||
try:
|
try:
|
||||||
await client.cancel_pairing(adapter, address)
|
await client.cancel_pairing(adapter, address)
|
||||||
return {"status": "pairing_cancelled"}
|
return {"status": "pairing_cancelled"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
await ctx.error(f"Failed to cancel pairing: {e}")
|
||||||
return {"status": "error", "error": str(e)}
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
# Re-attempt pairing - agent would handle PIN
|
if ctx:
|
||||||
# For now, this is a simplified implementation
|
await ctx.info(f"Confirming pairing with {address}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await client.pair_device(adapter, address)
|
await client.pair_device(adapter, address)
|
||||||
device = await client.get_device(adapter, address)
|
device = await client.get_device(adapter, address)
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Pairing confirmed with {address}")
|
||||||
return {"status": "paired", "device": asdict(device) if device else None}
|
return {"status": "paired", "device": asdict(device) if device else None}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
await ctx.error(f"Pairing confirmation failed: {e}")
|
||||||
return {"status": "error", "error": str(e)}
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def bt_unpair(adapter: str, address: str) -> dict[str, str]:
|
async def bt_unpair(
|
||||||
|
adapter: str,
|
||||||
|
address: str,
|
||||||
|
ctx: Context | None = None,
|
||||||
|
) -> dict[str, str]:
|
||||||
"""Remove pairing with a device.
|
"""Remove pairing with a device.
|
||||||
|
|
||||||
This removes the device from the list of known devices and
|
This removes the device from the list of known devices and
|
||||||
@ -199,14 +261,24 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status of the operation
|
Status of the operation
|
||||||
"""
|
"""
|
||||||
client = await get_client()
|
client = await get_client()
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Removing device {address}")
|
||||||
try:
|
try:
|
||||||
await client.remove_device(adapter, address)
|
await client.remove_device(adapter, address)
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Device {address} removed successfully")
|
||||||
return {"status": "removed", "address": address}
|
return {"status": "removed", "address": address}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
await ctx.error(f"Failed to remove device: {e}")
|
||||||
return {"status": "error", "error": str(e)}
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def bt_connect(adapter: str, address: str) -> dict[str, Any]:
|
async def bt_connect(
|
||||||
|
adapter: str,
|
||||||
|
address: str,
|
||||||
|
ctx: Context | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Connect to a paired device.
|
"""Connect to a paired device.
|
||||||
|
|
||||||
Establishes an active connection to a previously paired device.
|
Establishes an active connection to a previously paired device.
|
||||||
@ -220,15 +292,25 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Connection status and device info
|
Connection status and device info
|
||||||
"""
|
"""
|
||||||
client = await get_client()
|
client = await get_client()
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Connecting to {address}")
|
||||||
try:
|
try:
|
||||||
await client.connect_device(adapter, address)
|
await client.connect_device(adapter, address)
|
||||||
device = await client.get_device(adapter, address)
|
device = await client.get_device(adapter, address)
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Connected to {device.name or address}")
|
||||||
return {"status": "connected", "device": asdict(device) if device else None}
|
return {"status": "connected", "device": asdict(device) if device else None}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
await ctx.error(f"Connection failed: {e}")
|
||||||
return {"status": "error", "error": str(e)}
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def bt_disconnect(adapter: str, address: str) -> dict[str, Any]:
|
async def bt_disconnect(
|
||||||
|
adapter: str,
|
||||||
|
address: str,
|
||||||
|
ctx: Context | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Disconnect from a device.
|
"""Disconnect from a device.
|
||||||
|
|
||||||
Terminates the active connection but preserves pairing.
|
Terminates the active connection but preserves pairing.
|
||||||
@ -241,15 +323,26 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Status of the operation
|
Status of the operation
|
||||||
"""
|
"""
|
||||||
client = await get_client()
|
client = await get_client()
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Disconnecting from {address}")
|
||||||
try:
|
try:
|
||||||
await client.disconnect_device(adapter, address)
|
await client.disconnect_device(adapter, address)
|
||||||
device = await client.get_device(adapter, address)
|
device = await client.get_device(adapter, address)
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Disconnected from {address}")
|
||||||
return {"status": "disconnected", "device": asdict(device) if device else None}
|
return {"status": "disconnected", "device": asdict(device) if device else None}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
await ctx.error(f"Disconnect failed: {e}")
|
||||||
return {"status": "error", "error": str(e)}
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def bt_trust(adapter: str, address: str, trusted: bool) -> dict[str, Any]:
|
async def bt_trust(
|
||||||
|
adapter: str,
|
||||||
|
address: str,
|
||||||
|
trusted: bool,
|
||||||
|
ctx: Context | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Set device trust status.
|
"""Set device trust status.
|
||||||
|
|
||||||
Trusted devices can connect automatically without explicit
|
Trusted devices can connect automatically without explicit
|
||||||
@ -264,15 +357,27 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Updated device info
|
Updated device info
|
||||||
"""
|
"""
|
||||||
client = await get_client()
|
client = await get_client()
|
||||||
|
action = "Trusting" if trusted else "Untrusting"
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"{action} device {address}")
|
||||||
try:
|
try:
|
||||||
await client.set_device_trusted(adapter, address, trusted)
|
await client.set_device_trusted(adapter, address, trusted)
|
||||||
device = await client.get_device(adapter, address)
|
device = await client.get_device(adapter, address)
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Device {address} {'trusted' if trusted else 'untrusted'}")
|
||||||
return {"status": "updated", "device": asdict(device) if device else None}
|
return {"status": "updated", "device": asdict(device) if device else None}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
await ctx.error(f"Failed to update trust: {e}")
|
||||||
return {"status": "error", "error": str(e)}
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def bt_block(adapter: str, address: str, blocked: bool) -> dict[str, Any]:
|
async def bt_block(
|
||||||
|
adapter: str,
|
||||||
|
address: str,
|
||||||
|
blocked: bool,
|
||||||
|
ctx: Context | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Block or unblock a device.
|
"""Block or unblock a device.
|
||||||
|
|
||||||
Blocked devices cannot connect to this adapter.
|
Blocked devices cannot connect to this adapter.
|
||||||
@ -286,15 +391,27 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Updated device info
|
Updated device info
|
||||||
"""
|
"""
|
||||||
client = await get_client()
|
client = await get_client()
|
||||||
|
action = "Blocking" if blocked else "Unblocking"
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"{action} device {address}")
|
||||||
try:
|
try:
|
||||||
await client.set_device_blocked(adapter, address, blocked)
|
await client.set_device_blocked(adapter, address, blocked)
|
||||||
device = await client.get_device(adapter, address)
|
device = await client.get_device(adapter, address)
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Device {address} {'blocked' if blocked else 'unblocked'}")
|
||||||
return {"status": "updated", "device": asdict(device) if device else None}
|
return {"status": "updated", "device": asdict(device) if device else None}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
await ctx.error(f"Failed to update block status: {e}")
|
||||||
return {"status": "error", "error": str(e)}
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def bt_device_set_alias(adapter: str, address: str, alias: str) -> dict[str, Any]:
|
async def bt_device_set_alias(
|
||||||
|
adapter: str,
|
||||||
|
address: str,
|
||||||
|
alias: str,
|
||||||
|
ctx: Context | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Set a friendly name for a device.
|
"""Set a friendly name for a device.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -306,9 +423,15 @@ def register_tools(mcp: FastMCP) -> None:
|
|||||||
Updated device info
|
Updated device info
|
||||||
"""
|
"""
|
||||||
client = await get_client()
|
client = await get_client()
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Setting alias for {address} to '{alias}'")
|
||||||
try:
|
try:
|
||||||
await client.set_device_alias(adapter, address, alias)
|
await client.set_device_alias(adapter, address, alias)
|
||||||
device = await client.get_device(adapter, address)
|
device = await client.get_device(adapter, address)
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Alias set for {address}")
|
||||||
return {"status": "updated", "device": asdict(device) if device else None}
|
return {"status": "updated", "device": asdict(device) if device else None}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
await ctx.error(f"Failed to set alias: {e}")
|
||||||
return {"status": "error", "error": str(e)}
|
return {"status": "error", "error": str(e)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user