UNPKG

imo-publications-mcp-server

Version:

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

204 lines (203 loc) 8.33 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 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}`); } }