UNPKG

imo-publications-mcp-server

Version:

MCP server for IMO (International Maritime Organization) publications - Node.js TypeScript version

169 lines (168 loc) 6.79 kB
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}`); } }