UNPKG

ble-mcp-test

Version:

Complete BLE testing stack: WebSocket bridge server, MCP observability layer, and Web Bluetooth API mock. Test real BLE devices in Playwright/E2E tests without browser support.

212 lines (211 loc) 8.84 kB
import { z } from 'zod'; import { getPackageMetadata } from './utils.js'; // Tool registry for dynamic tool listing export const toolRegistry = []; // Helper function to register tool and update registry function registerToolWithRegistry(server, tool) { const { name, handler, ...definition } = tool; // Wrap handler to add logging const wrappedHandler = async (args) => { console.log(`[MCP Tool] Executing '${name}' with args:`, JSON.stringify(args)); try { const result = await handler(args); console.log(`[MCP Tool] '${name}' completed successfully`); return result; } catch (error) { console.error(`[MCP Tool] '${name}' failed:`, error.message); throw error; } }; server.registerTool(name, definition, wrappedHandler); toolRegistry.push({ name, description: definition.description }); } export function registerMcpTools(server, provider) { // Tool 1: get_logs registerToolWithRegistry(server, { name: 'get_logs', title: 'Get BLE Communication Logs', description: 'Retrieve recent BLE communication logs with filtering options', inputSchema: { since: z.string().default('30s').describe("Time filter: duration (30s, 5m, 1h), ISO timestamp, or 'last'"), filter: z.string().optional().describe("Filter by 'TX'/'RX', hex pattern, or regex"), exclude_packets: z.boolean().optional().describe("Exclude TX/RX packet data, show only system messages"), exclude_regex: z.string().optional().describe("Regex pattern to exclude from results"), regex: z.boolean().optional().describe("Treat filter as regex pattern"), limit: z.number().min(1).max(1000).default(100).describe("Maximum entries to return") }, handler: async (args) => { const { since, filter, exclude_packets, exclude_regex, regex, limit } = args; let logs = provider.getLogBuffer().getLogsSince(since, limit * 2); // Get more to account for filtering // First apply exclude_packets filter (show only system messages) if (exclude_packets) { logs = logs.filter((log) => log.direction === 'INFO'); } // Apply exclusion regex if provided if (exclude_regex) { try { const excludePattern = new RegExp(exclude_regex, 'i'); logs = logs.filter((log) => !excludePattern.test(log.hex)); } catch (error) { throw new Error(`Invalid exclude regex pattern: ${exclude_regex} - ${error}`); } } // Apply filter if provided if (filter) { const filterUpper = filter.toUpperCase(); if (regex) { // Use regex filtering try { const pattern = new RegExp(filter, 'i'); logs = logs.filter((log) => pattern.test(log.hex)); } catch (error) { throw new Error(`Invalid regex pattern: ${filter} - ${error}`); } } else if (filterUpper === 'TX' || filterUpper === 'RX') { // Direction filtering logs = logs.filter((log) => log.direction === filterUpper); } else { // Hex pattern filtering (case-insensitive substring) const cleanFilter = filter.replace(/\s+/g, '').toUpperCase(); logs = logs.filter((log) => { const cleanHex = log.hex.replace(/\s+/g, '').toUpperCase(); return cleanHex.includes(cleanFilter); }); } } const response = { logs: logs.slice(0, limit), count: logs.length, truncated: logs.length > limit }; return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] }; } }); // Tool 2: search_packets registerToolWithRegistry(server, { name: 'search_packets', title: 'Search BLE Packets', description: 'Search for hex patterns in BLE packets', inputSchema: { hex_pattern: z.string().describe("Hex pattern to search for (case insensitive, spaces optional)"), limit: z.number().min(1).max(1000).default(100).describe("Maximum results to return") }, handler: async (args) => { const { hex_pattern, limit } = args; const matches = provider.getLogBuffer().searchPackets(hex_pattern, limit); const response = { matches, count: matches.length, pattern: hex_pattern }; return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] }; } }); // Tool 3: get_connection_state registerToolWithRegistry(server, { name: 'get_connection_state', title: 'Get Connection State', description: 'Get detailed BLE connection state and activity', inputSchema: {}, handler: async () => { const state = provider.getConnectionState(); const stats = provider.getLogBuffer().getConnectionStats(); const response = { ...state, ...stats }; return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] }; } }); // Tool 4: status registerToolWithRegistry(server, { name: 'status', title: 'Get Bridge Server Status', description: 'Get bridge server status and configuration', inputSchema: {}, handler: async () => { // Determine active transports const hasTty = process.stdin.isTTY && process.stdout.isTTY; const stdioEnabled = hasTty && process.env.BLE_MCP_STDIO_DISABLED !== 'true'; const httpEnabled = !!process.argv.includes('--mcp-http') || !!process.env.BLE_MCP_HTTP_PORT || !!process.env.BLE_MCP_HTTP_TOKEN; const metadata = getPackageMetadata(); const status = { version: metadata.version, uptime: process.uptime(), wsPort: parseInt(process.env.BLE_MCP_WS_PORT || '8080'), mcpTransports: { stdio: stdioEnabled, http: httpEnabled, httpPort: httpEnabled ? parseInt(process.env.BLE_MCP_HTTP_PORT || '8081') : undefined, httpAuth: !!process.env.BLE_MCP_HTTP_TOKEN }, logBufferSize: parseInt(process.env.BLE_MCP_LOG_BUFFER_SIZE || '10000'), logLevel: process.env.BLE_MCP_LOG_LEVEL || 'debug' }; return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] }; } }); // Tool 5: scan_devices registerToolWithRegistry(server, { name: 'scan_devices', title: 'Scan for BLE Devices', description: 'Scan for nearby BLE devices (only when not connected)', inputSchema: { duration: z.number().min(1000).max(30000).default(5000).describe("Scan duration in milliseconds") }, handler: async () => { try { const devices = await provider.scanDevices(); return { content: [{ type: 'text', text: JSON.stringify({ devices }, null, 2) }] }; } catch (error) { // Handle scan-while-connected error if (error.message.includes('Cannot scan while connected')) { return { content: [{ type: 'text', text: JSON.stringify({ error: error.message, code: 'SCAN_WHILE_CONNECTED' }, null, 2) }] }; } throw error; } } }); }