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
JavaScript
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}¶ms=${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