UNPKG

navigation-equipment-manuals-mcp-server

Version:

Navigation Equipment Manuals MCP Server for create, update, delete, and search navigation equipment manuals

1,164 lines 51.9 kB
/**
 * Navigation Equipment Manuals MCP Server
 * A low-level server implementation using Model Context Protocol for Navigation Equipment Manuals API
 */
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { connectToMongoDB, getDb } from "./utils/mongodb.js";
import { getTypesenseClient, initializeTypesense } from "./utils/typesense.js";
import { logger } from './utils/api.js';
import { NAVIGATION_MANUALS_COLLECTION, COHERE_RERANK_LIMIT, COHERE_MODEL, DEFAULT_SEARCH_LIMIT, COHERE_MAX_DOCUMENTS, setRuntimeConfig, getCohereApiKey } from './utils/constants.js';
import { getListOfArtifacts, formatHit, sanitizeFilterValue, createTextContent } from './utils/helpers.js';
import { CohereClientV2 } from 'cohere-ai';
let manualConfig;
/**
 * Helper function to safely extract parameters from request arguments
 */
function safeGetArgs(args, defaultValues) {
    if (!args || typeof args !== 'object') {
        return defaultValues;
    }
    const result = { ...defaultValues };
    for (const key in defaultValues) {
        if (args[key] !== undefined) {
            result[key] = args[key];
        }
    }
    return result;
}
function parseArgs() {
    // First check for environment variables (used when running in npx mode)
    if (process.env.MONGO_URI || process.env.TYPESENSE_HOST) {
        logger.log('Using configuration from environment variables');
        return {
            mongoUri: process.env.MONGO_URI || '',
            dbName: process.env.DB_NAME || '',
            typesenseHost: process.env.TYPESENSE_HOST || '',
            typesensePort: process.env.TYPESENSE_PORT || '',
            typesenseProtocol: process.env.TYPESENSE_PROTOCOL || '',
            typesenseApiKey: process.env.TYPESENSE_API_KEY || '',
            openaiApiKey: process.env.OPENAI_API_KEY || '',
            cohereApiKey: process.env.COHERE_API_KEY || '',
            perplexityApiKey: process.env.PERPLEXITY_API_KEY || ''
        };
    }
    // Otherwise parse from command line
    const args = process.argv.slice(2);
    const config = {
        mongoUri: ''
    };
    for (let i = 0; i < args.length; i++) {
        const arg = args[i];
        if (arg === '--mongo-uri' && i + 1 < args.length) {
            config.mongoUri = args[++i];
        }
        else if (arg === '--typesense-host' && i + 1 < args.length) {
            config.typesenseHost = args[++i];
        }
        else if (arg === '--typesense-port' && i + 1 < args.length) {
            config.typesensePort = args[++i];
        }
        else if (arg === '--typesense-protocol' && i + 1 < args.length) {
            config.typesenseProtocol = args[++i];
        }
        else if (arg === '--typesense-api-key' && i + 1 < args.length) {
            config.typesenseApiKey = args[++i];
        }
        else if (arg === '--db-name' && i + 1 < args.length) {
            config.dbName = args[++i];
        }
        else if (arg === '--openai-api-key' && i + 1 < args.length) {
            config.openaiApiKey = args[++i];
        }
        else if (arg === '--cohere-api-key' && i + 1 < args.length) {
            config.cohereApiKey = args[++i];
        }
        else if (arg === '--perplexity-api-key' && i + 1 < args.length) {
            config.perplexityApiKey = args[++i];
        }
        else if (arg === '--help' || arg === '-h') {
            console.log(`
Navigation Equipment Manuals MCP Server

USAGE:
  node dist/index.js [OPTIONS]

REQUIRED OPTIONS:
  --mongo-uri <uri>              MongoDB connection URI
  --typesense-host <host>        Typesense server host
  --typesense-port <port>        Typesense server port
  --typesense-protocol <proto>   Typesense protocol (http/https)
  --typesense-api-key <key>      Typesense API key

OPTIONAL OPTIONS:
  --db-name <name>               MongoDB database name
  --openai-api-key <key>         OpenAI API key for LLM features
  --cohere-api-key <key>         Cohere API key for reranking
  --perplexity-api-key <key>     Perplexity API key for web search

ENVIRONMENT VARIABLES:
  MONGO_URI                      MongoDB connection URI
  DB_NAME                        MongoDB database name
  TYPESENSE_HOST                 Typesense server host
  TYPESENSE_PORT                 Typesense server port
  TYPESENSE_PROTOCOL             Typesense protocol (http/https)
  TYPESENSE_API_KEY              Typesense API key
  OPENAI_API_KEY                 OpenAI API key for LLM features
  COHERE_API_KEY                 Cohere API key for reranking
  PERPLEXITY_API_KEY             Perplexity API key for web search

EXAMPLES:
  # Using command line arguments
  node dist/index.js --mongo-uri "mongodb://localhost:27017" \\
    --typesense-host "localhost" --typesense-port "8108" \\
    --typesense-protocol "http" --typesense-api-key "xyz123"

  # Using environment variables
  export MONGO_URI="mongodb://localhost:27017"
  export TYPESENSE_HOST="localhost"
  export TYPESENSE_PORT="8108"
  export TYPESENSE_PROTOCOL="http"
  export TYPESENSE_API_KEY="xyz123"
  node dist/index.js
      `);
            process.exit(0);
        }
    }
    if (!config.mongoUri) {
        throw new Error('Mongo URI is required. Use --mongo-uri argument or set MONGO_URI environment variable.');
    }
    if (!config.typesenseHost) {
        throw new Error('Typesense host is required. Use --typesense-host argument or set TYPESENSE_HOST environment variable.');
    }
    if (!config.typesensePort) {
        throw new Error('Typesense port is required. Use --typesense-port argument or set TYPESENSE_PORT environment variable.');
    }
    if (!config.typesenseProtocol) {
        throw new Error('Typesense protocol is required. Use --typesense-protocol argument or set TYPESENSE_PROTOCOL environment variable.');
    }
    if (!config.typesenseApiKey) {
        throw new Error('Typesense API key is required. Use --typesense-api-key argument or set TYPESENSE_API_KEY environment variable.');
    }
    return config;
}
const server = new Server({
    name: "navigation-equipment-manuals-mcp-server",
    version: "1.0.3"
}, {
    capabilities: {
        resources: {
            read: true,
            list: true,
            templates: true
        },
        tools: {
            list: true,
            call: true
        },
        prompts: {
            list: true,
            get: true
        }
    }
});
// Set up the resource listing request handler
server.setRequestHandler(ListResourcesRequestSchema, async () => {
    logger.log('Received list resources request');
    return { resources: [] };
});
/**
 * Handler for reading manual information.
 */
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    logger.log('Received read resource request: ' + JSON.stringify(request));
    throw new Error("Resource reading not implemented");
});
/**
 * List available tools for interacting with navigation equipment manuals data.
 */
