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

120 lines 7.07 kB
import { getTypesenseClient, updateTypesenseFilterWithCompanyImos, processTypesenseResults } from "../../utils/index.js"; import { logger } from "../../index.js"; // Define allowed keys for validation (new format only) const UniversalVoyageSearchKeys = [ 'query_by', 'q', 'filter_by', 'sort_by', 'page', 'per_page' ]; export class UniversalToolHandler { constructor() { this.typesenseClient = getTypesenseClient(); } async universalVoyageSearchHandler(arguments_) { const collection = "voyage"; // Validate that only allowed properties are present const providedProperties = Object.keys(arguments_); const invalidProperties = providedProperties.filter(prop => !UniversalVoyageSearchKeys.includes(prop)); if (invalidProperties.length > 0) { throw new Error(`Invalid properties provided: ${invalidProperties.join(', ')}. Only the following properties are allowed: ${UniversalVoyageSearchKeys.join(', ')}`); } // Extract new schema parameters only const args = arguments_; const query_by = args.query_by; const query_text = (args.q || "").trim() || "*"; // Validate that query_by is provided when q is specified if (args.q && args.q.trim() !== "*" && !query_by) { throw new Error("query_by parameter is required when using full-text search with q parameter. Only the following properties are allowed: query_by, q, filter_by, sort_by, page, per_page. Please check the tool schema for universal_voyage_search for the allowed properties."); } const filter_by = args.filter_by || ""; let sort_by = args.sort_by || "relevance"; const page = args.page || 1; const per_page = args.per_page || 50; try { let filterBy = filter_by; // Convert date fields from yyyy-mm-dd to Unix timestamps in filter_by if (filterBy) { const dateFields = ['reportdate', 'reportDate']; for (const dateField of dateFields) { // Handle date range operations (>=, <=, >, <, =) const dateRegexPatterns = [ { pattern: new RegExp(`${dateField}:>=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>=' }, { pattern: new RegExp(`${dateField}:<=(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<=' }, { pattern: new RegExp(`${dateField}:>(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':>' }, { pattern: new RegExp(`${dateField}:<(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':<' }, { pattern: new RegExp(`${dateField}:(\\d{4}-\\d{2}-\\d{2})`, 'g'), operator: ':' } ]; for (const { pattern, operator } of dateRegexPatterns) { filterBy = filterBy.replace(pattern, (match, dateStr) => { try { const timestamp = Math.floor(new Date(dateStr + 'T00:00:00.000Z').getTime() / 1000); // Normalize reportDate to reportdate (lowercase) for Typesense const normalizedField = dateField === 'reportDate' ? 'reportdate' : dateField; return `${normalizedField}${operator}${timestamp}`; } catch (error) { logger.warn(`Failed to convert date ${dateStr} for field ${dateField}:`, error); return match; // Keep original if conversion fails } }); } } // Normalize any remaining reportDate references to reportdate in filter_by filterBy = filterBy.replace(/reportDate/g, 'reportdate'); } // Normalize reportDate to reportdate in sort_by as well if (sort_by && sort_by !== "relevance") { sort_by = sort_by.replace(/reportDate/g, 'reportdate'); } // Apply company IMO restrictions filterBy = await updateTypesenseFilterWithCompanyImos(filterBy || ""); // Set up all available fields for inclusion (based on comprehensive schema) const includeFields = "vesselName,voyageNumber,event,eventtype,fromport,toport,location,vesselActivity,vesselstatus,chartererpartyspeed,chartererpartyconsumption,cargoData,freshWaterData,machineryWiseFuelOilConsumptionData,fuelOilConsumptionRobData,lubeOilConsumptionRobData,imo,createdAt,createdAtIst,reportdate,steamingTime,steaminghours,atPort,timeZone"; const excludeFields = "ownerId,fleetId"; const query = { q: query_text, query_by: query_by || "vesselName", include_fields: includeFields, exclude_fields: excludeFields, page: page, per_page: per_page }; if (filterBy) { query.filter_by = filterBy; } // Handle sorting if (sort_by && sort_by !== "relevance") { query.sort_by = sort_by; } console.log("query", query); console.log("filterBy", filterBy); console.log("sort_by", sort_by); console.log("page", page); console.log("per_page", per_page); logger.debug(`[Typesense Query] ${JSON.stringify(query)}`); const results = await this.typesenseClient.collections(collection).documents().search(query); // Debug: Log the structure of the search results logger.info(`UniversalVoyageSearchHandler - Results structure: ${JSON.stringify(Object.keys(results))}`); if (results.hits && results.hits.length > 0) { const sampleHit = results.hits[0]; logger.info(`UniversalVoyageSearchHandler - Sample hit structure: ${JSON.stringify(Object.keys(sampleHit))}`); } if (!results || !results.hits || results.hits.length === 0) { return [{ type: "text", text: `No voyage records found for query '${query_text}' with query_by fields '${query_by}'.`, title: "No Results Found", format: "json" }]; } // Format results using the utility function let title = `Universal Voyage Search Results for '${query_text}'`; const linkHeader = `Universal voyage search result for query: '${query_text}' in fields: ${query_by}`; return await processTypesenseResults(results, "universal_voyage_search", title, linkHeader); } catch (error) { logger.error('Error performing universal voyage search:', error); throw new Error(`Error performing search: ${error.message}`); } } } //# sourceMappingURL=universalTools.js.map