UNPKG

survey-mcp-server

Version:
419 lines (418 loc) 23.3 kB
import { processTypesenseResults, getTypesenseClient, getVesselImoListFromFleet, fetchQADetailsAndCreateResponse, exportSurveysForImoList, getDataLink, convertToCSV, combined_mongotools_with_grouped_category_mapping } from "./helper_functions.js"; import { updateTypesenseFilterWithCompanyImos, isValidImoForCompany, getAuthorizedImoNumbers, shouldBypassImoFiltering } from "../utils/company-filtering.js"; /** * ============================================================================ * OLD IMPLEMENTATION (COMMENTED OUT - REPLACED BY OPTIMIZED VERSION) * ============================================================================ * The old shouldBypassImoFiltering function has been replaced with an optimized * version from the company-filtering module that uses case-insensitive matching. * * See: src/utils/company-filtering.ts for the optimized implementation * ============================================================================ */ // /** // * Check if IMO filtering should be bypassed for admin companies // */ // function shouldBypassImoFiltering(companyName: string): boolean { // const adminCompanies = ['admin', 'Admin', 'ADMIN', 'SYIA', 'Syia']; // return adminCompanies.includes(companyName); // } export async function export_fleet_surveys(arguments_) { // // logger.info("exportFleetSurveys called", arguments_); const fleetImo = arguments_.fleet_imo; if (!fleetImo) { throw new Error(`fleet_imo parameter is required. Only the following property is allowed: fleet_imo. Please check the tool schema for export_fleet_surveys for the allowed properties.`); } try { // Step 1: Get vessel IMO list from fleet // // logger.info(`Step 1: Getting vessel IMO list for fleet IMO ${fleetImo}`); let vesselImoList = await getVesselImoListFromFleet(fleetImo); if (!vesselImoList || vesselImoList.length === 0) { return { content: [{ type: "text", text: `No vessels found for fleet IMO: ${fleetImo}`, title: "No Vessels Found" }] }; } // Filter IMO list by company const companyName = process.env.COMPANY_NAME || ''; const finalDbName = process.env.GROUP_DETAILS_DB_NAME || process.env.FLEET_DISTRIBUTION_DB_NAME || ''; const finalMongoUri = process.env.GROUP_DETAILS_MONGO_URI || process.env.FLEET_DISTRIBUTION_MONGO_URI || ''; if (companyName && finalDbName && finalMongoUri && companyName !== "Synergy" && !shouldBypassImoFiltering(companyName)) { const companyImos = await getAuthorizedImoNumbers(companyName, finalDbName, finalMongoUri); if (companyImos.length > 0) { const companyImoNumbers = companyImos.map(imo => Number(imo)); vesselImoList = vesselImoList.filter(imo => companyImoNumbers.includes(imo)); } else { // If no company IMOs configured, return empty result return { content: [{ type: "text", text: `No vessels found for fleet IMO: ${fleetImo} (company filtering applied)`, title: "No Vessels Found" }] }; } } if (!vesselImoList || vesselImoList.length === 0) { return { content: [{ type: "text", text: `No vessels found for fleet IMO: ${fleetImo} (company filtering applied)`, title: "No Vessels Found" }] }; } // // logger.info(`Found ${vesselImoList.length} vessels in fleet IMO ${fleetImo}`); // Step 2: Export surveys for all vessels in the fleet // // logger.info(`Step 2: Exporting surveys for ${vesselImoList.length} vessels`); const surveyDocuments = await exportSurveysForImoList(vesselImoList); if (!surveyDocuments || surveyDocuments.length === 0) { return { content: [{ type: "text", text: `No survey/certificate records found for fleet IMO ${fleetImo} with the specified criteria`, title: "No Survey Records Found" }] }; } // // logger.info(`Found ${surveyDocuments.length} survey/certificate records for fleet IMO ${fleetImo}`); // Step 3: Convert to CSV // // logger.info(`Step 3: Converting ${surveyDocuments.length} records to CSV`); const csvData = convertToCSV(surveyDocuments); // Insert tracking data to MongoDB (optional) try { const dataLink = await getDataLink(surveyDocuments); const linkHeader = `Fleet surveys export for fleet IMO ${fleetImo}`; const vesselName = surveyDocuments.length > 0 ? surveyDocuments[0].vesselName : null; } catch (trackingError) { // logger.warn(`Failed to insert tracking data: ${trackingError}`); // Continue execution even if tracking fails } // Return CSV data const content = { type: "text", text: csvData, title: `Fleet Surveys Export - IMO ${fleetImo}`, format: "csv" }; return { content: [content] }; } catch (error) { // logger.error(`Error exporting fleet surveys for IMO ${fleetImo}: ${error}`); throw new Error(`Error exporting fleet surveys: ${String(error)}`); } } export async function get_fleet_dry_docking_status(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error(`imo parameter is required. Only the following property is allowed: imo. Please check the tool schema for get_fleet_dry_docking_status for the allowed properties.`); } // Validate IMO for company const companyName = process.env.COMPANY_NAME || ''; const finalDbName = process.env.GROUP_DETAILS_DB_NAME || process.env.FLEET_DISTRIBUTION_DB_NAME || ''; const finalMongoUri = process.env.GROUP_DETAILS_MONGO_URI || process.env.FLEET_DISTRIBUTION_MONGO_URI || ''; if (companyName && finalDbName && finalMongoUri && companyName !== "Synergy" && !shouldBypassImoFiltering(companyName)) { const isValid = await isValidImoForCompany(imo, companyName, finalDbName, finalMongoUri); if (!isValid) { return { content: [{ type: "text", text: `Access denied: IMO ${imo} is not authorized for company ${companyName}`, title: "Access Denied" }] }; } } // Debug logging // console.log(`getFleetDryDockingStatus called with IMO: ${imo}`); // console.log(`process.env.VESSEL_INFO_DB_NAME: ${process.env.VESSEL_INFO_DB_NAME}`); // console.log(`process.env.VESSEL_INFO_MONGO_URI: ${process.env.VESSEL_INFO_MONGO_URI ? 'Set' : 'Not set'}`); try { const result = await fetchQADetailsAndCreateResponse(imo.toString(), 26, "get_fleet_dry_docking_status", "fleet dry docking status", process.env.VESSEL_INFO_DB_NAME, process.env.VESSEL_INFO_MONGO_URI); return { content: result }; } catch (error) { // logger.error(`Error getting fleet dry docking status for IMO ${imo}:`, error); throw new Error(`Error getting fleet dry docking status: ${error.message}`); } } export async function get_fleet_annual_survey_status(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error(`imo parameter is required. Only the following property is allowed: imo. Please check the tool schema for get_fleet_annual_survey_status for the allowed properties.`); } // Validate IMO for company const companyName = process.env.COMPANY_NAME || ''; const finalDbName = process.env.GROUP_DETAILS_DB_NAME || process.env.FLEET_DISTRIBUTION_DB_NAME || ''; const finalMongoUri = process.env.GROUP_DETAILS_MONGO_URI || process.env.FLEET_DISTRIBUTION_MONGO_URI || ''; if (companyName && finalDbName && finalMongoUri && companyName !== "Synergy" && !shouldBypassImoFiltering(companyName)) { const isValid = await isValidImoForCompany(imo, companyName, finalDbName, finalMongoUri); if (!isValid) { return { content: [{ type: "text", text: `Access denied: IMO ${imo} is not authorized for company ${companyName}`, title: "Access Denied" }] }; } } try { const result = await fetchQADetailsAndCreateResponse(imo.toString(), 27, "get_fleet_annual_survey_status", "fleet annual survey status", process.env.VESSEL_INFO_DB_NAME, process.env.VESSEL_INFO_MONGO_URI); return { content: result }; } catch (error) { // logger.error(`Error getting fleet annual survey status for IMO ${imo}:`, error); throw new Error(`Error getting fleet annual survey status: ${error.message}`); } } export async function get_fleet_ihm_certificate_status(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error(`imo parameter is required. Only the following property is allowed: imo. Please check the tool schema for get_fleet_ihm_certificate_status for the allowed properties.`); } // Validate IMO for company const companyName = process.env.COMPANY_NAME || ''; const finalDbName = process.env.GROUP_DETAILS_DB_NAME || process.env.FLEET_DISTRIBUTION_DB_NAME || ''; const finalMongoUri = process.env.GROUP_DETAILS_MONGO_URI || process.env.FLEET_DISTRIBUTION_MONGO_URI || ''; if (companyName && finalDbName && finalMongoUri && companyName !== "Synergy" && !shouldBypassImoFiltering(companyName)) { const isValid = await isValidImoForCompany(imo, companyName, finalDbName, finalMongoUri); if (!isValid) { return { content: [{ type: "text", text: `Access denied: IMO ${imo} is not authorized for company ${companyName}`, title: "Access Denied" }] }; } } try { const result = await fetchQADetailsAndCreateResponse(imo.toString(), 91, "get_fleet_ihm_certificate_status", "fleet IHM certificate status", process.env.VESSEL_INFO_DB_NAME, process.env.VESSEL_INFO_MONGO_URI); return { content: result }; } catch (error) { // logger.error(`Error getting fleet IHM certificate status for IMO ${imo}:`, error); throw new Error(`Error getting fleet IHM certificate status: ${error.message}`); } } export async function get_fleet_lsa_ffa_certificate_status(arguments_) { const imo = arguments_.imo; if (!imo) { throw new Error(`imo parameter is required. Only the following property is allowed: imo. Please check the tool schema for get_fleet_lsa_ffa_certificate_status for the allowed properties.`); } // Validate IMO for company const companyName = process.env.COMPANY_NAME || ''; const finalDbName = process.env.GROUP_DETAILS_DB_NAME || process.env.FLEET_DISTRIBUTION_DB_NAME || ''; const finalMongoUri = process.env.GROUP_DETAILS_MONGO_URI || process.env.FLEET_DISTRIBUTION_MONGO_URI || ''; if (companyName && finalDbName && finalMongoUri && companyName !== "Synergy" && !shouldBypassImoFiltering(companyName)) { const isValid = await isValidImoForCompany(imo, companyName, finalDbName, finalMongoUri); if (!isValid) { return { content: [{ type: "text", text: `Access denied: IMO ${imo} is not authorized for company ${companyName}`, title: "Access Denied" }] }; } } try { const result = await fetchQADetailsAndCreateResponse(imo.toString(), 109, "get_fleet_lsa_ffa_certificate_status", "fleet LSA/FFA certificate status", process.env.VESSEL_INFO_DB_NAME, process.env.VESSEL_INFO_MONGO_URI); return { content: result }; } catch (error) { // logger.error(`Error getting fleet LSA/FFA certificate status for IMO ${imo}:`, error); throw new Error(`Error getting fleet LSA/FFA certificate status: ${error.message}`); } } export async function get_vessel_survey_certificate_information(args) { // logger.info("getVesselSurveyCertificateInformation called", args); try { // Validate IMO for company if IMO is provided if (args.imo) { const companyName = process.env.COMPANY_NAME || ''; const finalDbName = process.env.GROUP_DETAILS_DB_NAME || process.env.FLEET_DISTRIBUTION_DB_NAME || ''; const finalMongoUri = process.env.GROUP_DETAILS_MONGO_URI || process.env.FLEET_DISTRIBUTION_MONGO_URI || ''; if (companyName && finalDbName && finalMongoUri && companyName !== "Synergy" && !shouldBypassImoFiltering(companyName)) { const isValid = await isValidImoForCompany(args.imo, companyName, finalDbName, finalMongoUri); if (!isValid) { return { content: [{ type: "text", text: `Access denied: IMO ${args.imo} is not authorized for company ${companyName}`, title: "Access Denied" }] }; } } } // Initialize database connection const qaDbName = process.env.VESSEL_INFO_DB_NAME || ''; const qaMongoUri = process.env.VESSEL_INFO_MONGO_URI || ''; // Use the combined helper function for grouped category mapping const result = await combined_mongotools_with_grouped_category_mapping(args, [ "class_society_compliance", "hazardous_materials_management", "safety_equipment_compliance", "erp_certificate_tracking" ], { "class_society_compliance": [19, 20, 21, 22, 23, 24, 25], "hazardous_materials_management": [87, 88, 89, 90], "safety_equipment_compliance": [106, 107, 108], "erp_certificate_tracking": [115] }, "get_vessel_survey_certificate_information", qaDbName, qaMongoUri); return result; } catch (error) { // logger.error("Error in getVesselSurveyCertificateInformation", { error }); const errorResponse = { type: "text", text: `Error retrieving vessel survey and certificate information: ${error instanceof Error ? error.message : String(error)}` }; return { content: [errorResponse] }; } } export async function universal_certificate_survey_search(arguments_) { const collection = "certificate"; // Extract new schema parameters 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. Only the following properties are allowed: query_by, q, filter_by, sort_by, page, per_page. Please check the tool schema for universal_certificate_survey_search for the allowed properties.`); } const filter_by = args.filter_by || ""; const sort_by = args.sort_by || "relevance"; const page = args.page || 1; const per_page = args.per_page || 50; try { let filterBy = filter_by; // Apply company filtering filterBy = await updateTypesenseFilterWithCompanyImos(filterBy || ""); // Convert date fields from yyyy-mm-dd to Unix timestamps in filter_by if (filterBy) { const dateFields = ["issueDate", "expiryDate", "windowStartDate", "windowEndDate", "extensionDate"]; 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); return `${dateField}${operator}${timestamp}`; } catch (error) { // logger.warn(`Failed to convert date ${dateStr} for field ${dateField}:`, error); return match; // Keep original if conversion fails } }); } } } // Set up all available fields for inclusion (based on schema) const includeFields = "imo,vesselId,vesselName,certificateSurveyEquipmentName,isExtended,issuingAuthority," + "currentStatus,dataSource,type,issueDate,expiryDate,windowStartDate,windowEndDate," + "extensionDate,daysToExpiry,certificateNumber,certificateLink"; const excludeFields = "embedding"; const query = { q: query_text, query_by: query_by, 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; } // logger.debug(`[Typesense Query] ${JSON.stringify(query)}`); let results; try { const typesenseClient = await getTypesenseClient(); results = await typesenseClient.collections(collection).documents().search(query); } catch (searchError) { // Check if this is a filter field error using regex const filterErrorRegex = /Could not find a filter field named `([^`]+)` in the schema/i; const filterMatch = searchError.message?.match(filterErrorRegex); if (filterMatch) { const problematicField = filterMatch[1]; // logger.error(`Filter field '${problematicField}' not found in schema.`); // Return user-friendly error message return { content: [{ type: "text", text: `Error: The field '${problematicField}' is not available for filtering in the certificate/survey schema.\n\nPlease remove '${problematicField}' from your filter_by parameter and try again.`, title: "Invalid Filter Field", format: "text" }] }; } // Check if this is a sorting error using regex const sortErrorRegex = /Could not find a field named `([^`]+)` in the schema for sorting/i; const sortMatch = searchError.message?.match(sortErrorRegex); if (sortMatch && query.sort_by) { const problematicField = sortMatch[1]; // logger.warn(`Sorting field '${problematicField}' not found in schema. Removing from sort_by and retrying...`); // Remove the problematic field from sort_by const sortByFields = query.sort_by.split(',').map((field) => field.trim()); const filteredSortBy = sortByFields.filter((field) => { // Remove both "fieldname:asc" and "fieldname:desc" patterns const fieldName = field.split(':')[0]; return fieldName !== problematicField; }); // If there are remaining fields, use them; otherwise remove sort_by entirely if (filteredSortBy.length > 0) { query.sort_by = filteredSortBy.join(','); // logger.debug(`[Typesense Query Retry] ${JSON.stringify(query)}`); } else { delete query.sort_by; // logger.debug(`[Typesense Query Retry] No valid sort fields remaining, removing sort_by: ${JSON.stringify(query)}`); } // Retry the search try { const typesenseClient = await getTypesenseClient(); results = await typesenseClient.collections(collection).documents().search(query); // logger.info(`Search succeeded after removing problematic sort field '${problematicField}'`); } catch (retryError) { // logger.error('Error performing search even after removing problematic sort field:', retryError); throw retryError; } } else { // Not a sorting or filter error, rethrow throw searchError; } } // Debug: Log the structure of the search results // logger.info(`UniversalCertificateSurveySearchHandler - Results structure: ${JSON.stringify(Object.keys(results))}`); if (results.hits && results.hits.length > 0) { const sampleHit = results.hits[0]; // logger.info(`UniversalCertificateSurveySearchHandler - Sample hit structure: ${JSON.stringify(Object.keys(sampleHit))}`); } if (!results || !results.hits || results.hits.length === 0) { // throw new Error(`No certificate/survey records found`); // return a message that no records were found return { content: [{ type: "text", text: "No certificate/survey records found", title: "No certificate/survey records found", format: "json" }] }; } // Format results using the utility function const title = `Universal Certificate/Survey Search Results for '${query_text}'`; const linkHeader = `Universal certificate/survey search result for query: '${query_text}' in fields: ${query_by}`; return await processTypesenseResults(results, "universal_certificate_survey_search", title, linkHeader); } catch (error) { // logger.error('Error performing universal certificate/survey search:', error); throw new Error(`Error performing search: ${error.message}`); } } // Simple logger const logger = { info: (...args) => console.error(...args), error: (...args) => console.error(...args), warn: (...args) => console.error(...args), debug: (...args) => console.error(...args) };