server.setRequestHandler(ListToolsRequestSchema, async () => {
    return { tools: [
            {
                name: "smart_navigation_manual_search",
                description: ("Universal search tool for navigation equipment manuals. " +
                    "This is the primary tool for finding any information in the manual database. " +
                    "It intelligently adapts search strategy based on query intent and can handle " +
                    "everything from specific lookups to general browsing."),
                inputSchema: {
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": ("Natural language search query. Leave empty for browsing mode. " +
                                "Examples: 'radar calibration procedure', 'AIS not showing targets', " +
                                "'power consumption specifications'"),
                        },
                        "search_type": {
                            "type": "string",
                            "description": "Search strategy. Fixed to 'semantic' for conceptual queries.",
                            "enum": ["semantic"],
                            "default": "semantic"
                        },
                        "filters": {
                            "type": "object",
                            "description": "Filters to narrow search results. All filters are optional and use exact matching",
                            "properties": {
                                "maker": {
                                    "type": "string",
                                    "description": "Equipment manufacturer name",
                                    "enum": ["FURUNO", "JRC", "SIMRAD", "KODEN", "SPERRY MARINE", "SAM ELECTRONICS", "SAILOR",
                                        "SAAB", "RAYTHEON", "RAYTHEON ANSCHUTZ", "ANSCHUTZ", "SKIPPER", "NINGLU", "INMARSAT",
                                        "DANELEC MARINE", "SPEERY", "JMC", "TOKIMEC", "AMI", "SAMYUNG NAVTEX", "TOKYO KEIKI",
                                        "HYUNDAI", "STRATUM FIVE SSAS", "SM ELECTRICS", "MARTEK", "JOTRON", "INTELLIAN",
                                        "FBB LAUNCH PAD", "DANLEC", "STX", "RM YOUNG"]
                                },
                                "model": {
                                    "type": "string",
                                    "description": "Specific equipment model number"
                                },
                                "equipment": {
                                    "type": "string",
                                    "description": "Type of navigation equipment",
                                    "enum": ["AIS", "RADAR", "MF-HF", "ECDIS", "SPEED LOG", "BNWAS", "GPS", "GYRO", "AUTO PILOT",
                                        "NAVTEX", "ECHO-SOUNDER", "VDR", "VHF", "SATC", "INMARSAT-C", "FBB", "SSAS", "ANEMOMETER",
                                        "SATELLITE LOG", "WEATHER FAX", "PUBLIC ALARM AND TALK BACK SYSTEM"]
                                },
                                "documentName": {
                                    "type": "string",
                                    "description": "Exact name of the manual document",
                                },
                                "documentType": {
                                    "type": "string",
                                    "description": "Category of manual",
                                    "enum": ["Operation Manual", "Installation Manual", "Instruction Manual", "Service Manual", "Troubleshooting",
                                        "MMPL Operation Manual", "MMPL Installation Manual", "MMPL Instruction Manual", "MMPL Service Manual",
                                        "MMPL Troubleshooting"]
                                },
                                "chapter": {
                                    "type": "string",
                                    "description": "Chapter name or number to search within",
                                },
                                "section": {
                                    "type": "string",
                                    "description": "Section name to search within",
                                },
                                "page_range": {
                                    "type": "array",
                                    "items": {
                                        "type": "number"
                                    },
                                    "minItems": 2,
                                    "maxItems": 2,
                                    "description": "Page range to search within [start_page, end_page]"
                                }
                            }
                        },
                        "max_results": {
                            "type": "number",
                            "description": "Maximum number of results to return",
                            "default": 7,
                            "minimum": 1,
                            "maximum": 10
                        }
                    },
                    "required": [],
                    "additionalProperties": false
                }
            },
            {
                name: "list_equipment_manufacturers",
                description: "Retrieves a list of all manufacturers for navigation equipment in the database, can be used to check if a specific manufacturer is available and by what name it is stored in the database.",
                inputSchema: {
                    "type": "object",
                    "properties": {},
                    "additionalProperties": false
                }
            },
            {
                name: "list_navigation_equipment_types",
                description: "Retrieves a list of navigation equipment, Returns the equipment categories like rdar ECDIS, GPS autopilot etc.Use this tool to check if a specific equipment type is available and verify the exact naming convention used in the database for queries.",
                inputSchema: {
                    "type": "object",
                    "properties": {
                        "maker": {
                            "type": "string",
                            "description": "Optional. The specific manufacturer of the navigation equipment to filter by.",
                            "enum": ["FURUNO", "JRC", "SIMRAD", "KODEN", "SPERRY MARINE", "SAM ELECTRONICS", "SAILOR",
                                "SAAB", "RAYTHEON", "RAYTHEON ANSCHUTZ", "ANSCHUTZ", "SKIPPER", "NINGLU", "INMARSAT",
                                "DANELEC MARINE", "SPEERY", "JMC", "TOKIMEC", "AMI", "SAMYUNG NAVTEX", "TOKYO KEIKI",
                                "HYUNDAI", "STRATUM FIVE SSAS", "SM ELECTRICS", "MARTEK", "JOTRON", "INTELLIAN",
                                "FBB LAUNCH PAD", "DANLEC", "STX", "RM YOUNG"]
                        }
                    },
                    "required": []
                }
            },
            {
                name: "list_navigation_equipment_models",
                description: "Retrieves a list of navigation equipment models by maker and/or equipment type. This requires exact names for both maker and/or equipment filters. Returns model names along with associated manufacturer and equipment type information.",
                inputSchema: {
                    "type": "object",
                    "properties": {
                        "maker": {
                            "type": "string",
                            "description": "Optional. The specific manufacturer of the navigation equipment to filter by."
                        },
                        "equipment": {
                            "type": "string",
                            "description": "Optional. The specific equipment type to filter by."
                        }
                    },
                    "required": []
                }
            },
            {
                name: "find_equipment_manuals",
                description: "Retrieves a list of all manuals available in the database for a specific maker, model, and equipment type. Must provide at least one of the parameters maker, model, or equipment. Use other tools to get the maker, model, or equipment type before using this tool.",
                inputSchema: {
                    "type": "object",
                    "properties": {
                        "maker": {
                            "type": "string",
                            "description": "The specific manufacturer of the navigation equipment to filter by.",
                            "enum": ["FURUNO", "JRC", "SIMRAD", "KODEN", "SPERRY MARINE", "SAM ELECTRONICS", "SAILOR",
                                "SAAB", "RAYTHEON", "RAYTHEON ANSCHUTZ", "ANSCHUTZ", "SKIPPER", "NINGLU", "INMARSAT",
                                "DANELEC MARINE", "SPEERY", "JMC", "TOKIMEC", "AMI", "SAMYUNG NAVTEX", "TOKYO KEIKI",
                                "HYUNDAI", "STRATUM FIVE SSAS", "SM ELECTRICS", "MARTEK", "JOTRON", "INTELLIAN",
                                "FBB LAUNCH PAD", "DANLEC", "STX", "RM YOUNG"]
                        },
                        "model": {
                            "type": "string",
                            "description": "The specific model name or number of the navigation equipment to filter by."
                        },
                        "equipment": {
                            "type": "string",
                            "description": "The specific equipment type to filter by.",
                            "enum": ["AIS", "RADAR", "MF-HF", "ECDIS", "SPEED LOG", "BNWAS", "GPS", "GYRO", "AUTO PILOT",
                                "NAVTEX", "ECHO-SOUNDER", "VDR", "VHF", "SATC", "INMARSAT-C", "FBB", "SSAS", "ANEMOMETER",
                                "SATELLITE LOG", "WEATHER FAX", "PUBLIC ALARM AND TALK BACK SYSTEM"]
                        }
                    },
                    "required": []
                }
            },
            {
                name: "search_manual_content",
                description: "Retrieves information corresponding to user query using semantic search from navigation manuals based on the maker, equipment, model, or manual type, whichever available. Pass an empty string if not available, that field will not be filtered. Filters, if passed, should be exact names.",
                inputSchema: {
                    "type": "object",
                    "properties": {
                        "maker": {
                            "type": "string",
                            "description": "Optional. The specific manufacturer of the navigation equipment. Examples: 'Simrad', 'JMA'."
                        },
                        "model": {
                            "type": "string",
                            "description": "Optional. The specific model name or number of the navigation equipment. Examples: 'GP-90', 'FMD-3200', 'JMA-1030'."
                        },
                        "equipment": {
                            "type": "string",
                            "description": "Optional. The specific equipment type of the navigation equipment. Examples: 'GPS', 'Radar', 'Autopilot'."
                        },
                        "manual_type": {
                            "type": "string",
                            "description": "Optional. Type of manual required. Options: installation, operation, or service."
                        },
                        "user_query": {
                            "type": "string",
                            "description": "Optional. Natural language query for vector search. If provided, this will be used for semantic search using document embeddings."
                        }
                    },
                    "required": []
                }
            },
            // {
            //   name: "google_search",
            //   description: "Perform a Google search using a natural language query. Returns relevant web results.",
            //   inputSchema: {
            //     "type": "object",
            //     "required": ["query"],
            //     "properties": {
            //       "query": {
            //         "type": "string",
            //         "description": "The search query to be executed."
            //       }
            //     },
            //     "additionalProperties": false
            //   }
            // },
        ] };
});
/**
 * Handler for tool execution
 */
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;
    logger.log(`Received tool call: ${name} with args: ` + JSON.stringify(args, null, 2));
    try {
        switch (name) {
            case "smart_navigation_manual_search":
                return await handleSmartNavigationManualSearch(args);
            case "list_equipment_manufacturers":
                return await handleListEquipmentManufacturers(args);
            case "list_navigation_equipment_types":
                return await handleListNavigationEquipmentTypes(args);
            case "list_navigation_equipment_models":
                return await handleListNavigationEquipmentModels(args);
            case "find_equipment_manuals":
                return await handleFindEquipmentManuals(args);
            case "search_manual_content":
                return await handleSearchManualContent(args);
            // case "google_search":
            //   return await handleGoogleSearch(args);
            default:
                throw new Error(`Unknown tool: ${name}`);
        }
    }
    catch (error) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
        logger.log(`Error in tool ${name}: ${errorMessage}`);
        throw new Error(`Tool execution failed: ${errorMessage}`);
    }
});
/**
 * Handle smart navigation manual search tool
 */
