1use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter};
2use btleplug::platform::Manager;
3use rmcp::{
4 handler::server::{router::tool::ToolRouter, ServerHandler},
5 model::*,
6 ErrorData as McpError,
7};
8use std::time::Duration;
9
10#[derive(Debug)]
11pub struct BluetoothServer {
12 pub tool_router: ToolRouter<Self>,
13}
14
15impl Default for BluetoothServer {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl BluetoothServer {
22 pub fn new() -> Self {
23 Self {
24 tool_router: Self::tool_router(),
25 }
26 }
27}
28
29#[rmcp::tool_router]
30impl BluetoothServer {
31 #[rmcp::tool(description = "Scan for nearby Bluetooth Low Energy (BLE) devices")]
32 pub async fn scan_ble_devices(&self) -> Result<CallToolResult, McpError> {
33 let manager = Manager::new().await
34 .map_err(|e| McpError::internal_error(format!("Failed to create BT manager: {}", e), None))?;
35
36 let adapters = manager.adapters().await
37 .map_err(|e| McpError::internal_error(format!("Failed to get adapters: {}", e), None))?;
38
39 if adapters.is_empty() {
40 return Ok(CallToolResult::success(vec![Content::text(
41 "Bluetooth Status:\n\nNo Bluetooth adapters found.\n"
42 )]));
43 }
44
45 let mut result = String::from("Bluetooth Devices:\n\n");
46
47 for adapter in adapters {
48 let adapter_info = adapter.adapter_info().await
49 .unwrap_or_else(|_| "Unknown adapter".to_string());
50 result.push_str(&format!("Adapter: {}\n\n", adapter_info));
51
52 if let Err(e) = adapter.start_scan(ScanFilter::default()).await {
54 result.push_str(&format!(" Could not scan: {}\n", e));
55 continue;
56 }
57
58 tokio::time::sleep(Duration::from_secs(3)).await;
60
61 let _ = adapter.stop_scan().await;
63
64 let peripherals = adapter.peripherals().await
66 .map_err(|e| McpError::internal_error(format!("Failed to get peripherals: {}", e), None))?;
67
68 if peripherals.is_empty() {
69 result.push_str(" No BLE devices found nearby.\n");
70 } else {
71 let mut count = 0;
72 for peripheral in peripherals {
73 count += 1;
74
75 let properties = peripheral.properties().await
76 .ok()
77 .flatten();
78
79 let name = properties.as_ref()
80 .and_then(|p| p.local_name.clone())
81 .unwrap_or_else(|| "Unknown".to_string());
82
83 let address = properties.as_ref()
84 .map(|p| p.address.to_string())
85 .unwrap_or_else(|| "??:??:??:??:??:??".to_string());
86
87 let rssi = properties.as_ref()
88 .and_then(|p| p.rssi)
89 .map(|r| format!(" ({}dBm)", r))
90 .unwrap_or_default();
91
92 result.push_str(&format!(" {}. {}{}\n", count, name, rssi));
93 result.push_str(&format!(" Address: {}\n", address));
94 }
95 result.push_str(&format!("\n Total: {} BLE devices\n", count));
96 }
97 }
98
99 Ok(CallToolResult::success(vec![Content::text(result)]))
100 }
101}
102
103#[rmcp::tool_handler]
104impl ServerHandler for BluetoothServer {
105 fn get_info(&self) -> ServerInfo {
106 ServerInfo {
107 protocol_version: ProtocolVersion::V_2024_11_05,
108 capabilities: ServerCapabilities::builder()
109 .enable_tools()
110 .build(),
111 server_info: Implementation::from_build_env(),
112 instructions: Some("Cross-platform Bluetooth Low Energy device scanner".into()),
113 }
114 }
115}