voyage-and-consumption-mcp-server
Version:
Voyage and consumption management server handling vessel voyages, fuel consumption, performance monitoring, and operational data with ERP access for data extraction
1,030 lines • 86.6 kB
JavaScript
import { logger } from "../utils/logger.js";
import { config } from "../utils/config.js";
import { validateAndParseImoForDatabase, validateAndParseImoForApi, validateAndParseImoForPosition } from "../utils/imo-validator.js";
import { createVesselDataResponse, createDualResponse } from "../utils/response-formatter.js";
import { withErrorHandling } from "../middleware/error-handler.js";
import { executeVesselDataQuery } from "../database/query-builder.js";
import { validationMiddleware } from "../middleware/validation-middleware.js";
import { SanitizationMiddleware } from "../middleware/sanitization-middleware.js";
import { validateImoNumber, shouldBypassImoFiltering } from "../utils/imoUtils.js";
import { filterResponseByCompanyImos, filterSearchResultsByCompanyImo } from "../utils/responseFilter.js";
import fetch from 'node-fetch';
// API Configuration
const API_CONFIG = {
navtor: {
base_url: config.navtorApiBase || "https://api.navtor.com",
username: config.navtorUsername || "",
password: config.navtorPassword || "",
client_id: config.navtorClientId || "",
client_secret: config.navtorClientSecret || ""
},
siya: {
base_url: config.siyaApiBase || "https://app-api.siya.com",
api_key: config.siyaApiKey || ""
},
stormglass: {
base_url: config.stormglassApiBase || "https://api.stormglass.io/v2",
api_key: config.stormglassApiKey || ""
}
};
// NAVTOR authentication cache
const NAVTOR_AUTH = {
token: null,
expires: 0
};
// Exception classes
class MissingParameterError extends Error {
constructor(param, tool_name) {
super(`Missing required parameter '${param}' for tool '${tool_name}'`);
this.param = param;
this.tool_name = tool_name;
}
}
class NavtorServiceError extends Error {
constructor(message, original_error) {
super(message);
this.original_error = original_error;
}
}
class SiyaServiceError 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 VesselFuelConsumptionError extends Error {
constructor(imo) {
super(`Vessel fuel consumption data not available for IMO ${imo}`);
this.imo = imo;
}
}
class VesselEtaError extends Error {
constructor(imo) {
super(`Vessel ETA data not available for IMO ${imo}`);
this.imo = imo;
}
}
class WeatherDataError extends Error {
constructor(coordinates, message) {
super(message || `Weather data not available for coordinates ${coordinates}`);
this.coordinates = coordinates;
}
}
class SearchError extends Error {
constructor(query, message, original_error) {
super(message || `Search failed for query: ${query}`);
this.query = query;
this.original_error = original_error;
}
}
class MongoDBError extends Error {
constructor(message, original_error) {
super(message);
this.original_error = original_error;
}
}
// 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;
}
}
function getArtifact(functionName, url) {
return {
id: "msg_browser_ghi789",
parentTaskId: `task_${functionName}_${Date.now()}`,
timestamp: Math.floor(Date.now() / 1000),
agent: {
id: "agent_siya_browser",
name: "SIYA",
type: "qna"
},
messageType: "action",
action: {
tool: "browser",
operation: "browsing",
params: {
url: url,
pageTitle: `Tool response for ${functionName}`,
visual: {
icon: "browser",
color: "#2D8CFF"
},
stream: {
type: "vnc",
streamId: "stream_browser_1",
target: "browser"
}
}
},
content: `Viewed page: ${functionName}`,
artifacts: [
{
id: "artifact_webpage_1746018877304_994",
type: "browser_view",
content: {
url: url,
title: functionName,
screenshot: "",
textContent: `Observed output of cmd \`${functionName}\` executed:`,
extractedInfo: {}
},
metadata: {
domainName: "example.com",
visitTimestamp: Date.now(),
category: "web_page"
}
}
],
status: "completed"
};
}
async function getDataLink(data) {
const url = "https://app-api.siya.com/v1.0/vessel-info/qna-snapshot";
const headers = {
"Content-Type": "application/json",
"Authorization": `Bearer ${config.siyaApiKey || ''}`
};
const payload = {
data: data
};
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload)
});
if (!response.ok) {
logger.error(`Failed to get data link: HTTP ${response.status}`);
return null;
}
const responseData = await response.json();
if (responseData.status === "OK") {
return responseData.resultData;
}
else {
logger.error(`Data link API returned non-OK status: ${responseData.status}`);
return null;
}
}
catch (error) {
logger.error(`Error getting data link: ${error}`);
return null;
}
}
async function getVesselQnaSnapshot(imoNumber, questionNo) {
const snapshotUrl = `https://app-api.siya.com/v1.0/vessel-info/qna-snapshot/${imoNumber}/${questionNo}`;
const jwtToken = `Bearer ${config.siyaApiKey}`;
const headers = {
'Authorization': jwtToken
};
try {
const response = await fetch(snapshotUrl, { method: 'GET', headers });
if (!response.ok) {
throw new DOMException(`Request failed with status ${response.status}`, 'FetchError');
}
const data = await response.json();
if ('resultData' in data) {
return data.resultData;
}
return data;
}
catch (error) {
console.error('Error fetching vessel QnA snapshot:', error);
return null;
}
}
export class ToolHandler {
constructor(server, databaseService) {
this.server = server;
this.databaseService = databaseService;
}
async handleCallTool(name, arguments_) {
try {
// Validate input arguments using validation middleware
const validatedArgs = validationMiddleware.validateToolInput(name, arguments_);
logger.info(`Tool ${name} arguments validated successfully`);
// Define tools requiring IMO pre-validation
const imoRequiredTools = [
"get_live_position_from_navtor",
"get_vessel_fuel_consumption_rob",
"get_vessel_eta_cargo_activity",
"get_voyage_details_from_shippalm",
"get_me_cylinder_oil_consumption_and_rob",
"get_mecc_aecc_consumption_and_rob",
"get_fresh_water_status",
"get_charter_party_compliance_status",
"get_vessel_fuel_consumption_history",
"get_vessel_fresh_water_history",
"get_vessel_mecc_history",
"meclo_historical_data",
"get_vessel_aecc_history",
"get_fleet_eta_cargo_activity",
"get_fleet_vessels_cii_rating",
"get_fleet_charter_party_compliance_status"
];
// Layer 1: Pre-validation for IMO-required tools
if (imoRequiredTools.includes(name) && !shouldBypassImoFiltering(config.companyName || "")) {
const imoParam = validatedArgs.imo || validatedArgs.filters?.imo;
if (!imoParam) {
return [{ type: "text", text: "IMO number is required for this operation" }];
}
const validation = validateImoNumber(imoParam, config.companyName || "");
if (!validation.isValid) {
return [{ type: "text", text: validation.errorMessage || "Invalid IMO number" }];
}
}
// Layer 2: Execute tool
let response;
switch (name) {
case 'get_live_position_from_navtor':
response = await this.handleGetVesselLivePositionAndEta(validatedArgs);
break;
case 'get_vessel_fuel_consumption_rob':
response = await this.handleGetVesselFuelConsumptionRob(validatedArgs);
break;
case 'get_vessel_eta_cargo_activity':
response = await this.handleGetVesselEtaCargoActivity(validatedArgs);
break;
case 'get_voyage_details_from_shippalm':
response = await this.handleGetVoyageDetailsFromShippalm(validatedArgs);
break;
case 'get_me_cylinder_oil_consumption_and_rob':
response = await this.handleGetMeCylinderOilConsumptionAndRob(validatedArgs);
break;
case 'get_mecc_aecc_consumption_and_rob':
response = await this.handleGetMeccAeccConsumptionAndRob(validatedArgs);
break;
case 'get_fresh_water_status':
response = await this.handleGetFreshWaterStatus(validatedArgs);
break;
case 'get_charter_party_compliance_status':
response = await this.handleGetCharterPartyComplianceStatus(validatedArgs);
break;
case 'get_vessel_fuel_consumption_history':
response = await this.handleGetVesselFuelConsumptionHistory(validatedArgs);
break;
case 'get_vessel_fresh_water_history':
response = await this.handleGetVesselFreshWaterHistory(validatedArgs);
break;
case 'get_vessel_mecc_history':
response = await this.handleGetVesselMeccHistory(validatedArgs);
break;
case 'meclo_historical_data':
response = await this.handleMecloHistoricalData(validatedArgs);
break;
case 'get_vessel_aecc_history':
response = await this.handleGetVesselAeccHistory(validatedArgs);
break;
case 'get_live_weather_by_coordinates':
response = await this.handleGetLiveWeatherByCoordinates(validatedArgs);
break;
case 'get_vessel_details':
response = await this.handleGetVesselDetails(validatedArgs);
break;
case 'universal_voyage_search':
response = await this.handleSmartVoyageSearch(validatedArgs);
break;
case 'get_fleet_eta_cargo_activity':
response = await this.handleGetFleetEtaCargoActivity(validatedArgs);
break;
case 'get_fleet_vessels_cii_rating':
response = await this.handleGetFleetVesselsCiiRating(validatedArgs);
break;
case 'get_fleet_charter_party_compliance_status':
response = await this.handleGetFleetCharterPartyComplianceStatus(validatedArgs);
break;
case 'write_casefile_data':
response = await this.handleWriteCasefileData(validatedArgs);
break;
case 'retrieve_casefile_data':
response = await this.handleRetrieveCasefileData(validatedArgs);
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
// Layer 3: Universal post-response filtering
const startTime = Date.now();
const filteredResponse = await filterResponseByCompanyImos(response);
const filteringTime = Date.now() - startTime;
// Monitoring and logging
logger.debug(`Tool ${name} completed with response filtering`, {
toolName: name,
companyName: config.companyName,
filteringTimeMs: filteringTime,
originalResponseLength: response.length,
filteredResponseLength: filteredResponse.length,
hasImoFiltering: !shouldBypassImoFiltering(config.companyName || "")
});
return filteredResponse;
}
catch (error) {
logger.error(`Error calling tool ${name}: ${error}`);
throw new Error(`Error calling tool ${name}: ${error}`);
}
}
async handleGetVesselLivePositionAndEta(args) {
const { imo } = args;
const toolName = "get_live_position_from_navtor";
return withErrorHandling(async () => {
// Validate IMO using utility (basic validation for position)
const validatedImo = validateAndParseImoForPosition(imo, toolName);
logger.info(`Retrieving live position for vessel with IMO: ${validatedImo}`);
// Get authentication token directly
const token = await getNavtorToken();
// Make direct API request to NAVTOR
const endpoint = `api/v1/vessels/${validatedImo}/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(validatedImo));
}
// Return single response using utility
return createVesselDataResponse(vesselData, "Live position and ETA", validatedImo);
}, toolName, imo);
}
async handleGetVesselFuelConsumptionRob(args) {
const { imo } = args;
const toolName = "get_vessel_fuel_consumption_rob";
return withErrorHandling(async () => {
// Validate and parse IMO using utility
const parsedImo = validateAndParseImoForApi(imo, toolName);
logger.info(`Fetching fuel consumption data for vessel with IMO: ${parsedImo}`);
// Make API request to SIYA
const response = await makeApiRequest(API_CONFIG.siya.base_url, "/v1.0/vessel-info/vessel-app/", "POST", {
imo: parsedImo,
qNo: [34]
}, "siya");
// Validate the response
if (!response || !response.resultData) {
throw new VesselFuelConsumptionError(String(parsedImo));
}
// Get vessel snapshot link
const link = await getVesselQnaSnapshot(String(parsedImo), "34");
const artifactData = getArtifact(toolName, link);
// Return dual response using utility
return createDualResponse(response, artifactData, "Fuel consumption data", parsedImo);
}, toolName, imo);
}
async handleGetVesselEtaCargoActivity(args) {
const { imo } = args;
const toolName = "get_vessel_eta_cargo_activity";
return withErrorHandling(async () => {
// Validate and parse IMO using utility
const parsedImo = validateAndParseImoForApi(imo, toolName);
logger.info(`Fetching ETA data for vessel with IMO: ${parsedImo}`);
// Make API request to SIYA
const response = await makeApiRequest(API_CONFIG.siya.base_url, "/v1.0/vessel-info/vessel-app/", "POST", {
imo: parsedImo,
qNo: [32]
}, "siya");
// Validate the response
if (!response || !response.resultData) {
throw new VesselEtaError(String(parsedImo));
}
// Get vessel snapshot link
const link = await getVesselQnaSnapshot(String(parsedImo), "32");
const artifactData = getArtifact(toolName, link);
// Return dual response using utility
return createDualResponse(response, artifactData, "ETA data from emails", parsedImo);
}, toolName, imo);
}
async handleGetVoyageDetailsFromShippalm(args) {
const { imo } = args;
const toolName = "get_voyage_details_from_shippalm";
return withErrorHandling(async () => {
// Validate and parse IMO using utility
const parsedImo = validateAndParseImoForApi(imo, toolName);
logger.info(`Fetching voyage details for vessel with IMO: ${parsedImo}`);
// Make API request to SIYA
const response = await makeApiRequest(API_CONFIG.siya.base_url, "/v1.0/vessel-info/vessel-app/", "POST", {
imo: parsedImo,
qNo: [31]
}, "siya");
// Validate the response
if (!response || !response.resultData) {
throw new Error(`No voyage details found for IMO ${parsedImo}`);
}
// Get vessel snapshot link
const link = await getVesselQnaSnapshot(String(parsedImo), "31");
const artifactData = getArtifact(toolName, link);
// Return dual response using utility
return createDualResponse(response, artifactData, "Voyage details from Shippalm", parsedImo);
}, toolName, imo);
}
async handleGetMeCylinderOilConsumptionAndRob(args) {
const imo = args.imo;
if (!imo || (typeof imo === 'string' && imo.trim() === "")) {
throw new MissingParameterError("imo", "get_me_cylinder_oil_consumption_and_rob");
}
try {
const parsedImo = typeof imo === 'string' && /^\d+$/.test(imo) ? parseInt(imo) : imo;
logger.info(`Fetching ME cylinder oil data for vessel with IMO: ${parsedImo}`);
// Make API request to SIYA
const response = await makeApiRequest(API_CONFIG.siya.base_url, "/v1.0/vessel-info/vessel-app/", "POST", {
imo: parsedImo,
qNo: [35]
}, "siya");
// Validate the response
if (!response || !response.resultData) {
throw new Error(`No ME cylinder oil data found for IMO ${parsedImo}`);
}
// Format the results as JSON
const formattedText = JSON.stringify(response, null, 2);
const link = await getVesselQnaSnapshot(String(parsedImo), "35");
const artifactData = getArtifact("get_me_cylinder_oil_consumption_and_rob", link);
return [
{
type: "text",
text: formattedText,
title: `ME cylinder oil consumption and ROB for vessel with IMO ${parsedImo}`,
format: "json"
},
{
type: "text",
text: JSON.stringify(artifactData, null, 2),
title: `ME cylinder oil consumption and ROB for vessel with IMO ${parsedImo}`,
format: "json"
}
];
}
catch (error) {
if (error instanceof MissingParameterError) {
return [{ type: "text", text: `Error: ${error.message}` }];
}
if (error instanceof SiyaServiceError) {
return [{ type: "text", text: `Error with SIYA service: ${error.message}` }];
}
const errorMsg = `Failed to fetch ME cylinder oil data for IMO ${imo}: ${error}`;
logger.error(errorMsg);
return [{ type: "text", text: `Error: ${errorMsg}` }];
}
}
async handleGetMeccAeccConsumptionAndRob(args) {
const imo = args.imo;
if (!imo || (typeof imo === 'string' && imo.trim() === "")) {
throw new MissingParameterError("imo", "get_mecc_aecc_consumption_and_rob");
}
try {
const parsedImo = typeof imo === 'string' && /^\d+$/.test(imo) ? parseInt(imo) : imo;
logger.info(`Fetching MECC/AECC oil data for vessel with IMO: ${parsedImo}`);
// Make API request to SIYA
const response = await makeApiRequest(API_CONFIG.siya.base_url, "/v1.0/vessel-info/vessel-app/", "POST", {
imo: parsedImo,
qNo: [36]
}, "siya");
// Validate the response
if (!response || !response.resultData) {
throw new Error(`No MECC/AECC oil data found for IMO ${parsedImo}`);
}
// Format the results as JSON
const formattedText = JSON.stringify(response, null, 2);
const link = await getVesselQnaSnapshot(String(parsedImo), "36");
const artifactData = getArtifact("get_mecc_aecc_consumption_and_rob", link);
return [
{
type: "text",
text: formattedText,
title: `MECC/AECC oil consumption and ROB for vessel with IMO ${parsedImo}`,
format: "json"
},
{
type: "text",
text: JSON.stringify(artifactData, null, 2),
title: `MECC/AECC oil consumption and ROB for vessel with IMO ${parsedImo}`,
format: "json"
}
];
}
catch (error) {
if (error instanceof MissingParameterError) {
return [{ type: "text", text: `Error: ${error.message}` }];
}
if (error instanceof SiyaServiceError) {
return [{ type: "text", text: `Error with SIYA service: ${error.message}` }];
}
const errorMsg = `Failed to fetch MECC/AECC oil data for IMO ${imo}: ${error}`;
logger.error(errorMsg);
return [{ type: "text", text: `Error: ${errorMsg}` }];
}
}
async handleGetFreshWaterStatus(args) {
const imo = args.imo;
if (!imo || (typeof imo === 'string' && imo.trim() === "")) {
throw new MissingParameterError("imo", "get_fresh_water_status");
}
try {
const parsedImo = typeof imo === 'string' && /^\d+$/.test(imo) ? parseInt(imo) : imo;
logger.info(`Fetching fresh-water data for vessel with IMO: ${parsedImo}`);
// Make API request to SIYA
const response = await makeApiRequest(API_CONFIG.siya.base_url, "/v1.0/vessel-info/vessel-app/", "POST", {
imo: parsedImo,
qNo: [37]
}, "siya");
// Validate the response
if (!response || !response.resultData) {
throw new Error(`No fresh-water data found for IMO ${parsedImo}`);
}
// Format the results as JSON
const formattedText = JSON.stringify(response, null, 2);
const link = await getVesselQnaSnapshot(String(parsedImo), "37");
const artifactData = getArtifact("get_fresh_water_status", link);
return [
{
type: "text",
text: formattedText,
title: `Fresh-water production, consumption, and ROB for vessel with IMO ${parsedImo}`,
format: "json"
},
{
type: "text",
text: JSON.stringify(artifactData, null, 2),
title: `Fresh-water production, consumption, and ROB for vessel with IMO ${parsedImo}`,
format: "json"
}
];
}
catch (error) {
if (error instanceof MissingParameterError) {
return [{ type: "text", text: `Error: ${error.message}` }];
}
if (error instanceof SiyaServiceError) {
return [{ type: "text", text: `Error with SIYA service: ${error.message}` }];
}
const errorMsg = `Failed to fetch fresh-water data for IMO ${imo}: ${error}`;
logger.error(errorMsg);
return [{ type: "text", text: `Error: ${errorMsg}` }];
}
}
async handleGetCharterPartyComplianceStatus(args) {
const imo = args.imo;
if (!imo || (typeof imo === 'string' && imo.trim() === "")) {
throw new MissingParameterError("imo", "get_charter_party_compliance_status");
}
try {
const parsedImo = typeof imo === 'string' && /^\d+$/.test(imo) ? parseInt(imo) : imo;
logger.info(`Fetching charter-party compliance status for vessel with IMO: ${parsedImo}`);
// Make API request to SIYA
const response = await makeApiRequest(API_CONFIG.siya.base_url, "/v1.0/vessel-info/vessel-app/", "POST", {
imo: parsedImo,
qNo: [33]
}, "siya");
// Validate the response
if (!response || !response.resultData) {
throw new Error(`No charter-party compliance status found for IMO ${parsedImo}`);
}
// Process the response to convert ag-grid URLs to markdown tables
let processedResponse = { ...response };
if (response.resultData && Array.isArray(response.resultData)) {
const imoNumber = typeof parsedImo === 'number' ? parsedImo : parseInt(String(parsedImo));
processedResponse.resultData = await Promise.all(response.resultData.map(async (item) => {
if (item.answer) {
return {
...item,
answer: await addComponentData(item.answer, imoNumber, this.databaseService)
};
}
return item;
}));
}
// Format the results as JSON
const formattedText = JSON.stringify(processedResponse, null, 2);
const link = await getVesselQnaSnapshot(String(parsedImo), "33");
const artifactData = getArtifact("get_charter_party_compliance_status", link);
return [
{
type: "text",
text: formattedText,
title: `Charter-party compliance status for vessel with IMO ${parsedImo}`,
format: "json"
},
{
type: "text",
text: JSON.stringify(artifactData, null, 2),
title: `Charter-party compliance status for vessel with IMO ${parsedImo}`,
format: "json"
}
];
}
catch (error) {
if (error instanceof MissingParameterError) {
return [{ type: "text", text: `Error: ${error.message}` }];
}
if (error instanceof SiyaServiceError) {
return [{ type: "text", text: `Error with SIYA service: ${error.message}` }];
}
const errorMsg = `Failed to fetch charter-party compliance status for IMO ${imo}: ${error}`;
logger.error(errorMsg);
return [{ type: "text", text: `Error: ${errorMsg}` }];
}
}
async handleGetFleetEtaCargoActivity(args) {
const { imo } = args;
const toolName = "get_fleet_eta_cargo_activity";
return withErrorHandling(async () => {
// Validate and parse IMO using utility
const parsedImo = validateAndParseImoForApi(imo, toolName);
logger.info(`Fetching ETA and cargo activity data for fleet with IMO: ${parsedImo}`);
// Make API request to SIYA
const response = await makeApiRequest(API_CONFIG.siya.base_url, "/v1.0/vessel-info/vessel-app/", "POST", {
imo: parsedImo,
qNo: [58]
}, "siya");
// Validate the response
if (!response || !response.resultData) {
throw new Error(`No ETA and cargo activity data found for IMO ${parsedImo}`);
}
// Get vessel snapshot link
const link = await getVesselQnaSnapshot(String(parsedImo), "58");
const artifactData = getArtifact(toolName, link);
// Return dual response using utility
return createDualResponse(response, artifactData, "ETA and cargo activity data", parsedImo);
}, toolName, imo);
}
async handleGetFleetVesselsCiiRating(args) {
const { imo } = args;
const toolName = "get_fleet_vessels_cii_rating";
return withErrorHandling(async () => {
// Validate and parse IMO using utility
const parsedImo = validateAndParseImoForApi(imo, toolName);
logger.info(`Fetching CII rating data for fleet with IMO: ${parsedImo}`);
// Make API request to SIYA
const response = await makeApiRequest(API_CONFIG.siya.base_url, "/v1.0/vessel-info/vessel-app/", "POST", {
imo: parsedImo,
qNo: [205]
}, "siya");
// Validate the response
if (!response || !response.resultData) {
throw new Error(`No CII rating data found for IMO ${parsedImo}`);
}
// Get vessel snapshot link
const link = await getVesselQnaSnapshot(String(parsedImo), "205");
const artifactData = getArtifact(toolName, link);
// Return dual response using utility
return createDualResponse(response, artifactData, "CII rating data", parsedImo);
}, toolName, imo);
}
async handleGetFleetCharterPartyComplianceStatus(args) {
const { imo } = args;
const toolName = "get_fleet_charter_party_compliance_status";
return withErrorHandling(async () => {
// Validate and parse IMO using utility
const parsedImo = validateAndParseImoForApi(imo, toolName);
logger.info(`Fetching charter-party compliance status for fleet with IMO: ${parsedImo}`);
// Make API request to SIYA
const response = await makeApiRequest(API_CONFIG.siya.base_url, "/v1.0/vessel-info/vessel-app/", "POST", {
imo: parsedImo,
qNo: [233]
}, "siya");
// Validate the response
if (!response || !response.resultData) {
throw new Error(`No charter-party compliance status found for IMO ${parsedImo}`);
}
// Get vessel snapshot link
const link = await getVesselQnaSnapshot(String(parsedImo), "233");
const artifactData = getArtifact(toolName, link);
// Return dual response using utility
return createDualResponse(response, artifactData, "Charter-party compliance status", parsedImo);
}, toolName, imo);
}
// Historical data methods
async handleGetVesselFuelConsumptionHistory(args) {
const { imo, start_date: startDate, end_date: endDate, session_id: sessionId = "testing" } = args;
const toolName = "get_vessel_fuel_consumption_history";
return withErrorHandling(async () => {
// Validate and parse IMO using utility
const parsedImo = validateAndParseImoForDatabase(imo, toolName);
logger.info(`Fetching fuel consumption history for vessel with IMO: ${parsedImo} using connection pooling`);
// Use connection pooling via database service
const collection = await this.databaseService.getCollection('common_consumption_log_api', true);
// Execute standardized vessel data query
const documents = await executeVesselDataQuery(collection, parsedImo, 'fuel', { startDate, endDate });
// Handle no data found case
if (!documents || documents.length === 0) {
return createVesselDataResponse(`No fuel consumption data found for vessel with IMO ${parsedImo}`, "Fuel consumption history", parsedImo);
}
// Get data link using the real API
const dataLink = await getDataLink(documents);
// Create artifact data
const artifactData = getArtifact(toolName, dataLink || `https://app-api.siya.com/v1.0/vessel-info/fuel-consumption-history/${parsedImo}/${Date.now()}`);
// Return dual response (main data + artifact)
return createDualResponse(documents, artifactData, "Fuel consumption history", parsedImo);
}, toolName, imo);
}
async handleGetVesselFreshWaterHistory(args) {
const { imo, start_date: startDate, end_date: endDate, session_id: sessionId = "testing" } = args;
const toolName = "get_vessel_fresh_water_history";
return withErrorHandling(async () => {
// Validate and parse IMO using utility
const parsedImo = validateAndParseImoForDatabase(imo, toolName);
logger.info(`Fetching fresh water history for vessel with IMO: ${parsedImo} using connection pooling`);
// Use connection pooling via database service
const collection = await this.databaseService.getCollection('common_consumption_log_api', true);
// Execute standardized vessel data query
const documents = await executeVesselDataQuery(collection, parsedImo, 'freshwater', { startDate, endDate });
// Handle no data found case
if (!documents || documents.length === 0) {
return createVesselDataResponse(`No fresh water data found for vessel with IMO ${parsedImo}`, "Fresh Water History", parsedImo);
}
// Get data link using the real API
const dataLink = await getDataLink(documents);
// Create artifact data
const artifactData = getArtifact(toolName, dataLink || `https://app-api.siya.com/v1.0/vessel-info/fresh-water-history/${parsedImo}/${Date.now()}`);
// Return dual response (main data + artifact)
return createDualResponse(documents, artifactData, "Fresh Water History", parsedImo);
}, toolName, imo);
}
async handleGetVesselMeccHistory(args) {
const { imo, start_date: startDate, end_date: endDate, session_id: sessionId = "testing" } = args;
const toolName = "get_vessel_mecc_history";
return withErrorHandling(async () => {
// Validate and parse IMO using utility
const parsedImo = validateAndParseImoForDatabase(imo, toolName);
logger.info(`Fetching MECC consumption history for vessel with IMO: ${parsedImo} using connection pooling`);
// Use connection pooling via database service
const collection = await this.databaseService.getCollection('common_consumption_log_api', true);
// Execute standardized vessel data query
const documents = await executeVesselDataQuery(collection, parsedImo, 'mecc', { startDate, endDate });
// Handle no data found case
if (!documents || documents.length === 0) {
return createVesselDataResponse(`No MECC consumption data found for vessel with IMO ${parsedImo}`, "MECC Consumption History", parsedImo);
}
// Get data link using the real API
const dataLink = await getDataLink(documents);
// Create artifact data
const artifactData = getArtifact(toolName, dataLink || `https://app-api.siya.com/v1.0/vessel-info/mecc-history/${parsedImo}/${Date.now()}`);
// Return dual response (main data + artifact)
return createDualResponse(documents, artifactData, "MECC Consumption History", parsedImo);
}, toolName, imo);
}
async handleMecloHistoricalData(args) {
const { imo, start_date: startDate, end_date: endDate, session_id: sessionId = "testing" } = args;
const toolName = "meclo_historical_data";
return withErrorHandling(async () => {
// Validate and parse IMO using utility
const parsedImo = validateAndParseImoForDatabase(imo, toolName);
logger.info(`Fetching ME cylinder oil consumption history for vessel with IMO: ${parsedImo} using connection pooling`);
// Use connection pooling via database service
const collection = await this.databaseService.getCollection('common_consumption_log_api', true);
// Execute standardized vessel data query
const documents = await executeVesselDataQuery(collection, parsedImo, 'cylinder', { startDate, endDate });
// Handle no data found case
if (!documents || documents.length === 0) {
return createVesselDataResponse(`No ME cylinder oil consumption data found for vessel with IMO ${parsedImo}`, "ME Cylinder Oil Consumption History", parsedImo);
}
// Get data link using the real API
const dataLink = await getDataLink(documents);
// Create artifact data
const artifactData = getArtifact(toolName, dataLink || `https://app-api.siya.com/v1.0/vessel-info/meclo-history/${parsedImo}/${Date.now()}`);
// Return dual response (main data + artifact)
return createDualResponse(documents, artifactData, "ME Cylinder Oil Consumption History", parsedImo);
}, toolName, imo);
}
async handleGetVesselAeccHistory(args) {
const { imo, start_date: startDate, end_date: endDate, session_id: sessionId = "testing" } = args;
const toolName = "get_vessel_aecc_history";
return withErrorHandling(async () => {
// Validate and parse IMO using utility
const parsedImo = validateAndParseImoForDatabase(imo, toolName);
logger.info(`Fetching AECC/MECC consumption history for vessel with IMO: ${parsedImo} using connection pooling`);
// Use connection pooling via database service
const collection = await this.databaseService.getCollection('common_consumption_log_api', true);
// Execute standardized vessel data query
const documents = await executeVesselDataQuery(collection, parsedImo, 'aecc', { startDate, endDate });
// Handle no data found case
if (!documents || documents.length === 0) {
return createVesselDataResponse(`No AECC/MECC consumption data found for vessel with IMO ${parsedImo}`, "AECC/MECC Consumption History", parsedImo);
}
// Get data link using the real API
const dataLink = await getDataLink(documents);
// Create artifact data
const artifactData = getArtifact(toolName, dataLink || `https://app-api.siya.com/v1.0/vessel-info/aecc-history/${parsedImo}/${Date.now()}`);
// Return dual response (main data + artifact)
return createDualResponse(documents, artifactData, "AECC/MECC Consumption History", parsedImo);
}, toolName, imo);
}
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 = SanitizationMiddleware.sanitizeCoordinate(parsedLat, 'latitude');
const validatedLng = SanitizationMiddleware.sanitizeCoordinate(parsedLng, 'longitude');
const sanitizedTimestamp = SanitizationMiddleware.sanitizeDateString(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 handleGetVessel