UNPKG

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
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}&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 handleGetVessel