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
JavaScript
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