imo-publications-mcp-server
Version:
MCP server for IMO (International Maritime Organization) publications - Node.js TypeScript version
303 lines (263 loc) • 10.8 kB
JavaScript
import path from 'path';
import { fileURLToPath } from 'url';
import { spawn } from 'child_process';
import fs from 'fs';
import readline from 'readline';
import os from 'os';
// Debug info - write to a debug file
const debugFile = path.join(os.tmpdir(), 'imo-publications-debug.log');
const writeDebug = (message) => {
try {
fs.appendFileSync(debugFile, `${new Date().toISOString()} - ${message}\n`);
} catch (err) {
// Silently fail if we can't write to the debug file
}
};
// Start debugging
writeDebug('CLI script started');
writeDebug(`Node version: ${process.version}`);
writeDebug(`Platform: ${process.platform}`);
writeDebug(`CLI Arguments: ${process.argv.join(' ')}`);
writeDebug(`Is stdin a TTY: ${process.stdin.isTTY}`);
writeDebug(`Is stdout a TTY: ${process.stdout.isTTY}`);
writeDebug(`Process PID: ${process.pid}`);
writeDebug(`Executable path: ${process.execPath}`);
writeDebug(`Current directory: ${process.cwd()}`);
// Print debug file location to stderr (not stdout)
console.error(`Debug log: ${debugFile}`);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageRoot = path.resolve(__dirname, '..');
writeDebug(`__filename: ${__filename}`);
writeDebug(`__dirname: ${__dirname}`);
writeDebug(`packageRoot: ${packageRoot}`);
// Check if bin/cli.js is executable
try {
const stats = fs.statSync(__filename);
const isExecutable = !!(stats.mode & fs.constants.S_IXUSR);
writeDebug(`Is CLI executable: ${isExecutable}`);
// Make it executable if it's not
if (!isExecutable) {
fs.chmodSync(__filename, '755');
writeDebug('Made CLI executable');
}
} catch (err) {
writeDebug(`Error checking/setting executable: ${err.message}`);
}
// Check for stored configuration
let storedConfig = {};
const configDir = path.join(process.env.HOME || process.env.USERPROFILE, '.imo-publications-mcp');
const configFile = path.join(configDir, 'config.json');
try {
if (fs.existsSync(configFile)) {
storedConfig = JSON.parse(fs.readFileSync(configFile, 'utf8'));
writeDebug('Found stored configuration');
}
} catch (err) {
console.warn('Warning: Could not read stored configuration.');
writeDebug(`Error reading config: ${err.message}`);
}
// Parse command line arguments
const args = process.argv.slice(2);
let mongoUri = storedConfig.mongoUri || '';
let dbName = storedConfig.dbName || 'imo_publications';
let typesenseHost = storedConfig.typesenseHost || '';
let typesensePort = storedConfig.typesensePort || '';
let typesenseProtocol = storedConfig.typesenseProtocol || '';
let typesenseApiKey = storedConfig.typesenseApiKey || '';
let debug = false;
let nonInteractive = false;
// Detect if we're running under an MCP context (Claude/ChatGPT/etc.)
const isMcpContext =
!process.stdin.isTTY ||
process.env.npm_execpath?.includes('npx') ||
process.env.CLAUDE_API_KEY ||
args.includes('--non-interactive') || args.includes('-n');
writeDebug(`Detected MCP context: ${isMcpContext}`);
if (isMcpContext) {
nonInteractive = true;
writeDebug('Setting non-interactive mode due to MCP context detection');
}
// Process command line arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if ((arg === '--mongo-uri' || arg === '-m') && i + 1 < args.length) {
mongoUri = args[++i];
writeDebug('Found mongo uri in arguments');
} else if ((arg === '--db-name' || arg === '-d') && i + 1 < args.length) {
dbName = args[++i];
writeDebug('Found db name in arguments');
} else if ((arg === '--typesense-host' || arg === '-th') && i + 1 < args.length) {
typesenseHost = args[++i];
writeDebug('Found typesense host in arguments');
} else if ((arg === '--typesense-port' || arg === '-tp') && i + 1 < args.length) {
typesensePort = args[++i];
writeDebug('Found typesense port in arguments');
} else if ((arg === '--typesense-protocol' || arg === '-tpr') && i + 1 < args.length) {
typesenseProtocol = args[++i];
writeDebug('Found typesense protocol in arguments');
} else if ((arg === '--typesense-api-key' || arg === '-tk') && i + 1 < args.length) {
typesenseApiKey = args[++i];
writeDebug('Found typesense api key in arguments');
} else if (arg === '--debug') {
debug = true;
writeDebug('Debug mode enabled');
} else if (arg === '--non-interactive' || arg === '-n') {
nonInteractive = true;
writeDebug('Non-interactive mode enabled via flag');
} else if (arg === '--help' || arg === '-h') {
console.log(`
IMO Publications MCP Server - International Maritime Organization Publications API
Usage: npx imo-publications-mcp-server [options]
Options:
--mongo-uri, -m <uri> Your MongoDB URI (required if not stored)
--db-name, -d <name> Database name (default: imo_publications)
--typesense-host, -th <host> Typesense host (required if not stored)
--typesense-port, -tp <port> Typesense port (required if not stored)
--typesense-protocol, -tpr <protocol> Typesense protocol (http/https) (required if not stored)
--typesense-api-key, -tk <key> Typesense API key (required if not stored)
--debug Enable debug output
--non-interactive, -n Run in non-interactive mode (no prompt)
--help, -h Show this help message
Example:
npx imo-publications-mcp-server --mongo-uri YOUR_MONGO_URI --db-name imo_publications --typesense-host localhost --typesense-port 8108 --typesense-protocol http --typesense-api-key YOUR_API_KEY
Environment Variables:
MONGODB_URI MongoDB connection string
MONGODB_DB_NAME Database name
TYPESENSE_HOST Typesense server hostname
TYPESENSE_PORT Typesense server port
TYPESENSE_PROTOCOL Protocol to use (http/https)
TYPESENSE_API_KEY Typesense API key
COHERE_API_KEY Cohere API key for reranking
OPENAI_API_KEY OpenAI API key for LLM features
PERPLEXITY_API_KEY Perplexity API key
LLAMA_API_KEY LlamaParse API key
`);
process.exit(0);
}
}
// Check for required configuration
const requiredConfigs = [
{ value: mongoUri, name: 'MongoDB URI', arg: '--mongo-uri' },
{ value: typesenseHost, name: 'Typesense host', arg: '--typesense-host' },
{ value: typesensePort, name: 'Typesense port', arg: '--typesense-port' },
{ value: typesenseProtocol, name: 'Typesense protocol', arg: '--typesense-protocol' },
{ value: typesenseApiKey, name: 'Typesense API key', arg: '--typesense-api-key' }
];
const missingConfigs = requiredConfigs.filter(config => !config.value);
if (missingConfigs.length > 0) {
if (nonInteractive) {
console.error('Error: Missing required configuration in non-interactive mode:');
missingConfigs.forEach(config => {
console.error(` ${config.name}: Use ${config.arg} argument`);
});
writeDebug('Exiting: missing configuration in non-interactive mode');
process.exit(1);
}
// Interactive mode - prompt for missing configuration
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
let currentConfigIndex = 0;
const promptForConfig = () => {
if (currentConfigIndex >= missingConfigs.length) {
rl.close();
// Ask if user wants to save the configuration
const saveRl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
saveRl.question('Would you like to save this configuration for future use? (y/n): ', (saveAnswer) => {
saveRl.close();
if (saveAnswer.toLowerCase() === 'y' || saveAnswer.toLowerCase() === 'yes') {
try {
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
const configToSave = {
mongoUri,
dbName,
typesenseHost,
typesensePort,
typesenseProtocol,
typesenseApiKey
};
fs.writeFileSync(configFile, JSON.stringify(configToSave, null, 2));
console.log(`Configuration saved to ${configFile}`);
writeDebug('Configuration saved');
} catch (err) {
console.warn('Warning: Could not save configuration.');
writeDebug(`Error saving config: ${err.message}`);
}
}
startServer();
});
} else {
const config = missingConfigs[currentConfigIndex];
rl.question(`Please enter ${config.name}: `, (answer) => {
switch (config.arg) {
case '--mongo-uri':
mongoUri = answer;
break;
case '--typesense-host':
typesenseHost = answer;
break;
case '--typesense-port':
typesensePort = answer;
break;
case '--typesense-protocol':
typesenseProtocol = answer;
break;
case '--typesense-api-key':
typesenseApiKey = answer;
break;
}
currentConfigIndex++;
promptForConfig();
});
}
};
console.log('Missing required configuration. Please provide the following:');
promptForConfig();
} else {
startServer();
}
function startServer() {
writeDebug('Starting server with configuration');
// Set environment variables for the server
process.env.MONGODB_URI = mongoUri;
process.env.MONGODB_DB_NAME = dbName;
process.env.TYPESENSE_HOST = typesenseHost;
process.env.TYPESENSE_PORT = typesensePort;
process.env.TYPESENSE_PROTOCOL = typesenseProtocol;
process.env.TYPESENSE_API_KEY = typesenseApiKey;
writeDebug(`Environment variables set`);
// Try to start the server
const serverPath = path.join(packageRoot, 'dist', 'index.js');
writeDebug(`Trying to start server at: ${serverPath}`);
if (fs.existsSync(serverPath)) {
startServerWithPath(serverPath);
} else {
console.error('Error: Server not found. Please run "npm run build" first.');
writeDebug('Server file not found');
process.exit(1);
}
}
function startServerWithPath(serverPath) {
writeDebug(`Starting server process: node ${serverPath}`);
const child = spawn('node', [serverPath], {
stdio: 'inherit',
env: process.env
});
child.on('error', (error) => {
console.error('Error starting IMO Publications MCP server:', error);
writeDebug(`Server error: ${error.message}`);
process.exit(1);
});
child.on('exit', (code) => {
writeDebug(`Server exited with code: ${code}`);
process.exit(code);
});
}