async function handleSmartNavigationManualSearch(args) {
    logger.log('Smart navigation manual search with args: ' + JSON.stringify(args, null, 2));
    try {
        const collection = NAVIGATION_MANUALS_COLLECTION;
        const client = getTypesenseClient();
        // Extract arguments with defaults
        const query = args.query || "";
        const searchType = args.search_type || (query ? "hybrid" : "browse");
        const filters = args.filters || {};
        const maxResults = args.max_results || DEFAULT_SEARCH_LIMIT;
        // Build filter string from filters dict
        const filterParts = [];
        for (const [field, value] of Object.entries(filters)) {
            if (value) {
                if (field === "page_range" && Array.isArray(value) && value.length === 2) {
                    filterParts.push(`pageNumber:>=${value[0]} && pageNumber:<=${value[1]}`);
                }
                else {
                    filterParts.push(`${field}:${sanitizeFilterValue(value)}`);
                }
            }
        }
        const filterString = filterParts.length > 0 ? filterParts.join(" && ") : null;
        // Enhance query based on intent
        const enhancedQuery = query;
        // const searchQuery: any = {
        //   q: enhancedQuery,
        //   query_by: "embedding",
        //   prefix: false,
        //   per_page: maxResults,
        //   include_fields: "documentHeader,documentName,chapter,section,revNo,originalText,documentLink"
        // };
        // If query is empty, use browse mode (non-embedding search)
        let searchQuery;
        if (searchType === "browse") {
            // Browse mode: no query, just list/filter by a plain text field
            searchQuery = {
                q: '*',
                query_by: 'embText', // Use a plain text field for browsing
                prefix: false,
                per_page: maxResults,
                include_fields: "documentHeader,documentName,chapter,section,revNo,originalText,documentLink"
            };
        }
        else if (searchType === "hybrid") {
            // Hybrid mode: semantic search using embeddings
            searchQuery = {
                q: enhancedQuery,
                query_by: 'embedding',
                prefix: false,
                per_page: maxResults,
                include_fields: "documentHeader,documentName,chapter,section,revNo,originalText,documentLink"
            };
        }
        else {
            // Fallback: you can handle other search types or throw an error
            throw new Error(`Unknown search type: ${searchType}`);
        }
        // Add filters if any
        if (filterString) {
            searchQuery.filter_by = filterString;
        }
        // Execute search
        const results = await client.collections(collection).documents().search(searchQuery);
        const hits = results.hits || [];
        const totalFound = results.found || 0;
        const allHits = hits;
        // If we have results and <= 50, apply Cohere reranking
        if (allHits && getCohereApiKey() && allHits.length <= COHERE_MAX_DOCUMENTS) {
            try {
                const docsWithOriginals = [];
                for (const hit of allHits) {
                    const document = hit.document || {};
                    if (document.originalText) {
                        docsWithOriginals.push({
                            text: document.originalText,
                            original: document
                        });
                    }
                }
                const docs = docsWithOriginals.map(doc => doc.text);
                const cohere = new CohereClientV2({ token: getCohereApiKey() });
                const reranked = await cohere.rerank({
                    model: COHERE_MODEL,
                    query: query,
                    documents: docs,
                    topN: Math.min(COHERE_RERANK_LIMIT, docs.length)
                });
                const topResults = reranked.results.map(result => docsWithOriginals[result.index].original);
                // Collect link data for artifact
                const linkData = [];
                for (const doc of topResults) {
                    const docAny = doc;
                    if (docAny.documentLink) {
                        linkData.push({
                            title: docAny.documentName || "Unknown Document",
                            url: docAny.documentLink
                        });
                    }
                }
                const artifactData = getListOfArtifacts("smart_navigation_manual_search", linkData);
                const content = createTextContent(JSON.stringify(topResults, null, 2), "Reranked Company Manual Search Results", "json");
                return {
                    content: [content, ...artifactData]
                };
            }
            catch (error) {
                logger.error('Error in Cohere reranking: ' + (error instanceof Error ? error.message : 'Unknown error'));
            }
        }
        // Format results
        const formattedResults = {
            search_metadata: {
                query: query,
                search_type: searchType,
                filters_applied: filters,
                total_found: results.found || 0,
                returned: Math.min(results.found || 0, maxResults)
            },
            results: []
        };
        for (const hit of results.hits || []) {
            const doc = hit.document;
            const hitData = formatHit(hit, doc);
            formattedResults.results.push(hitData);
        }
        const title = query ? `Search Results: ${query.substring(0, 50)}...` : "Browse Results";
        const content = createTextContent(JSON.stringify(formattedResults, null, 2), title, "json");
        const linkData = [];
        for (const hit of results.hits || []) {
            const document = hit.document || {};
            if (document.documentLink) {
                linkData.push({
                    title: document.documentName || "Unknown Document",
                    url: document.documentLink
                });
            }
        }
        const artifacts = getListOfArtifacts("smart_navigation_manual_search", linkData);
        return {
            content: [content, ...artifacts]
        };
    }
    catch (error) {
        return {
            content: [
                createTextContent(`Error retrieving search results: ${error instanceof Error ? error.message : 'Unknown error'}`, "Error", "json")
            ]
        };
    }
}
/**
 * Handle list equipment manufacturers tool
 */
