UNPKG

signalk-mcp-server

Version:
845 lines 31.5 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { SignalKClient } from './signalk-client.js'; import * as fs from 'fs/promises'; import * as path from 'path'; import { getCurrentDirname } from './utils/path-utils.js'; // Get __dirname equivalent for ES modules const currentDirname = getCurrentDirname(); /** * SignalK MCP Server - Provides AI agents with access to marine vessel data via Model Context Protocol * * This server bridges SignalK marine data systems with AI agents by exposing vessel navigation, * AIS targets, alarms, and sensor data through standardized MCP tools. It maintains a persistent * connection to a SignalK server and provides real-time access to marine data. * * Features: * - Real-time vessel state (position, heading, speed, wind) * - AIS target tracking (nearby vessels) * - System alarm monitoring * - Dynamic path discovery * - Connection status monitoring * - Graceful error handling with continued operation * * @example * // Basic usage * const server = new SignalKMCPServer({ * serverName: 'my-signalk-mcp', * serverVersion: '1.0.0' * }); * await server.run(); * * // With custom SignalK client * const client = new SignalKClient({ hostname: '192.168.1.100', port: 3000 }); * const server = new SignalKMCPServer({ signalkClient: client }); * await server.run(); */ export class SignalKMCPServer { signalkClient; server; serverName; serverVersion; resources = new Map(); resourcesDir; /** * Creates a new SignalK MCP Server instance with configuration from options or environment variables * * Configuration priority: * 1. Constructor options * 2. Environment variables (SERVER_NAME, SERVER_VERSION) * 3. Default values * * Server capabilities: * - Registers 6 MCP tools for vessel data access * - Sets up error handling for SignalK connection failures * - Initiates asynchronous connection to SignalK server * - Continues operation even if SignalK is unavailable * * @param options - Server configuration options * @param options.serverName - MCP server identifier (default: 'signalk-mcp-server') * @param options.serverVersion - Version string (default: '1.0.0') * @param options.signalkClient - Custom SignalK client instance (optional) * * @example * // Default configuration * const server = new SignalKMCPServer(); * * // Custom configuration * const server = new SignalKMCPServer({ * serverName: 'my-boat-mcp', * serverVersion: '2.1.0' * }); * * // With environment variables * // SERVER_NAME=production-signalk-mcp * // SERVER_VERSION=1.5.0 * const server = new SignalKMCPServer(); */ constructor(options = {}) { this.serverName = options.serverName || process.env.SERVER_NAME || 'signalk-mcp-server'; this.serverVersion = options.serverVersion || process.env.SERVER_VERSION || '1.0.0'; this.signalkClient = options.signalkClient || new SignalKClient(); this.server = new Server({ name: this.serverName, version: this.serverVersion, }, { capabilities: { tools: {}, resources: {}, }, }); // Setup resources directory // Use currentDirname to find the resources directory relative to this file // When compiled, this file is at dist/src/signalk-mcp-server.js // Resources are at project-root/resources if (currentDirname.includes('dist')) { // Running from built dist directory - go up 2 levels from dist/src this.resourcesDir = path.join(currentDirname, '..', '..', 'resources'); } else { // Running from source directory - go up 1 level from src this.resourcesDir = path.join(currentDirname, '..', 'resources'); } this.setupToolHandlers(); this.setupResourceHandlers(); // Load resources this.loadResources().catch((error) => { console.error('Failed to load resources:', error.message || error); }); // Handle SignalK connection errors gracefully this.signalkClient.on('error', (error) => { console.error('SignalK connection error:', error.message || error); // Don't crash the server on SignalK connection errors }); // Connect to SignalK asynchronously this.connectToSignalK().catch((error) => { console.error('SignalK connection failed during startup:', error.message || error); }); } /** * Loads resources from the filesystem */ async loadResources() { try { const resourceFiles = [ 'signalk-overview.json', 'data-model-reference.json', 'path-categories-guide.json', 'mcp-tool-reference.json', ]; for (const file of resourceFiles) { try { const filePath = path.join(this.resourcesDir, file); const content = await fs.readFile(filePath, 'utf-8'); const resourceName = file.replace('.json', '').replace(/-/g, '_'); this.resources.set(`signalk://${resourceName}`, JSON.parse(content)); } catch (error) { console.error(`Failed to load resource ${file}:`, error.message); } } } catch (error) { console.error('Failed to load resources directory:', error.message); } } /** * Establishes connection to SignalK server with graceful error handling * * Connection behavior: * - Attempts to connect to SignalK server using client configuration * - Logs success/failure without throwing errors * - Allows MCP server to continue operating even if SignalK is unavailable * - Called automatically during server initialization * * @returns Promise that always resolves (never rejects) * * @example * // Manual reconnection attempt * await server.connectToSignalK(); * * // Connection is also attempted automatically during construction * const server = new SignalKMCPServer(); * // connectToSignalK() is called internally */ async connectToSignalK() { try { await this.signalkClient.connect(); console.error('SignalK client connected successfully'); } catch (error) { console.error('Failed to connect to SignalK:', error.message || error); // Continue running the MCP server even if SignalK is unavailable } } /** * Registers MCP tool handlers and defines the available tools for AI agents * * Registered tools: * - get_vessel_state: Current vessel navigation data * - get_ais_targets: Nearby vessels from AIS * - get_active_alarms: System notifications and alerts * - list_available_paths: Discover available SignalK data paths * - get_path_value: Get latest value for specific path * - get_connection_status: WebSocket connection health * * Handler features: * - JSON Schema validation for tool inputs * - Standardized error handling with MCP error codes * - Automatic request routing to appropriate methods * - Graceful error responses for tool execution failures * * @example * // Tools are registered automatically during construction * const server = new SignalKMCPServer(); * // setupToolHandlers() is called internally * * // AI agents can then call tools like: * // - get_vessel_state() * // - get_ais_targets() * // - get_path_value({"path": "navigation.position"}) */ setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [ { name: 'get_vessel_state', description: 'Get current vessel navigation data (position, heading, speed, wind, vessel identity)', inputSchema: { type: 'object', properties: {}, additionalProperties: false, }, }, { name: 'get_ais_targets', description: 'Get nearby AIS targets sorted by distance from self vessel (closest first). Includes distance in meters, position, course, and speed data.', inputSchema: { type: 'object', properties: { page: { type: 'number', description: 'Page number (1-based, default: 1)', minimum: 1, }, pageSize: { type: 'number', description: 'Number of targets per page (default: 10, max: 50)', minimum: 1, maximum: 50, }, }, additionalProperties: false, }, }, { name: 'get_active_alarms', description: 'Get current system notifications and alerts', inputSchema: { type: 'object', properties: {}, additionalProperties: false, }, }, { name: 'list_available_paths', description: 'Discover available SignalK data paths', inputSchema: { type: 'object', properties: {}, additionalProperties: false, }, }, { name: 'get_path_value', description: 'Get latest value for a specific SignalK path', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'SignalK data path (e.g., navigation.position)', }, }, required: ['path'], additionalProperties: false, }, }, { name: 'get_connection_status', description: 'Get SignalK WebSocket connection status and health', inputSchema: { type: 'object', properties: {}, additionalProperties: false, }, }, { name: 'get_initial_context', description: 'Get comprehensive SignalK context and documentation to understand available data and usage patterns', inputSchema: { type: 'object', properties: {}, additionalProperties: false, }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_vessel_state': return await this.getVesselState(); case 'get_ais_targets': return await this.getAISTargets(args.page, args.pageSize); case 'get_active_alarms': return await this.getActiveAlarms(); case 'list_available_paths': return await this.listAvailablePaths(); case 'get_path_value': return await this.getPathValue(args.path); case 'get_connection_status': return this.getConnectionStatus(); case 'get_initial_context': return this.getInitialContext(); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error.message}`); } }); } /** * Sets up MCP resource handlers for listing and reading reference resources * * Provides reference resources: * - SignalK overview and documentation * - Data model reference * - Path categories guide * - MCP tool reference */ setupResourceHandlers() { this.server.setRequestHandler(ListResourcesRequestSchema, () => { const resources = [ // Reference resources { uri: 'signalk://signalk_overview', name: 'SignalK Overview', description: 'Core concepts and data model structure of SignalK', mimeType: 'application/json', }, { uri: 'signalk://data_model_reference', name: 'SignalK Data Model Reference', description: 'Comprehensive reference of SignalK paths and their meanings', mimeType: 'application/json', }, { uri: 'signalk://path_categories_guide', name: 'SignalK Path Categories Guide', description: 'Guide to understanding and categorizing SignalK paths', mimeType: 'application/json', }, { uri: 'signalk://mcp_tool_reference', name: 'MCP Tool Reference', description: 'Reference guide for available MCP tools and their usage patterns', mimeType: 'application/json', }, ]; return { resources }; }); this.server.setRequestHandler(ReadResourceRequestSchema, (request) => { const { uri } = request.params; try { // Check if it's a resource const resourceContent = this.resources.get(uri); if (resourceContent) { return { contents: [ { uri, mimeType: 'application/json', text: JSON.stringify(resourceContent, null, 2), }, ], }; } // Unknown resource throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`); } catch (error) { throw new McpError(ErrorCode.InternalError, `Resource read failed: ${error.message}`); } }); } /** * MCP tool handler that returns current vessel state with all available sensor data * * Response format: * - JSON text content with vessel navigation and sensor data * - Includes connection status, context, and timestamp * - Dynamic data structure based on available SignalK paths * - Formatted with 2-space indentation for readability * * @returns MCPToolResponse with vessel state as formatted JSON text * * @example * // Called by AI agents via MCP protocol: * // Tool: get_vessel_state * // Arguments: {} * * // Response content: * // { * // "connected": true, * // "context": "vessels.self", * // "timestamp": "2023-06-22T10:30:15.123Z", * // "data": { * // "navigation.position": { * // "value": {"latitude": 37.8199, "longitude": -122.4783}, * // "timestamp": "2023-06-22T10:30:15.000Z" * // }, * // "navigation.speedOverGround": { * // "value": 5.2, * // "timestamp": "2023-06-22T10:30:15.000Z" * // } * // } * // } */ async getVesselState() { const data = await this.signalkClient.getVesselState(); return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } /** * MCP tool handler that returns nearby AIS targets (other vessels) with position and navigation data * * Response format: * - JSON text content with AIS target array sorted by distance * - Includes distance in meters from self vessel when positions available * - Supports pagination with configurable page size * - Filtered to targets updated within last 5 minutes * - Maximum 50 targets per page * - Includes MMSI, position, course, speed, and vessel identification * * @param page - Page number (1-based, default: 1) * @param pageSize - Number of targets per page (default: 10, max: 50) * @returns MCPToolResponse with AIS targets as formatted JSON text * * @example * // Called by AI agents via MCP protocol: * // Tool: get_ais_targets * // Arguments: {"page": 1, "pageSize": 10} * * // Response content: * // { * // "connected": true, * // "count": 2, * // "timestamp": "2023-06-22T10:30:15.123Z", * // "targets": [ * // { * // "mmsi": "123456789", * // "distanceMeters": 1852.5, * // "navigation.position": {"latitude": 37.8200, "longitude": -122.4800}, * // "navigation.courseOverGround": 45.0, * // "navigation.speedOverGround": 8.5, * // "lastUpdate": "2023-06-22T10:29:45.000Z" * // } * // ], * // "pagination": { * // "page": 1, * // "pageSize": 10, * // "totalCount": 15, * // "totalPages": 2, * // "hasNextPage": true, * // "hasPreviousPage": false * // } * // } */ async getAISTargets(page, pageSize) { const data = await this.signalkClient.getAISTargets(page, pageSize); return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } /** * MCP tool handler that returns current active alarms and system notifications * * Response format: * - JSON text content with active alarm array * - Includes alarm state (alert, warn, alarm, emergency) * - Contains alarm message, path, and timestamp * - Only returns alarms with non-normal states * * Alarm states: * - alert: General warning condition * - warn: Warning requiring attention * - alarm: Alarm requiring immediate attention * - emergency: Emergency requiring immediate action * * @returns MCPToolResponse with active alarms as formatted JSON text * * @example * // Called by AI agents via MCP protocol: * // Tool: get_active_alarms * // Arguments: {} * * // Response content: * // { * // "connected": true, * // "count": 1, * // "timestamp": "2023-06-22T10:30:15.123Z", * // "alarms": [ * // { * // "path": "notifications.engines.temperature", * // "state": "alert", * // "message": "Engine temperature high", * // "timestamp": "2023-06-22T10:25:30.000Z" * // } * // ] * // } */ async getActiveAlarms() { const data = await this.signalkClient.getActiveAlarms(); return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } /** * MCP tool handler that discovers and returns all available SignalK data paths * * Response format: * - JSON text content with sorted array of available paths * - Uses HTTP REST API for complete path discovery * - Falls back to WebSocket-discovered paths if HTTP fails * - Filters out metadata fields and internal paths * * Path discovery methods: * 1. Primary: HTTP REST API query to SignalK server * 2. Fallback: WebSocket-cached paths from live data * * @returns MCPToolResponse with available paths as formatted JSON text * * @example * // Called by AI agents via MCP protocol: * // Tool: list_available_paths * // Arguments: {} * * // Response content: * // { * // "connected": true, * // "count": 25, * // "timestamp": "2023-06-22T10:30:15.123Z", * // "paths": [ * // "electrical.batteries.house.voltage", * // "environment.wind.speedApparent", * // "navigation.courseOverGround", * // "navigation.position", * // "navigation.speedOverGround", * // "propulsion.main.temperature" * // ] * // } */ async listAvailablePaths() { const data = await this.signalkClient.listAvailablePaths(); return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } /** * MCP tool handler that gets the latest value for a specific SignalK data path * * Response format: * - JSON text content with path value and metadata * - Uses HTTP REST API for real-time data from server * - Falls back to WebSocket-cached value if HTTP fails * - Includes timestamps, source information, and error details * * Value retrieval methods: * 1. Primary: HTTP REST API query for specific path * 2. Fallback: WebSocket-cached value from live data * * @param path - SignalK data path in dot notation (e.g., 'navigation.position') * @returns MCPToolResponse with path value as formatted JSON text * * @example * // Called by AI agents via MCP protocol: * // Tool: get_path_value * // Arguments: {"path": "navigation.position"} * * // Response content: * // { * // "connected": true, * // "path": "navigation.position", * // "timestamp": "2023-06-22T10:30:15.123Z", * // "data": { * // "value": { * // "latitude": 37.8199, * // "longitude": -122.4783 * // }, * // "timestamp": "2023-06-22T10:30:15.000Z", * // "source": { * // "label": "GPS1", * // "type": "NMEA0183" * // } * // } * // } */ async getPathValue(path) { const data = await this.signalkClient.getPathValue(path); return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } /** * MCP tool handler that returns comprehensive SignalK connection status and health information * * Response format: * - JSON text content with detailed connection information * - WebSocket and HTTP URLs for debugging * - Server configuration details (hostname, port, TLS) * - Data cache statistics (paths, AIS targets, alarms) * - Current vessel context being monitored * * Status information includes: * - Connection state and server URLs * - Configuration parameters * - Data cache statistics * - Timestamp of status check * * @returns MCPToolResponse with connection status as formatted JSON text * * @example * // Called by AI agents via MCP protocol: * // Tool: get_connection_status * // Arguments: {} * * // Response content: * // { * // "connected": true, * // "url": "ws://localhost:3000", * // "wsUrl": "ws://localhost:3000", * // "httpUrl": "http://localhost:3000", * // "hostname": "localhost", * // "port": 3000, * // "useTLS": false, * // "context": "vessels.self", * // "pathCount": 25, * // "aisTargetCount": 3, * // "activeAlarmCount": 1, * // "timestamp": "2023-06-22T10:30:15.123Z" * // } */ getConnectionStatus() { const data = this.signalkClient.getConnectionStatus(); return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } /** * MCP tool handler that returns comprehensive SignalK context and documentation * * This tool provides AI agents with essential context about: * - SignalK overview and core concepts * - Complete data model reference with path meanings * - Path categorization guide for understanding data organization * - MCP tool reference with usage patterns and examples * * Response format: * - JSON text content with all reference materials combined * - Structured sections for each type of documentation * - Comprehensive guide for AI agents to understand and utilize SignalK data * * Usage: * - Call this tool first to understand the SignalK system * - Use the returned context to make informed decisions about other tool calls * - Reference the path categories and data model when interpreting vessel data * * @returns MCPToolResponse with comprehensive SignalK context as formatted JSON text * * @example * // Called by AI agents via MCP protocol: * // Tool: get_initial_context * // Arguments: {} * * // Response content: * // { * // "signalk_overview": {...}, * // "data_model_reference": {...}, * // "path_categories_guide": {...}, * // "mcp_tool_reference": {...}, * // "server_info": { * // "name": "signalk-mcp-server", * // "version": "1.0.0", * // "loaded_at": "2023-06-22T10:30:15.123Z" * // } * // } */ getInitialContext() { const contextData = { server_info: { name: this.serverName, version: this.serverVersion, loaded_at: new Date().toISOString(), description: 'SignalK MCP Server - Provides AI agents with access to marine vessel data', }, }; // Load all available resources for (const [uri, content] of this.resources.entries()) { const resourceKey = uri.replace('signalk://', ''); contextData[resourceKey] = content; } return { content: [ { type: 'text', text: JSON.stringify(contextData, null, 2), }, ], }; } /** * Starts the MCP server and begins listening for requests via stdio transport * * Server startup: * - Establishes stdio transport for MCP communication * - Connects MCP server to transport layer * - Logs server startup information to stderr * - Begins processing MCP requests from AI agents * * Transport details: * - Uses stdio (stdin/stdout) for MCP protocol communication * - Stderr used for logging to avoid interfering with MCP protocol * - Server runs indefinitely until process termination * * @returns Promise that resolves when server is running * * @example * // Start the MCP server * const server = new SignalKMCPServer(); * await server.run(); * // Server is now running and accepting MCP requests * * // Server logs will appear on stderr: * // "signalk-mcp-server v1.0.0 running on stdio" */ async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error(`${this.serverName} v${this.serverVersion} running on stdio`); } /** * Gets the underlying MCP Server instance for testing and advanced usage * * Testing usage: * - Allows direct access to MCP server internals * - Enables testing of request handlers and server configuration * - Provides access to server capabilities and metadata * * @returns The MCP Server instance * * @example * // Testing server configuration * const server = new SignalKMCPServer(); * const mcpServer = server.mcpServer; * console.log('Server name:', mcpServer.name); * console.log('Server version:', mcpServer.version); * * // Testing tool handlers * const tools = await mcpServer.request({method: 'tools/list'}); * console.log('Available tools:', tools.tools.length); */ get mcpServer() { return this.server; } /** * Gets the SignalK client instance for testing and direct access * * Testing usage: * - Allows direct access to SignalK client methods * - Enables testing of SignalK connection and data processing * - Provides access to cached vessel data and connection state * * @returns The SignalK client instance * * @example * // Testing SignalK connection * const server = new SignalKMCPServer(); * const client = server.signalkClientInstance; * console.log('Connected:', client.connected); * console.log('Available paths:', client.availablePaths.size); * * // Direct access to vessel data * const vesselState = client.getVesselState(); * console.log('Vessel data:', vesselState.data); * * // Testing event handling * client.on('delta', (delta) => { * console.log('Received delta:', delta); * }); */ get signalkClientInstance() { return this.signalkClient; } /** * Cleans up resources when shutting down the server * * - Clears automatic update intervals * - Disconnects from SignalK server * - Releases any held resources * * @example * // Graceful shutdown * const server = new SignalKMCPServer(); * await server.run(); * * // On shutdown signal * process.on('SIGINT', async () => { * await server.cleanup(); * process.exit(0); * }); */ cleanup() { if (this.signalkClient.connected) { this.signalkClient.disconnect(); } } } //# sourceMappingURL=signalk-mcp-server.js.map