UNPKG

voyage-and-consumption-mcp-server

Version:

Voyage and consumption management server handling vessel position tracking, ETA monitoring, fuel consumption, lube oil consumption, fresh water production and weather data

311 lines 13.6 kB
import { logger } from "../../index.js"; import fetch from 'node-fetch'; // API Configuration - support both UPPER_SNAKE_CASE and camelCase const API_CONFIG = { navtor: { base_url: process.env.NAVTOR_API_BASE || process.env.navtorApiBase || "", username: process.env.NAVTOR_USERNAME || process.env.navtorUsername || "", password: process.env.NAVTOR_PASSWORD || process.env.navtorPassword || "", client_id: process.env.NAVTOR_CLIENT_ID || process.env.navtorClientId || "", client_secret: process.env.NAVTOR_CLIENT_SECRET || process.env.navtorClientSecret || "" }, siya: { base_url: process.env.SIYA_API_BASE || process.env.siyaApiBase || "", api_key: process.env.SIYA_API_KEY || process.env.siyaApiKey || "" }, stormglass: { base_url: process.env.STORMGLASS_API_BASE || process.env.stormglassApiBase || "", api_key: process.env.STORMGLASS_API_KEY || process.env.stormglassApiKey || "" } }; // NAVTOR authentication cache const NAVTOR_AUTH = { token: null, expires: 0 }; class NavtorServiceError extends Error { constructor(message, original_error) { super(message); this.original_error = original_error; } } class StormglassServiceError extends Error { constructor(message, original_error) { super(message); this.original_error = original_error; } } class VesselPositionError extends Error { constructor(imo) { super(`Vessel position data not available for IMO ${imo}`); this.imo = imo; } } class MissingParameterError extends Error { constructor(parameter, toolName) { super(`Missing required parameter: ${parameter} for tool: ${toolName}.`); } } class WeatherDataError extends Error { constructor(coordinates, message) { super(message); this.coordinates = coordinates; } } // Utility Functions // Debug function to check NAVTOR configuration (without exposing sensitive data) function debugNavtorConfig() { logger.info("=== NAVTOR Configuration Debug ==="); logger.info(`Base URL: ${API_CONFIG.navtor.base_url}`); logger.info(`Username: ${API_CONFIG.navtor.username ? `${API_CONFIG.navtor.username.substring(0, 3)}***` : 'NOT SET'}`); logger.info(`Password: ${API_CONFIG.navtor.password ? '***SET***' : 'NOT SET'}`); logger.info(`Client ID: ${API_CONFIG.navtor.client_id ? `${API_CONFIG.navtor.client_id.substring(0, 8)}***` : 'NOT SET'}`); logger.info(`Client Secret: ${API_CONFIG.navtor.client_secret ? '***SET***' : 'NOT SET'}`); logger.info("=== End NAVTOR Configuration Debug ==="); } async function getNavtorToken() { const currentTime = Math.floor(Date.now() / 1000); // Check if we have a valid token if (NAVTOR_AUTH.token && NAVTOR_AUTH.expires > currentTime + 60) { logger.info("Using existing NAVTOR token"); return NAVTOR_AUTH.token; } // Debug configuration on first attempt debugNavtorConfig(); // Validate required credentials if (!API_CONFIG.navtor.username || !API_CONFIG.navtor.password) { throw new NavtorServiceError("NAVTOR username and password are required. Please set NAVTOR_USERNAME and NAVTOR_PASSWORD environment variables or use --navtor-username and --navtor-password CLI arguments."); } if (!API_CONFIG.navtor.client_id || !API_CONFIG.navtor.client_secret) { throw new NavtorServiceError("NAVTOR client credentials are required. Please set NAVTOR_CLIENT_ID and NAVTOR_CLIENT_SECRET environment variables or use --navtor-client-id and --navtor-client-secret CLI arguments."); } // Get a new token logger.info("Getting new NAVTOR OAuth token"); logger.info(`Using NAVTOR base URL: ${API_CONFIG.navtor.base_url}`); logger.info(`Using client_id: ${API_CONFIG.navtor.client_id.substring(0, 8)}...`); const tokenUrl = `${API_CONFIG.navtor.base_url}/Token`; const data = new URLSearchParams({ grant_type: "password", username: API_CONFIG.navtor.username, password: API_CONFIG.navtor.password, client_id: API_CONFIG.navtor.client_id, client_secret: API_CONFIG.navtor.client_secret }); try { logger.info(`Trying NAVTOR auth URL: ${tokenUrl}`); const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: data }); const responseText = await response.text(); if (!response.ok) { logger.error(`NAVTOR auth failed with status ${response.status}: ${responseText}`); if (response.status === 400 && responseText.includes("invalid_client")) { throw new Error(`Invalid NAVTOR client credentials. Please verify your NAVTOR_CLIENT_ID and NAVTOR_CLIENT_SECRET are correct. Status: ${response.status}, Response: ${responseText}`); } else if (response.status === 401) { throw new Error(`Invalid NAVTOR username/password. Please verify your NAVTOR_USERNAME and NAVTOR_PASSWORD are correct. Status: ${response.status}, Response: ${responseText}`); } else { throw new Error(`HTTP ${response.status}: ${responseText}`); } } let authData; try { authData = JSON.parse(responseText); } catch (parseError) { throw new Error(`Invalid JSON response from NAVTOR: ${responseText}`); } const accessToken = authData.access_token; const expiresIn = authData.expires_in || 3600; if (!accessToken) { throw new Error(`No access token received from NAVTOR. Response: ${responseText}`); } // Cache the token NAVTOR_AUTH.token = accessToken; NAVTOR_AUTH.expires = currentTime + expiresIn; logger.info("Successfully obtained NAVTOR token"); return accessToken; } catch (error) { logger.error(`NAVTOR authentication failed: ${error}`); throw new NavtorServiceError(`Authentication failed: ${error}`); } } async function makeApiRequest(baseUrl, endpoint, method = "GET", data, authService, apiKey, timeout = 30000) { const url = `${baseUrl}/${endpoint.replace(/^\//, '')}`; const headers = { "Content-Type": "application/json" }; // Add authentication if specified if (authService) { if (authService === "navtor") { const token = await getNavtorToken(); headers["Authorization"] = `Bearer ${token}`; } else { const serviceConfig = API_CONFIG[authService]; const key = apiKey || serviceConfig?.api_key; if (key) { if (authService === "stormglass") { headers["Authorization"] = key; } else if (authService === "siya") { headers["Authorization"] = `Bearer ${key}`; } else { headers["X-API-Key"] = key; } } } } try { logger.info(`Making ${method} request to ${url}`); const requestOptions = { method, headers, timeout }; if (method.toUpperCase() === "POST" && data) { requestOptions.body = JSON.stringify(data); } const response = await fetch(url, requestOptions); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${await response.text()}`); } return await response.json(); } catch (error) { logger.error(`API request failed: ${error}`); throw error; } } export class VesselPositionToolHandler { constructor() { } async handleGetLiveWeatherByCoordinates(args) { const latitude = args.latitude; const longitude = args.longitude; const timestamp = args.timestamp; if (!latitude) { throw new MissingParameterError("latitude", "get_live_weather_by_coordinates"); } if (!longitude) { throw new MissingParameterError("longitude", "get_live_weather_by_coordinates"); } if (!timestamp) { throw new MissingParameterError("timestamp", "get_live_weather_by_coordinates"); } try { // Convert to appropriate types with validation const parsedLat = typeof latitude === 'string' ? parseFloat(latitude) : latitude; const parsedLng = typeof longitude === 'string' ? parseFloat(longitude) : longitude; let parsedTime = timestamp; // Additional coordinate validation for safety const validatedLat = parsedLat; const validatedLng = parsedLng; const sanitizedTimestamp = parsedTime; logger.info(`Retrieving weather data for coordinates: lat=${validatedLat}, lng=${validatedLng}, time=${sanitizedTimestamp}`); // Use sanitized timestamp directly (already validated) parsedTime = sanitizedTimestamp; // Prepare the API request with params properly formatted // Using only valid Stormglass API parameters const params = [ "airTemperature", "windSpeed", "windDirection", "pressure", "humidity", "visibility", "waveHeight", "waveDirection", "wavePeriod", "swellHeight", "swellDirection", "swellPeriod", "waterTemperature", "currentSpeed", "currentDirection" ]; const paramsStr = params.join(","); // Make API request to Stormglass with validated coordinates const endpoint = `weather/point?lat=${validatedLat}&lng=${validatedLng}&params=${paramsStr}&start=${parsedTime}&end=${parsedTime}`; const response = await makeApiRequest(API_CONFIG.stormglass.base_url, endpoint, "GET", undefined, "stormglass"); // Check if response data exists and has necessary fields if (!response || !response.hours || response.hours.length === 0) { throw new WeatherDataError(`${validatedLat},${validatedLng}`, `No weather data available for coordinates (${validatedLat}, ${validatedLng}) at time ${parsedTime}`); } // Format the results as JSON const formattedText = JSON.stringify(response, null, 2); return [ { type: "text", text: formattedText, title: `Live weather data for coordinates (${validatedLat}, ${validatedLng}) at time ${parsedTime}`, format: "json" } ]; } catch (error) { if (error instanceof WeatherDataError || error instanceof MissingParameterError) { return [{ type: "text", text: `Error: ${error.message}` }]; } if (error instanceof StormglassServiceError) { return [{ type: "text", text: `Error with Stormglass service: ${error.message}` }]; } const errorMsg = `Failed to retrieve weather data for coordinates (${latitude}, ${longitude}): ${error}`; logger.error(errorMsg); return [{ type: "text", text: `Error: ${errorMsg}` }]; } } async handleGetVesselLivePositionAndEta(args) { const { imo } = args; try { // Basic IMO validation if (!imo || typeof imo !== 'string' || imo.length < 7) { throw new Error(`Invalid IMO number: ${imo}`); } logger.info(`Retrieving live position for vessel with IMO: ${imo}`); // Get authentication token directly const token = await getNavtorToken(); // Make direct API request to NAVTOR const endpoint = `api/v1/vessels/${imo}/reports/status`; const url = `${API_CONFIG.navtor.base_url}/${endpoint}`; const response = await fetch(url, { headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${await response.text()}`); } // Parse response const vesselData = await response.json(); // Check if response data exists and has necessary fields if (!vesselData || typeof vesselData !== 'object' || Object.keys(vesselData).length === 0) { throw new VesselPositionError(String(imo)); } // Return response return [{ type: "text", text: JSON.stringify(vesselData, null, 2), title: `Live position and ETA for vessel ${imo}`, format: "json" }]; } catch (error) { logger.error(`Error retrieving vessel position for IMO ${imo}: ${error}`); return [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, title: "Error", format: "text" }]; } } } //# sourceMappingURL=vesselPositionTools.js.map