imo-publications-mcp-server
Version:
MCP server for IMO (International Maritime Organization) publications - Node.js TypeScript version
169 lines (168 loc) • 6.79 kB
JavaScript
import { Client } from 'typesense';
import { logger } from './api.js';
let typesenseClient = null;
let currentConfig = null;
export function getTypesenseClient(config) {
if (!typesenseClient || !currentConfig ||
currentConfig.typesenseHost !== config.typesenseHost ||
currentConfig.typesensePort !== config.typesensePort ||
currentConfig.typesenseProtocol !== config.typesenseProtocol ||
currentConfig.typesenseApiKey !== config.typesenseApiKey) {
logger.log('Creating new Typesense client instance');
const TYPESENSE_CONFIG = {
nodes: [
{
host: config.typesenseHost,
port: parseInt(config.typesensePort),
protocol: config.typesenseProtocol
}
],
apiKey: config.typesenseApiKey,
connectionTimeoutSeconds: 10
};
logger.log(`Typesense configuration: ${JSON.stringify({
...TYPESENSE_CONFIG,
apiKey: '***REDACTED***'
})}`);
typesenseClient = new Client(TYPESENSE_CONFIG);
currentConfig = { ...config };
}
return typesenseClient;
}
async function getSafeQueryFields(config) {
try {
const client = getTypesenseClient(config);
const collection = await client.collections('imo_publication').retrieve();
if (!collection.fields) {
logger.error('No fields found in collection schema');
return ['documentName'];
}
const searchableFields = collection.fields.filter((field) => {
return field.index !== false;
}).map((field) => field.name);
logger.log(`All searchable fields: ${searchableFields.join(', ')}`);
const queryFields = [];
if (searchableFields.includes('documentName')) {
queryFields.push('documentName');
}
const textFields = ['content', 'originalText', 'text', 'summary', 'description'];
for (const field of textFields) {
if (searchableFields.includes(field)) {
queryFields.push(field);
break;
}
}
if (queryFields.length === 0) {
const stringFields = collection.fields.filter((field) => field.type === 'string' && field.index !== false).map((field) => field.name);
if (stringFields.length > 0) {
queryFields.push(stringFields[0]);
logger.log(`Using fallback field: ${stringFields[0]}`);
}
else {
logger.error('No searchable fields found, using documentName as last resort');
queryFields.push('documentName');
}
}
return queryFields;
}
catch (error) {
logger.error('Error getting schema fields, using default:', error);
return ['documentName'];
}
}
export async function searchIMOPublications(searchParams, config) {
try {
const { query = '*', documentName, chapter, section, pageRange, maxResults = 10, searchType = 'semantic', include_fields = [] } = searchParams;
logger.log(`Searching for IMO publications with parameters: ${JSON.stringify(searchParams)}`);
const client = getTypesenseClient(config);
let searchParameters;
if (query === '*') {
searchParameters = {
q: '*',
query_by: 'documentName',
group_by: 'documentName',
per_page: maxResults,
prefix: false,
num_typos: 0,
include_fields: include_fields.join(',')
};
}
else {
const queryFields = await getSafeQueryFields(config);
logger.log(`Using query fields: ${queryFields.join(', ')}`);
searchParameters = {
q: query,
query_by: queryFields.join(','),
per_page: maxResults,
prefix: searchType === 'semantic',
num_typos: searchType === 'semantic' ? 2 : 0,
include_fields: include_fields.join(',')
};
const filterBy = [];
if (documentName) {
filterBy.push(`documentName:=${documentName}`);
}
if (filterBy.length > 0) {
searchParameters.filter_by = filterBy.join(' && ');
}
}
const searchResults = await client.collections('imo_publication').documents().search(searchParameters);
const hits = searchResults.hits || [];
const groupedHits = searchResults.grouped_hits || [];
if (query === '*' && groupedHits.length > 0) {
const results = groupedHits.map(group => group.group_key[0]);
return {
found: results.length,
results,
searchType: 'browse'
};
}
else if (hits.length > 0) {
const results = hits.map(hit => {
const document = { ...hit.document };
delete document.embedding;
return {
...document,
score: hit.text_match || 0
};
});
return {
found: results.length,
results,
searchType: 'search'
};
}
return {
found: 0,
results: [],
searchType: query === '*' ? 'browse' : 'search'
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error(`Error searching IMO publications: ${errorMessage}`, error);
throw new Error(`IMO publications search failed: ${errorMessage}`);
}
}
export async function getIMOPublicationsList(config) {
try {
logger.log('Fetching list of all IMO publications');
const client = getTypesenseClient(config);
const searchParameters = {
q: '*',
query_by: 'documentName',
group_by: 'documentName',
per_page: 50
};
const searchResults = await client.collections('imo_publication').documents().search(searchParameters);
const groupedHits = searchResults.grouped_hits || [];
const documentNames = groupedHits.map(group => group.group_key[0]);
logger.log(`Found ${documentNames.length} unique IMO publications`);
return documentNames;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error(`Error fetching IMO publications list: ${errorMessage}`, error);
throw new Error(`Failed to fetch IMO publications list: ${errorMessage}`);
}
}