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
309 lines • 13.2 kB
JavaScript
import { isValidImoForCompany, shouldBypassImoFiltering, getCompanyImoNumbers } from './imoUtils.js';
import { config } from './config.js';
import { logger } from './logger.js';
/**
* Find IMO field in an object with various possible field names
* @param obj - Object to search for IMO field
* @returns Object with field name and value, or null if not found
*/
function findImoField(obj) {
const imoFieldNames = ['imo', 'vesselImo', 'imoNumber', 'vessel_imo', 'IMO'];
for (const fieldName of imoFieldNames) {
if (obj && typeof obj === 'object' && obj.hasOwnProperty(fieldName)) {
const value = obj[fieldName];
if (value !== null && value !== undefined && value !== '') {
return { fieldName, value };
}
}
}
return null;
}
/**
* Recursively check for unauthorized IMO numbers in nested objects and arrays
* @param obj - Object to check
* @param path - Current path for debugging (optional)
* @returns Object indicating if unauthorized IMO was found and its location
*/
function checkForUnauthorizedImo(obj, path = '') {
if (!obj || typeof obj !== 'object') {
return { found: false, location: '' };
}
// Check direct IMO fields in current object
const imoField = findImoField(obj);
if (imoField && !isValidImoForCompany(imoField.value)) {
const location = path ? `${path}.${imoField.fieldName}` : imoField.fieldName;
return { found: true, location: `${location}: ${imoField.value}` };
}
// Recursively check nested objects and arrays
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
const currentPath = path ? `${path}.${key}` : key;
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
const result = checkForUnauthorizedImo(value[i], `${currentPath}[${i}]`);
if (result.found) {
return result;
}
}
}
else if (typeof value === 'object' && value !== null) {
const result = checkForUnauthorizedImo(value, currentPath);
if (result.found) {
return result;
}
}
}
}
return { found: false, location: '' };
}
/**
* Filter array items based on IMO authorization
* @param array - Array to filter
* @returns Object with filtered array and statistics
*/
function filterArrayByImo(array) {
const stats = {
itemsFiltered: 0,
unauthorizedImos: [],
processingTimeMs: 0
};
const startTime = Date.now();
const filtered = array.filter(item => {
const unauthorizedCheck = checkForUnauthorizedImo(item);
if (unauthorizedCheck.found) {
stats.itemsFiltered++;
stats.unauthorizedImos.push(unauthorizedCheck.location);
logger.debug(`Filtered item with unauthorized IMO: ${unauthorizedCheck.location}`);
return false;
}
return true;
});
stats.processingTimeMs = Date.now() - startTime;
return { filtered, stats };
}
/**
* Recursively filter response content for unauthorized IMO numbers
* @param content - Content to filter
* @returns Object with filtered content and statistics
*/
function filterResponseContent(content) {
const stats = {
itemsFiltered: 0,
unauthorizedImos: [],
processingTimeMs: 0
};
const startTime = Date.now();
try {
// Handle arrays by filtering items with unauthorized IMOs
if (Array.isArray(content)) {
const result = filterArrayByImo(content);
stats.itemsFiltered = result.stats.itemsFiltered;
stats.unauthorizedImos = result.stats.unauthorizedImos;
stats.processingTimeMs = Date.now() - startTime;
return { filtered: result.filtered, stats };
}
// Handle objects by recursively filtering properties
if (typeof content === 'object' && content !== null) {
const filtered = { ...content };
for (const key in content) {
if (content.hasOwnProperty(key)) {
const value = content[key];
if (Array.isArray(value)) {
const result = filterArrayByImo(value);
filtered[key] = result.filtered;
stats.itemsFiltered += result.stats.itemsFiltered;
stats.unauthorizedImos.push(...result.stats.unauthorizedImos);
}
else if (typeof value === 'object' && value !== null) {
const result = filterResponseContent(value);
filtered[key] = result.filtered;
stats.itemsFiltered += result.stats.itemsFiltered;
stats.unauthorizedImos.push(...result.stats.unauthorizedImos);
}
else {
filtered[key] = value;
}
}
}
stats.processingTimeMs = Date.now() - startTime;
return { filtered, stats };
}
// Return primitive values unchanged
stats.processingTimeMs = Date.now() - startTime;
return { filtered: content, stats };
}
catch (error) {
logger.error(`Error during response filtering: ${error.message}`);
stats.processingTimeMs = Date.now() - startTime;
return { filtered: content, stats };
}
}
/**
* Main function to filter tool responses by company IMO authorization
* @param response - Tool response to filter
* @returns Filtered tool response
*/
export async function filterResponseByCompanyImos(response) {
const { companyName } = config;
const startTime = Date.now();
// Skip filtering for admin companies
if (!companyName || shouldBypassImoFiltering(companyName)) {
logger.debug('Skipping response filtering for admin company or missing company name');
return response;
}
// Skip filtering if no company IMOs are loaded
const companyImos = getCompanyImoNumbers();
if (companyImos.length === 0) {
logger.debug('Skipping response filtering - no company IMOs loaded');
return response;
}
const filteredResponse = [];
let totalItemsFiltered = 0;
let totalUnauthorizedImos = [];
try {
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);
totalItemsFiltered += result.stats.itemsFiltered;
totalUnauthorizedImos.push(...result.stats.unauthorizedImos);
// Check if all hits were filtered out and provide informative message
if (parsedContent.hits && Array.isArray(parsedContent.hits)) {
const originalHitsCount = parsedContent.hits.length;
const filteredHitsCount = result.filtered.hits ? result.filtered.hits.length : 0;
if (originalHitsCount > 0 && filteredHitsCount === 0 && result.stats.itemsFiltered > 0) {
// All hits were filtered out - return informative message
const unauthorizedImos = [...new Set(result.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.`;
filteredResponse.push({
...item,
text: errorMessage
});
continue;
}
}
// Update response with filtered content
filteredResponse.push({
...item,
text: JSON.stringify(result.filtered, null, 2)
});
}
catch (parseError) {
// Not JSON or parsing failed - pass through unchanged
filteredResponse.push(item);
}
}
else {
// Non-text items pass through unchanged
filteredResponse.push(item);
}
}
const processingTime = Date.now() - startTime;
// Log filtering results
if (totalItemsFiltered > 0) {
logger.warn(`Filtered ${totalItemsFiltered} unauthorized items from response`, {
companyName,
itemsRemoved: totalItemsFiltered,
unauthorizedImos: totalUnauthorizedImos.slice(0, 10), // Limit log size
processingTimeMs: processingTime
});
}
else {
logger.debug('No items filtered from response', {
companyName,
processingTimeMs: processingTime
});
}
return filteredResponse;
}
catch (error) {
logger.error(`Error during response filtering: ${error.message}`);
// Return original response on error to avoid breaking functionality
return response;
}
}
/**
* Filter search results before artifact creation to prevent unauthorized data leakage
* This is critical for tools like universal_voyage_search that create persistent snapshots
* @param searchResults - Array of search result objects
* @returns Object with filtered results and statistics
*/
export function filterSearchResultsByCompanyImo(searchResults) {
const { companyName } = config;
const startTime = Date.now();
const stats = {
itemsFiltered: 0,
unauthorizedImos: [],
processingTimeMs: 0
};
// Skip filtering for admin companies
if (!companyName || shouldBypassImoFiltering(companyName)) {
logger.debug('Skipping search result filtering for admin company or missing company name');
stats.processingTimeMs = Date.now() - startTime;
return { filtered: searchResults, stats };
}
// Skip filtering if no company IMOs are loaded
const companyImos = getCompanyImoNumbers();
if (companyImos.length === 0) {
logger.debug('Skipping search result filtering - no company IMOs loaded');
stats.processingTimeMs = Date.now() - startTime;
return { filtered: searchResults, stats };
}
try {
const filtered = searchResults.filter(result => {
// Check for unauthorized IMO in the result document
const unauthorizedCheck = checkForUnauthorizedImo(result);
if (unauthorizedCheck.found) {
stats.itemsFiltered++;
stats.unauthorizedImos.push(unauthorizedCheck.location);
logger.debug(`Filtered search result with unauthorized IMO: ${unauthorizedCheck.location}`);
return false;
}
return true;
});
stats.processingTimeMs = Date.now() - startTime;
// Log filtering results for security monitoring
if (stats.itemsFiltered > 0) {
logger.warn(`Filtered ${stats.itemsFiltered} unauthorized search results before artifact creation`, {
companyName,
itemsRemoved: stats.itemsFiltered,
unauthorizedImos: stats.unauthorizedImos.slice(0, 10), // Limit log size
processingTimeMs: stats.processingTimeMs,
securityEvent: 'search_result_filtering'
});
}
else {
logger.debug('No unauthorized search results filtered', {
companyName,
totalResults: searchResults.length,
processingTimeMs: stats.processingTimeMs
});
}
return { filtered, stats };
}
catch (error) {
logger.error(`Error during search result filtering: ${error.message}`);
stats.processingTimeMs = Date.now() - startTime;
// Return original results on error to avoid breaking functionality
return { filtered: searchResults, stats };
}
}
/**
* Get filtering statistics for monitoring
* @returns Object with current filtering configuration
*/
export function getFilteringStats() {
const { companyName } = config;
const companyImos = getCompanyImoNumbers();
return {
companyName: companyName || 'Not set',
isFilteringEnabled: !!(companyName && !shouldBypassImoFiltering(companyName) && companyImos.length > 0),
companyImoCount: companyImos.length,
isAdminCompany: companyName ? shouldBypassImoFiltering(companyName) : false
};
}
//# sourceMappingURL=responseFilter.js.map