async function handleListEquipmentManufacturers(args) {
    logger.log('List equipment manufacturers with args: ' + JSON.stringify(args, null, 2));
    try {
        // Query to get unique makers from the navigation_manuals collection using faceting
        const collection = NAVIGATION_MANUALS_COLLECTION;
        const client = getTypesenseClient();
        // Use faceting to get ALL unique values for the 'maker' field
        const query = {
            q: "*",
            query_by: "maker", // Query by maker field since that's what we're faceting on
            facet_by: "maker", // This will return all unique values in the maker field
            max_facet_values: 1000, // Set a high limit for facet values
            per_page: 0 // We don't need actual search results, just facets
        };
        const results = await client.collections(collection).documents().search(query);
        // Extract unique makers from facet results
        let makers = [];
        if (results.facet_counts && results.facet_counts.length > 0) {
            const makerFacet = results.facet_counts.find((facet) => facet.field_name === 'maker');
            if (makerFacet && makerFacet.counts) {
                makers = makerFacet.counts
                    .map((count) => count.value)
                    .filter((maker) => maker && maker.trim()) // Filter out empty/null makers
                    .sort(); // Sort alphabetically for consistent output
            }
        }
        logger.log(`Found ${makers.length} unique manufacturers using faceting`);
        return {
            content: [
                createTextContent(JSON.stringify(makers, null, 2), "Complete List of Navigation Equipment Manufacturers", "json")
            ]
        };
    }
    catch (error) {
        logger.error('Error in list equipment manufacturers: ' + (error instanceof Error ? error.message : 'Unknown error'));
        return {
            content: [
                createTextContent(`Error retrieving manufacturers list: ${error instanceof Error ? error.message : 'Unknown error'}`, "Error", "json")
            ]
        };
    }
}
/**
 * Handle list navigation equipment types tool
 */
