UNPKG

crew-management-mcp-server

Version:

Crew management server handling crew records, certifications, scheduling, payroll, and vessel assignments with ERP access for data extraction

224 lines 9.09 kB
import { logger } from './logger.js'; import { config } from './config.js'; import { isValidImoForCompany, shouldBypassImoFiltering } from './imoUtils.js'; /** * Universal response filter to remove unauthorized IMO data from tool responses * @param response - Tool response to filter * @returns Filtered response with unauthorized data removed */ export async function filterResponseByCompanyImos(response) { const startTime = Date.now(); const companyName = config.companyName || ''; // Skip filtering for admin companies if (shouldBypassImoFiltering(companyName)) { logger.debug(`Bypassing IMO filtering for admin company: ${companyName}`); return response; } const filteredResponse = []; for (const item of response) { if (item.type === 'text') { try { // Try to parse as JSON const parsedContent = JSON.parse(item.text); const result = filterResponseContent(parsedContent, companyName); // Log filtering statistics if items were filtered if (result.stats.itemsFiltered > 0) { logger.warn(`Filtered ${result.stats.itemsFiltered} unauthorized items from response`, { companyName, itemsRemoved: result.stats.itemsFiltered, unauthorizedImos: result.stats.unauthorizedImos, processingTimeMs: result.stats.processingTimeMs }); } filteredResponse.push({ ...item, text: JSON.stringify(result.filtered, null, 2) }); } catch (parseError) { // Non-JSON content - pass through unchanged filteredResponse.push(item); } } else { // Non-text content - pass through unchanged filteredResponse.push(item); } } const totalProcessingTime = Date.now() - startTime; logger.debug(`Response filtering completed in ${totalProcessingTime}ms`); return filteredResponse; } /** * Recursively filter response content to remove unauthorized IMO data */ function filterResponseContent(content, companyName) { const stats = { itemsFiltered: 0, unauthorizedImos: [], processingTimeMs: 0 }; const startTime = Date.now(); const filtered = filterContentRecursively(content, stats, companyName); stats.processingTimeMs = Date.now() - startTime; return { filtered, stats }; } /** * Recursively filter content based on IMO authorization */ function filterContentRecursively(content, stats, companyName) { // Handle arrays by filtering items with unauthorized IMOs if (Array.isArray(content)) { const result = filterArrayByImo(content, stats, companyName); return result; } // Handle objects by recursively filtering properties if (typeof content === 'object' && content !== null) { // Check if this object contains unauthorized IMO const unauthorizedResult = checkForUnauthorizedImo(content); if (unauthorizedResult.found) { // This entire object should be filtered out stats.itemsFiltered++; stats.unauthorizedImos.push(unauthorizedResult.location); return null; // Mark for removal } // Object is clean, recursively filter its properties const filtered = {}; for (const key in content) { if (content.hasOwnProperty(key)) { const filteredValue = filterContentRecursively(content[key], stats, companyName); // Only include non-null values (null means filtered out) if (filteredValue !== null) { filtered[key] = filteredValue; } } } return filtered; } // Primitive values - pass through unchanged return content; } /** * Filter array items to remove those with unauthorized IMO numbers */ function filterArrayByImo(array, stats, companyName) { const filteredArray = []; for (const item of array) { const filteredItem = filterContentRecursively(item, stats, companyName); // Only include items that weren't filtered out if (filteredItem !== null) { filteredArray.push(filteredItem); } } // Handle special case: if this was a hits array and all items were filtered, // provide informative message if (array.length > 0 && filteredArray.length === 0 && stats.itemsFiltered > 0) { const parentObject = findParentWithHitsArray(array); if (parentObject && parentObject.found !== undefined) { // This looks like a search results object, provide informative error const unauthorizedImos = [...new Set(stats.unauthorizedImos.map(imo => imo.split(': ')[1]))]; const errorMessage = `The queried vessel${unauthorizedImos.length > 1 ? 's are' : ' is'} not under ${companyName}. ` + `IMO number${unauthorizedImos.length > 1 ? 's' : ''} ${unauthorizedImos.join(', ')} ${unauthorizedImos.length > 1 ? 'are' : 'is'} not associated with this company.`; // Return an informative error instead of empty array throw new Error(errorMessage); } } return filteredArray; } /** * Check if an object contains unauthorized IMO numbers */ function checkForUnauthorizedImo(obj) { if (!obj || typeof obj !== 'object') { return { found: false, location: '' }; } // Find IMO field in this object const imoField = findImoField(obj); if (imoField && !isValidImoForCompany(imoField.value)) { return { found: true, location: `${imoField.fieldName}: ${imoField.value}` }; } // Recursively check nested objects and arrays for (const key in obj) { if (obj.hasOwnProperty(key)) { const value = obj[key]; if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { const result = checkForUnauthorizedImo(value[i]); if (result.found) { return { found: true, location: `${key}[${i}].${result.location}` }; } } } else if (typeof value === 'object' && value !== null) { const result = checkForUnauthorizedImo(value); if (result.found) { return { found: true, location: `${key}.${result.location}` }; } } } } return { found: false, location: '' }; } /** * Find IMO field in an object */ function findImoField(obj) { const IMO_FIELD_NAMES = [ 'imo', 'vesselImo', 'imoNumber', 'IMO', 'vessel_imo', 'imo_number', 'vesselIMO', 'IMO_NUMBER', 'vessel_imo_number' ]; for (const fieldName of IMO_FIELD_NAMES) { if (obj.hasOwnProperty(fieldName) && obj[fieldName] != null) { return { fieldName, value: obj[fieldName] }; } } return null; } /** * Helper function to find parent object containing hits array (for error messages) */ function findParentWithHitsArray(array) { // This is a simple heuristic - in practice, this would be called from // the context where we know the parent structure return null; } /** * Specialized filter for crew-related responses to handle ONBOARD_SAILING_STATUS logic * @param response - Response containing crew data * @param companyName - Company name for filtering * @returns Filtered response */ export function filterCrewResponseByCompanyImos(response, companyName) { if (shouldBypassImoFiltering(companyName)) { return response; } const filteredResponse = []; for (const item of response) { // Check if this is crew data with onboard status and IMO if (item && typeof item === 'object') { const onboardStatus = item.ONBOARD_SAILING_STATUS; const imoNumber = item.IMO_NUMBER || item.imo || item.vesselImo; // Apply filtering logic for onboard crew if (onboardStatus === 'Onboard') { // If crew is onboard but IMO is null, reject if (!imoNumber) { logger.debug(`Filtering out crew record with null IMO: ${item.CREW_CODE || 'unknown'}`); continue; } // If crew is onboard but vessel is not in company list, reject if (!isValidImoForCompany(imoNumber)) { logger.debug(`Filtering out onboard crew on unauthorized vessel: IMO ${imoNumber}`); continue; } } // Crew record is authorized, include it filteredResponse.push(item); } else { // Non-object data, pass through filteredResponse.push(item); } } return filteredResponse; } //# sourceMappingURL=responseFilter.js.map