signalk-mcp-server
Version:
MCP server for SignalK marine data integration
927 lines (926 loc) • 38.3 kB
JavaScript
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';
import { IsolateSandbox } from './execution-engine/isolate-sandbox.js';
import { SignalKBinding } from './bindings/signalk-binding.js';
import { prepareSDKForIsolate } from './sdk/generator.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;
executionMode;
sandbox;
binding;
sdkCode;
/**
* 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.executionMode =
options.executionMode ||
process.env.EXECUTION_MODE ||
'code';
this.signalkClient = options.signalkClient || new SignalKClient();
this.server = new Server({
name: this.serverName,
version: this.serverVersion,
}, {
capabilities: {
tools: {},
resources: {},
},
});
// Initialize code execution components if needed
if (this.executionMode === 'code' || this.executionMode === 'hybrid') {
this.sandbox = new IsolateSandbox();
this.binding = new SignalKBinding(this.signalkClient);
// Generate SDK code from tools
const { wrapperCode } = prepareSDKForIsolate(this.getToolDefinitions());
this.sdkCode = wrapperCode;
console.error(`[${this.executionMode.toUpperCase()} MODE] Code execution enabled`);
}
// 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
}
}
/**
* Extract MCP tool definitions in the format expected by SDK generator
*
* @returns Array of MCPTool definitions
*/
/**
* Returns tool definitions for SDK generation
*
* NOTE: Most legacy tools removed in favor of execute_code.
* Only essential utility tools remain for debugging and documentation.
*/
getToolDefinitions() {
return [
{
name: 'get_vessel_state',
description: '⚠️ DEPRECATED: Use execute_code instead for better performance.\n\n' +
'Get current vessel navigation data (position, heading, speed, wind, vessel identity)\n\n' +
'Migration example:\n' +
'```javascript\n' +
'(async () => {\n' +
' const vessel = await getVesselState();\n' +
' return JSON.stringify({\n' +
' name: vessel.data.name?.value,\n' +
' position: vessel.data["navigation.position"]?.value\n' +
' });\n' +
'})();\n' +
'```\n\n' +
'Benefits: 94% fewer tokens, client-side filtering.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
},
{
name: 'get_ais_targets',
description: '⚠️ DEPRECATED: Use execute_code instead for better performance.\n\n' +
'Get nearby AIS targets sorted by distance from self vessel (closest first). Includes distance in meters, position, course, and speed data.\n\n' +
'Migration example:\n' +
'```javascript\n' +
'(async () => {\n' +
' const ais = await getAisTargets({ pageSize: 50 });\n' +
' const nearby = ais.targets.filter(t => t.distanceMeters < 1852);\n' +
' return JSON.stringify({ count: nearby.length, vessels: nearby.slice(0, 5) });\n' +
'})();\n' +
'```\n\n' +
'Benefits: 95% fewer tokens, filter by distance in isolate.',
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: '⚠️ DEPRECATED: Use execute_code instead for better performance.\n\n' +
'Get current system notifications and alerts\n\n' +
'Migration example:\n' +
'```javascript\n' +
'(async () => {\n' +
' const alarms = await getActiveAlarms();\n' +
' const critical = alarms.alarms.filter(a => a.state === "alarm" || a.state === "emergency");\n' +
' return JSON.stringify({ hasCritical: critical.length > 0, count: critical.length });\n' +
'})();\n' +
'```\n\n' +
'Benefits: 90% fewer tokens, filter critical alarms in isolate.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
},
{
name: 'list_available_paths',
description: '⚠️ DEPRECATED: Use execute_code instead for better performance.\n\n' +
'Discover available SignalK data paths\n\n' +
'Migration example:\n' +
'```javascript\n' +
'(async () => {\n' +
' const result = await listAvailablePaths();\n' +
' const navPaths = result.paths.filter(p => p.startsWith("navigation."));\n' +
' return JSON.stringify({ navigation: navPaths });\n' +
'})();\n' +
'```\n\n' +
'Benefits: 92% fewer tokens, filter by path prefix in isolate.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
},
{
name: 'get_path_value',
description: '⚠️ DEPRECATED: Use execute_code instead for better performance.\n\n' +
'Get latest value for a specific SignalK path\n\n' +
'Migration example:\n' +
'```javascript\n' +
'(async () => {\n' +
' const speed = await getPathValue({ path: "navigation.speedOverGround" });\n' +
' const heading = await getPathValue({ path: "navigation.headingTrue" });\n' +
' return JSON.stringify({ speed: speed.data?.value, heading: heading.data?.value });\n' +
'})();\n' +
'```\n\n' +
'Benefits: 96% fewer tokens, query multiple paths in one execution.',
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 connection status and health. Useful for debugging and troubleshooting connectivity issues.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
},
{
name: 'get_initial_context',
description: 'Get comprehensive SignalK context and documentation to understand available data and usage patterns. Call this once at the start of a session to learn about SignalK capabilities.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false,
},
},
];
}
/**
* Registers MCP tool handlers and defines the available tools for AI agents
*
* Execution modes:
* - 'tools': Only legacy tools (backward compatible)
* - 'code': Only execute_code tool (new approach)
* - 'hybrid': Both legacy tools and execute_code (default)
*
* Legacy 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
* - get_initial_context: Comprehensive SignalK documentation
*
* Code execution tool:
* - execute_code: Execute JavaScript code in V8 isolate with SignalK SDK
*
* 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"})
* // - execute_code({"code": "const vessel = await getVesselState(); ..."})
*/
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, () => {
const tools = [];
// Add legacy tools if in 'tools' or 'hybrid' mode
if (this.executionMode === 'tools' || this.executionMode === 'hybrid') {
tools.push(...this.getToolDefinitions());
}
// Add execute_code tool if in 'code' or 'hybrid' mode
if (this.executionMode === 'code' || this.executionMode === 'hybrid') {
tools.push({
name: 'execute_code',
description: 'Execute JavaScript code in a secure V8 isolate with access to SignalK SDK functions. ' +
'IMPORTANT: ALL SDK functions are async and MUST be awaited, including getConnectionStatus(). ' +
'Available functions: await getVesselState(), await getAisTargets(options), ' +
'await getActiveAlarms(), await listAvailablePaths(), await getPathValue(path), await getConnectionStatus(). ' +
'Code MUST: (1) be wrapped in async IIFE, (2) await all SDK calls, (3) return JSON.stringify() of result. ' +
'Example: (async () => { const vessel = await getVesselState(); return JSON.stringify({ name: vessel.data.name?.value }); })()',
inputSchema: {
type: 'object',
properties: {
code: {
type: 'string',
description: 'JavaScript code to execute. Must be wrapped in async IIFE and return JSON.stringify() of result.',
},
},
required: ['code'],
additionalProperties: false,
},
});
}
return { tools };
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
// Handle execute_code tool
if (name === 'execute_code') {
if (this.executionMode === 'tools') {
throw new McpError(ErrorCode.MethodNotFound, 'execute_code tool is not available in tools-only mode');
}
return await this.executeCode(args.code);
}
// Handle legacy tools (only in tools/hybrid mode)
if (this.executionMode === 'tools' || this.executionMode === 'hybrid') {
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}`);
}
}
// In code-only mode, only utility tools are available
switch (name) {
case 'get_connection_status':
return this.getConnectionStatus();
case 'get_initial_context':
return this.getInitialContext();
default:
// Data-fetching tools not available in code mode
throw new McpError(ErrorCode.MethodNotFound, `Tool ${name} is not available in code-only mode. Use execute_code tool with SignalK SDK functions instead. ` +
`Available SDK functions: getVesselState(), getAisTargets(), getActiveAlarms(), listAvailablePaths(), getPathValue()`);
}
}
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 executes JavaScript code in a V8 isolate with SignalK SDK
*
* Execution environment:
* - Secure V8 isolate (128MB memory limit, 30s timeout)
* - SignalK SDK functions auto-injected
* - No access to Node.js globals or filesystem
* - Console.log captured and returned
*
* Available SDK functions:
* - getVesselState()
* - getAisTargets(options?)
* - getActiveAlarms()
* - listAvailablePaths()
* - getPathValue(path)
* - getConnectionStatus()
* - getInitialContext()
*
* Code requirements:
* - Must be wrapped in async IIFE: (async () => { ... })()
* - Must return JSON.stringify() of result object
*
* @param code - JavaScript code to execute
* @returns MCPToolResponse with execution result and logs
*
* @example
* // Called by AI agents via MCP protocol:
* // Tool: execute_code
* // Arguments: {
* // "code": "(async () => { const vessel = await getVesselState(); return JSON.stringify({ name: vessel.data.name?.value }); })()"
* // }
*
* // Response content:
* // {
* // "success": true,
* // "result": "{\"name\":\"My Boat\"}",
* // "logs": ["Calling getVesselState..."],
* // "executionTime": 45
* // }
*/
async executeCode(code) {
if (!this.sandbox || !this.binding || !this.sdkCode) {
throw new McpError(ErrorCode.InternalError, 'Code execution not available - execution mode not configured properly');
}
try {
// Inject SDK and execute code
const wrappedCode = `
${this.sdkCode}
${code}
`;
const result = await this.sandbox.execute(wrappedCode, {
signalk: this.binding,
});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
catch (error) {
throw new McpError(ErrorCode.InternalError, `Code execution 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
*/
async getVesselState() {
const data = await this.signalkClient.getVesselState();
// Add deprecation warning if in hybrid mode
const deprecationNotice = this.executionMode === 'hybrid'
? '\n⚠️ DEPRECATION WARNING: This tool will be removed in a future version. ' +
'Use execute_code with getVesselState() for 94% token savings.\n'
: '';
return {
content: [
{
type: 'text',
text: deprecationNotice + JSON.stringify(data, null, 2),
},
],
};
}
/**
* MCP tool handler that returns nearby AIS targets (other vessels) with position and navigation data
*
* @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
*/
async getAISTargets(page, pageSize) {
const data = await this.signalkClient.getAISTargets(page, pageSize);
// Add deprecation warning if in hybrid mode
const deprecationNotice = this.executionMode === 'hybrid'
? '\n⚠️ DEPRECATION WARNING: This tool will be removed in a future version. ' +
'Use execute_code with getAisTargets() for 95% token savings.\n'
: '';
return {
content: [
{
type: 'text',
text: deprecationNotice + JSON.stringify(data, null, 2),
},
],
};
}
/**
* MCP tool handler that returns current active alarms and system notifications
*
* @returns MCPToolResponse with active alarms as formatted JSON text
*/
async getActiveAlarms() {
const data = await this.signalkClient.getActiveAlarms();
// Add deprecation warning if in hybrid mode
const deprecationNotice = this.executionMode === 'hybrid'
? '\n⚠️ DEPRECATION WARNING: This tool will be removed in a future version. ' +
'Use execute_code with getActiveAlarms() for 90% token savings.\n'
: '';
return {
content: [
{
type: 'text',
text: deprecationNotice + JSON.stringify(data, null, 2),
},
],
};
}
/**
* MCP tool handler that discovers and returns all available SignalK data paths
*
* @returns MCPToolResponse with available paths as formatted JSON text
*/
async listAvailablePaths() {
const data = await this.signalkClient.listAvailablePaths();
// Add deprecation warning if in hybrid mode
const deprecationNotice = this.executionMode === 'hybrid'
? '\n⚠️ DEPRECATION WARNING: This tool will be removed in a future version. ' +
'Use execute_code with listAvailablePaths() for 92% token savings.\n'
: '';
return {
content: [
{
type: 'text',
text: deprecationNotice + JSON.stringify(data, null, 2),
},
],
};
}
/**
* MCP tool handler that gets the latest value for a specific SignalK data path
*
* @param path - SignalK data path in dot notation (e.g., 'navigation.position')
* @returns MCPToolResponse with path value as formatted JSON text
*/
async getPathValue(path) {
const data = await this.signalkClient.getPathValue(path);
// Add deprecation warning if in hybrid mode
const deprecationNotice = this.executionMode === 'hybrid'
? '\n⚠️ DEPRECATION WARNING: This tool will be removed in a future version. ' +
'Use execute_code with getPathValue() for 96% token savings.\n'
: '';
return {
content: [
{
type: 'text',
text: deprecationNotice + 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