async function handleListNavigationEquipmentTypes(args) {
    logger.log('List navigation equipment types with args: ' + JSON.stringify(args, null, 2));
    try {
        const collection = NAVIGATION_MANUALS_COLLECTION;
        const filters = [];
        if (args.maker && args.maker.trim()) {
            filters.push(`maker:=${args.maker}`);
        }
        // Use faceting to get ALL unique values for the 'equipment' field
        const query = {
            q: "*",
            query_by: "equipment", // Query by equipment field since that's what we're faceting on
            facet_by: "equipment", // This will return all unique values in the equipment field
            max_facet_values: 1000, // Set a high limit for facet values
            per_page: 0 // We don't need actual search results, just facets
        };
        // Only add filter_by if we have filters
        if (filters.length > 0) {
            query.filter_by = filters.join(" && ");
        }
        // Query to get unique equipment types
        const client = getTypesenseClient();
        const results = await client.collections(collection).documents().search(query);
        // Extract unique equipment types from facet results
        let equipments = [];
        if (results.facet_counts && results.facet_counts.length > 0) {
            const equipmentFacet = results.facet_counts.find((facet) => facet.field_name === 'equipment');
            if (equipmentFacet && equipmentFacet.counts) {
                equipments = equipmentFacet.counts
                    .map((count) => count.value)
                    .filter((equipment) => equipment && equipment.trim()) // Filter out empty/null equipment types
                    .sort(); // Sort alphabetically for consistent output
            }
        }
        logger.log(`Found ${equipments.length} unique equipment types using faceting`);
        return {
            content: [
                createTextContent(JSON.stringify(equipments, null, 2), "Complete List of Navigation Equipment Types", "json")
            ]
        };
    }
    catch (error) {
        logger.error('Error in list navigation equipment types: ' + (error instanceof Error ? error.message : 'Unknown error'));
        return {
            content: [
                createTextContent(`Error retrieving equipment types list: ${error instanceof Error ? error.message : 'Unknown error'}`, "Error", "json")
            ]
        };
    }
}
/**
 * Handle list navigation equipment models tool
 */
