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.
115 lines (114 loc) • 3.75 kB
JavaScript
import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { registerMcpTools } from './mcp-tools.js';
import { createHttpApp } from './mcp-http-transport.js';
import { getPackageMetadata } from './utils.js';
/**
* Observability Server - Separate service for health checks and MCP tools
*
* This server provides:
* - HTTP health check endpoint
* - MCP debugging tools via HTTP/stdio
* - Future: metrics, monitoring, etc.
*
* It observes the bridge server but doesn't interfere with it
*/
export class ObservabilityServer {
mcpServer;
sharedState;
bridgeServer = null;
httpServer;
constructor(sharedState) {
// Use shared state for log buffer
this.sharedState = sharedState;
// Initialize MCP server
const metadata = getPackageMetadata();
this.mcpServer = new McpServer({
name: metadata.name,
version: metadata.version
});
// Register MCP tools with bridge state access
registerMcpTools(this.mcpServer, this);
}
/**
* Connect to bridge server for observability
*/
connectToBridge(bridgeServer) {
this.bridgeServer = bridgeServer;
}
/**
* Start HTTP server for health checks and MCP
*/
async startHttp(port = 8081) {
const app = express();
// Health check endpoint
app.get('/health', (req, res) => {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
bridge: this.getBridgeHealth()
};
res.json(health);
});
// Add MCP HTTP endpoints
const mcpApp = createHttpApp(this.mcpServer, process.env.BLE_MCP_HTTP_TOKEN);
app.use('/', mcpApp);
return new Promise((resolve) => {
this.httpServer = app.listen(port, () => {
console.log(`📊 Observability server listening on port ${port}`);
console.log(` Health check: http://localhost:${port}/health`);
console.log(` MCP info: http://localhost:${port}/mcp/info`);
resolve();
});
});
}
/**
* Connect MCP stdio transport if available
*/
async connectStdio() {
const hasTty = process.stdin.isTTY && process.stdout.isTTY;
const stdioDisabled = process.env.BLE_MCP_STDIO_DISABLED === 'true';
if (hasTty && !stdioDisabled) {
const stdioTransport = new StdioServerTransport();
await this.mcpServer.connect(stdioTransport);
console.log('[MCP] Stdio transport connected');
}
}
/**
* Get bridge health status
*/
getBridgeHealth() {
const state = this.sharedState.getConnectionState();
return {
connected: state.connected,
deviceName: state.deviceName,
free: !state.connected && !state.recovering,
recovering: state.recovering
};
}
// MCP tool interface methods
getConnectionState() {
return this.getBridgeHealth();
}
async scanDevices() {
// Could proxy to bridge if it exposed scanning
throw new Error('Device scanning not available in ultra-simple mode');
}
getLogBuffer() {
return this.sharedState.getLogBuffer();
}
getMcpServer() {
return this.mcpServer;
}
/**
* Stop the HTTP server
*/
async stop() {
if (this.httpServer) {
return new Promise((resolve) => {
this.httpServer.close(() => resolve());
});
}
}
}