navigation-equipment-manuals-mcp-server
Version:
Navigation Equipment Manuals MCP Server for create, update, delete, and search navigation equipment manuals
235 lines • 8.57 kB
JavaScript
import { Client } from 'typesense';
import { logger } from './api.js';
let typesenseClient;
/**
* Initialize Typesense client
*/
export function initializeTypesense(config) {
try {
typesenseClient = new Client({
nodes: [
{
host: config.host,
port: parseInt(config.port),
protocol: config.protocol,
},
],
apiKey: config.apiKey,
connectionTimeoutSeconds: 10,
});
logger.log('Typesense client initialized successfully');
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown Typesense initialization error';
logger.error('Failed to initialize Typesense client', error);
throw new Error(`Typesense initialization failed: ${errorMessage}`);
}
}
/**
* Get the Typesense client instance
*/
export function getTypesenseClient() {
if (!typesenseClient) {
throw new Error('Typesense client not initialized. Call initializeTypesense first.');
}
return typesenseClient;
}
/**
* Search for navigation equipment manuals using Typesense
*/
export async function searchByManuals(query, filters, limit = 10) {
try {
if (!typesenseClient) {
throw new Error('Typesense client not initialized');
}
const searchParameters = {
q: query,
query_by: 'equipmentName,content,manufacturer,model,category,manualType',
limit: Math.min(limit, 50),
highlight_full_fields: 'content',
snippet_threshold: 30,
num_typos: 2,
typo_tokens_threshold: 1,
drop_tokens_threshold: 1,
highlight_affix_num_tokens: 4,
};
// Add filters if provided
if (filters) {
const filterBy = [];
if (filters.category) {
filterBy.push(`category:=${filters.category}`);
}
if (filters.manufacturer) {
filterBy.push(`manufacturer:=${filters.manufacturer}`);
}
if (filters.manualType) {
filterBy.push(`manualType:=${filters.manualType}`);
}
if (filterBy.length > 0) {
searchParameters.filter_by = filterBy.join(' && ');
}
}
logger.debug(`Searching Typesense with parameters: ${JSON.stringify(searchParameters)}`);
const searchResults = await typesenseClient
.collections('navigation_manuals')
.documents()
.search(searchParameters);
logger.debug(`Typesense search returned ${searchResults.hits?.length || 0} results`);
return searchResults.hits || [];
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown search error';
logger.error('Typesense search failed', error);
throw new Error(`Search failed: ${errorMessage}`);
}
}
/**
* Index a manual document in Typesense
*/
export async function indexManual(manual) {
try {
if (!typesenseClient) {
throw new Error('Typesense client not initialized');
}
// Prepare document for indexing
const document = {
id: manual._id?.toString() || manual.id,
equipmentName: manual.equipmentName || '',
equipmentId: manual.equipmentId?.toString() || '',
manufacturer: manual.manufacturer || '',
model: manual.model || '',
category: manual.category || 'general',
manualType: manual.manualType || 'general',
version: manual.version || '',
status: manual.status || 'active',
content: extractContentFromPages(manual.pages || []),
pageCount: manual.pages?.length || 0,
indexTerms: extractIndexTerms(manual.index || []),
createdAt: manual.createdAt || new Date().toISOString(),
updatedAt: manual.updatedAt || new Date().toISOString(),
};
await typesenseClient
.collections('navigation_manuals')
.documents()
.upsert(document);
logger.debug(`Indexed manual: ${manual.equipmentName} (${manual.equipmentId})`);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown indexing error';
logger.error('Failed to index manual in Typesense', error);
throw new Error(`Indexing failed: ${errorMessage}`);
}
}
/**
* Delete a manual from Typesense index
*/
export async function deleteManualFromIndex(manualId) {
try {
if (!typesenseClient) {
throw new Error('Typesense client not initialized');
}
await typesenseClient
.collections('navigation_manuals')
.documents(manualId)
.delete();
logger.debug(`Deleted manual from index: ${manualId}`);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown deletion error';
logger.error('Failed to delete manual from Typesense', error);
// Don't throw error for deletion failures as they might not exist
logger.warn(`Could not delete manual from index: ${errorMessage}`);
}
}
/**
* Create or update the Typesense collection schema
*/
export async function createOrUpdateSchema() {
try {
if (!typesenseClient) {
throw new Error('Typesense client not initialized');
}
const schema = {
name: 'navigation_manuals',
fields: [
{ name: 'equipmentName', type: 'string' },
{ name: 'equipmentId', type: 'string' },
{ name: 'manufacturer', type: 'string', optional: true },
{ name: 'model', type: 'string', optional: true },
{ name: 'category', type: 'string', facet: true },
{ name: 'manualType', type: 'string', facet: true },
{ name: 'version', type: 'string', optional: true },
{ name: 'status', type: 'string', facet: true },
{ name: 'content', type: 'string' },
{ name: 'pageCount', type: 'int32' },
{ name: 'indexTerms', type: 'string[]', optional: true },
{ name: 'createdAt', type: 'string' },
{ name: 'updatedAt', type: 'string' },
],
default_sorting_field: 'updatedAt',
};
try {
// Try to get existing collection
await typesenseClient.collections('navigation_manuals').retrieve();
logger.info('Typesense collection already exists');
}
catch (error) {
// Collection doesn't exist, create it
await typesenseClient.collections().create(schema);
logger.info('Created Typesense collection: navigation_manuals');
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown schema error';
logger.error('Failed to create/update Typesense schema', error);
throw new Error(`Schema operation failed: ${errorMessage}`);
}
}
/**
* Extract searchable content from manual pages
*/
function extractContentFromPages(pages) {
if (!Array.isArray(pages)) {
return '';
}
return pages
.map(page => {
const content = page.content || '';
const title = page.title || '';
const section = page.section || '';
const subsection = page.subsection || '';
return [title, section, subsection, content].filter(Boolean).join(' ');
})
.join(' ')
.substring(0, 50000); // Limit content length for indexing
}
/**
* Extract index terms for searchability
*/
function extractIndexTerms(indexEntries) {
if (!Array.isArray(indexEntries)) {
return [];
}
return indexEntries
.map(entry => entry.term)
.filter(Boolean)
.filter((term, index, array) => array.indexOf(term) === index) // Remove duplicates
.slice(0, 100); // Limit number of terms
}
/**
* Check Typesense connection health
*/
export async function checkTypesenseHealth() {
try {
if (!typesenseClient) {
return false;
}
await typesenseClient.health.retrieve();
return true;
}
catch (error) {
logger.error('Typesense health check failed', error);
return false;
}
}
//# sourceMappingURL=typesense.js.map