async function handleListNavigationEquipmentModels(args) {
    logger.log('List navigation equipment models with args: ' + JSON.stringify(args, null, 2));
    try {
        const collection = NAVIGATION_MANUALS_COLLECTION;
        const filters = [];
        if (args.maker && args.maker.trim()) {
            filters.push(`maker:=${args.maker}`);
        }
        if (args.equipment && args.equipment.trim()) {
            filters.push(`equipment:=${args.equipment}`);
        }
        // Use a high pagination limit to ensure we get all models
        // Note: We can't use faceting here because we need additional fields (maker, equipment, documentType)
        const query = {
            q: "*",
            query_by: "model",
            group_by: "model",
            per_page: 100, // Increased from 100 to 1000 to capture more models
            include_fields: "model,maker,equipment,documentType"
        };
        // Only add filter_by if we have filters
        if (filters.length > 0) {
            query.filter_by = filters.join(" && ");
        }
        // Query to get unique models with additional fields
        const client = getTypesenseClient();
        const results = await client.collections(collection).documents().search(query);
        // Check if we might be hitting pagination limits
        const totalFound = results.found || 0;
        const groupedHitsCount = (results.grouped_hits || []).length;
        if (groupedHitsCount >= 1000) {
            logger.log(`WARNING: Retrieved ${groupedHitsCount} unique models, but there might be more. Consider implementing pagination or using filters.`);
        }
        logger.log(`Found ${groupedHitsCount} unique models out of ${totalFound} total documents`);
        // Extract relevant data from results
        const modelData = [];
        const seenModels = new Set();
        if (results.grouped_hits) {
            for (const group of results.grouped_hits) {
                // Get the model name from the group key
                const model = group.group_key[0];
                // Get the first document in the group to extract other fields
                if (group.hits && group.hits.length > 0 && group.hits[0].document) {
                    const doc = group.hits[0].document;
                    // Only include fields not used as filters
                    const entry = { model: model };
                    if (!args.maker) {
                        entry.maker = doc.maker || "";
                    }
                    if (!args.equipment) {
                        entry.equipment = doc.equipment || "";
                    }
                    // Always include document type
                    entry.available_manuals = doc.documentType || "";
                    modelData.push(entry);
                    seenModels.add(model);
                }
            }
        }
        // Add metadata about potential pagination limits
        const responseData = {
            models: modelData,
            metadata: {
                total_unique_models: modelData.length,
                total_documents_found: totalFound,
                filters_applied: filters.length > 0 ? filters : "none",
                pagination_warning: groupedHitsCount >= 1000 ? "Results may be truncated. Consider using filters to narrow the search." : null
            }
        };
        return {
            content: [
                createTextContent(JSON.stringify(responseData, null, 2), "Complete List of Navigation Equipment Models", "json")
            ]
        };
    }
    catch (error) {
        logger.error('Error in list navigation equipment models: ' + (error instanceof Error ? error.message : 'Unknown error'));
        return {
            content: [
                createTextContent(`Error retrieving equipment models list: ${error instanceof Error ? error.message : 'Unknown error'}`, "Error", "json")
            ]
        };
    }
}
/**
 * Handle find equipment manuals tool
 */
async function handleFindEquipmentManuals(args) {
    logger.log('Find equipment manuals with args: ' + JSON.stringify(args, null, 2));
    try {
        const collection = NAVIGATION_MANUALS_COLLECTION;
        const maker = args.maker;
        const model = args.model;
        const equipment = args.equipment;
        const maxResults = 100;
        const filters = [];
        if (maker) {
            filters.push(`maker:${maker}`);
        }
        if (model) {
            filters.push(`model:${model}`);
        }
        if (equipment) {
            filters.push(`equipment:${equipment}`);
        }
        if (filters.length === 0) {
            return {
                content: [
                    createTextContent(JSON.stringify({
                        error: "At least one filter (maker, model, or equipment) must be provided to avoid fetching all documents",
                        suggestion: "Please specify at least one of: maker, model, or equipment type",
                        available_actions: "try using other tools to get the maker / model / equipment type",
                        parameters_received: {
                            maker: maker,
                            model: model,
                            equipment: equipment,
                            max_results: maxResults
                        }
                    }, null, 2), "Error: No Filters Provided", "json")
                ]
            };
        }
        // Create search parameters
        const query = {
            q: "*",
            group_by: "documentName",
            per_page: maxResults, // Now 1000 instead of 100
            include_fields: "documentName,documentType,documentLink,maker,model,equipment",
            filter_by: filters.join(" && ")
        };
        // Execute search
        const client = getTypesenseClient();
        const results = await client.collections(collection).documents().search(query);
        // Check if we might be hitting pagination limits
        const totalFound = results.found || 0;
        const groupedHitsCount = (results.grouped_hits || []).length;
        if (groupedHitsCount >= 100) {
            logger.log(`WARNING: Retrieved ${groupedHitsCount} unique manuals, but there might be more. Consider using more specific filters.`);
        }
        logger.log(`Found ${groupedHitsCount} unique manuals out of ${totalFound} total documents`);
        // Extract results from grouped hits
        const manuals = (results.grouped_hits || []).map((group) => ({
            documentName: group.group_key[0],
            documentType: group.hits[0]?.document?.documentType || "",
            documentLink: group.hits[0]?.document?.documentLink || "",
            maker: group.hits[0]?.document?.maker || "",
            model: group.hits[0]?.document?.model || "",
            equipment: group.hits[0]?.document?.equipment || ""
        }));
        const linkData = [];
        for (const document of manuals) {
            if (document.documentLink) {
                linkData.push({
                    title: document.documentName || "Unknown Document",
                    url: document.documentLink
                });
            }
        }
        const artifacts = getListOfArtifacts("find_equipment_manuals", linkData);
        const content = createTextContent(JSON.stringify(manuals, null, 2), "List of Navigation Equipment Manuals", "json");
        return {
            content: [content, ...artifacts]
        };
    }
    catch (error) {
        logger.error('Error in find equipment manuals: ' + (error instanceof Error ? error.message : 'Unknown error'));
        return {
            content: [
                createTextContent(`Error retrieving equipment manuals: ${error instanceof Error ? error.message : 'Unknown error'}`, "Error", "json")
            ]
        };
    }
}
/**
 * Handle search manual content tool
 */
