imo-publications-mcp-server
Version:
MCP server for IMO (International Maritime Organization) publications - Node.js TypeScript version
204 lines (203 loc) • 8.33 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 getCollectionSchema(config) {
try {
const client = getTypesenseClient(config);
const collection = await client.collections('imo_publication').retrieve();
logger.log(`Collection schema retrieved: ${collection.fields?.length || 0} fields found`);
return collection;
}
catch (error) {
logger.error('Error getting collection schema:', error);
throw error;
}
}
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 === '*') {
logger.log('Browse mode: Getting all documents grouped by documentName');
searchParameters = {
q: '*',
query_by: 'documentName',
group_by: 'documentName',
per_page: maxResults,
prefix: false,
num_typos: 0,
include_fields: include_fields.join(',')
};
}
else if (searchType === 'semantic') {
logger.log('Semantic search mode: Using embedding field for vector similarity');
searchParameters = {
q: query,
query_by: 'embedding,embText',
per_page: maxResults,
prefix: false,
exclude_fields: 'embedding',
include_fields: include_fields.length > 0 ? include_fields.join(',') : undefined
};
logger.log('Semantic search: query_by=embedding (vector similarity)');
}
else if (searchType === 'exact') {
logger.log('Exact text search mode');
logger.log('Using query field: embText');
searchParameters = {
q: query,
query_by: 'embText,documentName',
per_page: maxResults,
prefix: false,
num_typos: 0,
include_fields: include_fields.join(',')
};
logger.log('Exact search: prefix=false, typos=0');
}
else if (searchType === 'name') {
logger.log('Document name search mode');
logger.log('Using query field: documentName only');
searchParameters = {
q: query,
query_by: 'documentName',
group_by: 'documentName',
per_page: maxResults,
prefix: true,
num_typos: 2,
include_fields: 'documentName,documentLink'
};
logger.log('Name search: query_by=documentName, group_by=documentName, prefix=true, typos=2');
}
else {
logger.log(`Unknown search type "${searchType}", falling back to semantic search`);
searchParameters = {
q: query,
query_by: 'embText,documentName',
per_page: maxResults,
prefix: true,
num_typos: 2,
include_fields: include_fields.join(',')
};
}
const filterBy = [];
if (documentName) {
filterBy.push(`documentName:${documentName}`);
logger.log(`Added filter: documentName contains ${documentName}`);
}
if (chapter) {
filterBy.push(`chapter:${chapter}`);
logger.log(`Added filter: chapter contains ${chapter}`);
}
if (section) {
filterBy.push(`section:${section}`);
logger.log(`Added filter: section contains ${section}`);
}
if (pageRange && pageRange.length === 2) {
filterBy.push(`pageNumber:>=${pageRange[0]} && pageNumber:<=${pageRange[1]}`);
logger.log(`Added filter: pageNumber between ${pageRange[0]} and ${pageRange[1]}`);
}
if (filterBy.length > 0) {
searchParameters.filter_by = filterBy.join(' && ');
logger.log(`Applied filters: ${searchParameters.filter_by}`);
}
const searchResults = await client.collections('imo_publication').documents().search(searchParameters);
const hits = searchResults.hits || [];
const groupedHits = searchResults.grouped_hits || [];
if (groupedHits.length > 0) {
logger.log(`Processing ${groupedHits.length} grouped results`);
const results = groupedHits.map(group => {
const firstHit = group.hits[0];
const document = { ...firstHit.document };
delete document.embedding;
return {
...document,
score: firstHit.text_match || 0,
group_size: group.hits.length
};
});
return {
found: results.length,
results,
searchType: query === '*' ? 'browse' : 'grouped_search'
};
}
else if (hits.length > 0) {
logger.log(`Processing ${hits.length} individual results`);
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}`);
}
}