async function handleSearchManualContent(args) {
    logger.log('Search manual content with args: ' + JSON.stringify(args, null, 2));
    try {
        const collection = NAVIGATION_MANUALS_COLLECTION;
        const filters = [];
        const userQuery = args.user_query || "";
        const cohereLimit = 5;
        if (args.maker && args.maker.trim()) {
            filters.push(`maker:=${args.maker}`);
        }
        if (args.model && args.model.trim()) {
            filters.push(`model:=${args.model}`);
        }
        if (args.equipment && args.equipment.trim()) {
            filters.push(`equipment:=${args.equipment}`);
        }
        if (args.manual_type && args.manual_type.trim()) {
            if (args.manual_type === "installation") {
                filters.push(`documentType:=Installation Manual`);
            }
            else if (args.manual_type === "operation") {
                filters.push(`documentType:=Operation Manual OR documentType:=Instruction Manual`);
            }
            else if (args.manual_type === "service") {
                filters.push(`documentType:=Service Manual`);
            }
            else {
                throw new Error(`Invalid manual type: ${args.manual_type}`);
            }
        }
        // Create search parameters with proper handling for None values
        const query = {
            q: userQuery || "*",
            query_by: "embedding,embText",
            per_page: 10, // Increased to have more candidates for reranking
            include_fields: "documentName,documentType,documentLink,equipment,maker,model,originalText",
            prefix: false
        };
        // Only add filter_by if we have filters
        if (filters.length > 0) {
            query.filter_by = filters.join(" && ");
        }
        const client = getTypesenseClient();
        const results = await client.collections(collection).documents().search(query);
        // If we have results and a user query, apply Cohere reranking
        if (results.hits && userQuery && getCohereApiKey()) {
            try {
                // Collect documents with their original text for reranking
                const docsWithOriginals = [];
                for (const hit of results.hits) {
                    const document = hit.document;
                    if (document && document.originalText) {
                        docsWithOriginals.push({
                            text: document.originalText,
                            original: document
                        });
                    }
                }
                // Extract just the text for reranking
                const docs = docsWithOriginals.map(doc => doc.text);
                // Initialize Cohere client
                const cohere = new CohereClientV2({ token: getCohereApiKey() });
                // Perform reranking
                const reranked = await cohere.rerank({
                    model: COHERE_MODEL,
                    query: userQuery,
                    documents: docs,
                    topN: cohereLimit
                });
                // Get the top results based on reranking
                const topResults = reranked.results.map(result => docsWithOriginals[result.index].original);
                const linkData = [];
                for (const document of topResults) {
                    if (document.documentLink) {
                        linkData.push({
                            title: document.documentName || "Unknown Document",
                            url: document.documentLink
                        });
                    }
                }
                const artifacts = getListOfArtifacts("search_manual_content", linkData);
                const content = createTextContent(JSON.stringify(topResults, null, 2), "Reranked Navigation Manual Search Results", "json");
                return {
                    content: [content, ...artifacts]
                };
            }
            catch (error) {
                logger.error('Error in Cohere reranking: ' + (error instanceof Error ? error.message : 'Unknown error'));
                // Fall back to original results if reranking fails
            }
        }
        const hits = results.hits || [];
        // Return original results if no reranking was done
        const linkData = [];
        for (const hit of hits) {
            const doc = hit.document || {};
            if (doc.documentLink) {
                linkData.push({
                    title: doc.documentName || "Unknown Document",
                    url: doc.documentLink
                });
            }
        }
        const artifacts = getListOfArtifacts("search_manual_content", linkData);
        const content = createTextContent(JSON.stringify(hits.map((hit) => hit.document), null, 2), "Navigation Manual Search Results", "json");
        return {
            content: [content, ...artifacts]
        };
    }
    catch (error) {
        logger.error('Error in search manual content: ' + (error instanceof Error ? error.message : 'Unknown error'));
        return {
            content: [
                createTextContent(`Error searching manual content: ${error instanceof Error ? error.message : 'Unknown error'}`, "Error", "json")
            ]
        };
    }
}
/**
 * Handle Google search tool
 */
// async function handleGoogleSearch(args: any) {
//   logger.log('Google search with args: ' + JSON.stringify(args, null, 2));
//   try {
//     const query = args.query;
//     if (!query) {
//       throw new Error("Search query is required");
//     }
//     const url = "https://api.perplexity.ai/chat/completions";
//     const headers = {
//       "Content-Type": "application/json",
//       "Authorization": `Bearer ${getPerplexityApiKey()}`
//     };
//     const payload = {
//       model: "sonar-reasoning-pro",
//       messages: [
//         {
//           role: "system",
//           content: "You are an expert assistant helping with reasoning tasks."
//         },
//         {
//           role: "user",
//           content: query
//         }
//       ],
//       max_tokens: 2000,
//       temperature: 0.2,
//       top_p: 0.9,
//       search_domain_filter: null,
//       return_images: false,
//       return_related_questions: false,
//       search_recency_filter: "week",
//       top_k: 0,
//       stream: false,
//       presence_penalty: 0,
//       frequency_penalty: 1,
//       response_format: null
//     };
//     const response = await fetch(url, {
//       method: 'POST',
//       headers: headers,
//       body: JSON.stringify(payload)
//     });
//     if (response.ok) {
//       const result = await response.json() as any;
//       const citations = result.citations || [];
//       const content = result.choices?.[0]?.message?.content || "";
//       return {
//         content: [
//           createTextContent(
//             `Response: ${content}\n\nCitations: ${JSON.stringify(citations, null, 2)}`,
//             "Google Search Results",
//             "text"
//           )
//         ]
//       };
//     } else {
//       const errorText = await response.text();
//       return {
//         content: [
//           createTextContent(
//             `Error: ${response.status}, ${errorText}`,
//             "Search Error",
//             "text"
//           )
//         ]
//       };
//     }
//   } catch (error) {
//     logger.error('Failure to execute the search operation: ' + (error instanceof Error ? error.message : 'Unknown error'));
//     return {
//       content: [
//         createTextContent(
//           `Error executing search: ${error instanceof Error ? error.message : 'Unknown error'}`,
//           "Error",
//           "text"
//         )
//       ]
//     };
//   }
// }
/**
 * Handle upsert manual tool
 */
async function handleUpsertManual(args) {
    const db = getDb();
    const collection = db.collection('navigation_manuals');
    // Implementation placeholder
    logger.log('Upserting manual with data: ' + JSON.stringify(args, null, 2));
    // TODO: Implement upsert logic
    return {
        content: [
            {
                type: "text",
                text: "Manual upsert functionality not yet implemented"
            }
        ]
    };
}
/**
 * Handle search manuals tool
 */
async function handleSearchManuals(args) {
    // Implementation placeholder
    logger.log('Searching manuals with query: ' + JSON.stringify(args, null, 2));
    // TODO: Implement search functionality
    return {
        content: [
            {
                type: "text",
                text: "Manual search functionality not yet implemented"
            }
        ]
    };
}
/**
 * Handle get manual by ID tool
 */
async function handleGetManualById(args) {
    // Implementation placeholder
    logger.log('Getting manual by ID: ' + JSON.stringify(args, null, 2));
    // TODO: Implement get by ID functionality
    return {
        content: [
            {
                type: "text",
                text: "Get manual by ID functionality not yet implemented"
            }
        ]
    };
}
/**
 * Handle list manuals tool
 */
async function handleListManuals(args) {
    // Implementation placeholder
    logger.log('Listing manuals with filters: ' + JSON.stringify(args, null, 2));
    // TODO: Implement list functionality
    return {
        content: [
            {
                type: "text",
                text: "List manuals functionality not yet implemented"
            }
        ]
    };
}
/**
 * Main function to start the server
 */
async function main() {
    try {
        // Parse configuration
        manualConfig = parseArgs();
        logger.log('Parsed configuration successfully');
        // Set runtime configuration for API keys
        setRuntimeConfig({
            openaiApiKey: manualConfig.openaiApiKey,
            cohereApiKey: manualConfig.cohereApiKey,
            perplexityApiKey: manualConfig.perplexityApiKey
        });
        logger.log('Set runtime API key configuration');
        // Connect to MongoDB
        await connectToMongoDB(manualConfig.mongoUri);
        logger.log('Connected to MongoDB successfully');
        // Initialize Typesense client
        initializeTypesense({
            host: manualConfig.typesenseHost,
            port: manualConfig.typesensePort,
            protocol: manualConfig.typesenseProtocol,
            apiKey: manualConfig.typesenseApiKey
        });
        logger.log('Initialized Typesense client successfully');
        // Start the server
        const transport = new StdioServerTransport();
        await server.connect(transport);
        logger.log('Navigation Equipment Manuals MCP Server started successfully');
    }
    catch (error) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
        logger.log(`Failed to start server: ${errorMessage}`);
        process.exit(1);
    }
}
// Start the server
main().catch((error) => {
    logger.log(`Unhandled error: ${error}`);
    process.exit(1);
});
//# sourceMappingURL=index.js.map