UNPKG

survey-mcp-server

Version:

Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management

380 lines 15.5 kB
/** * Secure Survey MCP Server * Refactored to use secure service layer with proper validation and error handling */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourceTemplatesRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { SecureToolHandler } from "./tools/secure-handler.js"; import { secureConfigManager } from "./config/index.js"; import { serviceManager } from "./services/index.js"; import { logger } from "./utils/logger.js"; import { toolDefinitions } from "./tools/schema.js"; import { Command } from 'commander'; import { promptList, handleGetPrompt } from "./prompts/index.js"; import { resourceList, handleReadResource } from "./resources/index.js"; import { ConfigurationError } from "./middleware/error-handling.js"; async function parseAndValidateCommandLineArgs() { const program = new Command(); program .name('survey-mcp-server') .description('Secure Survey MCP Server with comprehensive security and service layer') .version('2.0.0') .option('--mongodb-uri <uri>', 'MongoDB connection URI') .option('--mongodb-db-name <name>', 'MongoDB database name') .option('--mongodb-uri-syia-etl <uri>', 'MongoDB connection URI for syia-etl') .option('--db-name-syia-etl <name>', 'MongoDB database name for syia-etl') .option('--typesense-host <host>', 'Typesense host') .option('--typesense-port <port>', 'Typesense port') .option('--typesense-api-key <key>', 'Typesense API key') .option('--typesense-protocol <protocol>', 'Typesense protocol') .option('--openai-api-key <key>', 'OpenAI API key') .option('--perplexity-api-key <key>', 'Perplexity API key') .option('--api-token <token>', 'API authentication token') .option('--api-base-url <url>', 'API base URL') .option('--downloads-dir <dir>', 'Downloads directory path') .option('--log-level <level>', 'Log level (debug, info, warn, error)', 'info') .option('--env-file <path>', 'Path to .env file') .option('--strict-validation', 'Enable strict configuration validation', false) .option('--security-report', 'Generate security report and exit', false) .allowUnknownOption() .parse(); const options = program.opts(); // Validate CLI arguments const cliValidation = secureConfigManager.validateCliArguments(options); if (!cliValidation.isValid) { logger.error('Invalid CLI arguments:', cliValidation.errors); throw new ConfigurationError(`Invalid CLI arguments: ${cliValidation.errors.join(', ')}`); } // Load custom .env file if specified if (options.envFile) { logger.info(`Loading custom .env file: ${options.envFile}`); const { loadEnvFile } = await import('./utils/config.js'); loadEnvFile(options.envFile); } // Override environment variables with sanitized CLI arguments const sanitizedArgs = cliValidation.sanitizedArgs; if (sanitizedArgs.mongodbUri) process.env.MONGODB_URI = sanitizedArgs.mongodbUri; if (sanitizedArgs.mongodbDbName) process.env.MONGODB_DB_NAME = sanitizedArgs.mongodbDbName; if (sanitizedArgs.mongodbUriSyiaEtl) process.env.MONGODB_URI_SYIA_ETL = sanitizedArgs.mongodbUriSyiaEtl; if (sanitizedArgs.dbNameSyiaEtl) process.env.DB_NAME_SYIA_ETL = sanitizedArgs.dbNameSyiaEtl; if (sanitizedArgs.typesenseHost) process.env.TYPESENSE_HOST = sanitizedArgs.typesenseHost; if (sanitizedArgs.typesensePort) process.env.TYPESENSE_PORT = sanitizedArgs.typesensePort; if (sanitizedArgs.typesenseApiKey) process.env.TYPESENSE_API_KEY = sanitizedArgs.typesenseApiKey; if (sanitizedArgs.typesenseProtocol) process.env.TYPESENSE_PROTOCOL = sanitizedArgs.typesenseProtocol; if (sanitizedArgs.openaiApiKey) process.env.OPENAI_API_KEY = sanitizedArgs.openaiApiKey; if (sanitizedArgs.perplexityApiKey) process.env.PERPLEXITY_API_KEY = sanitizedArgs.perplexityApiKey; if (sanitizedArgs.apiToken) process.env.API_TOKEN = sanitizedArgs.apiToken; if (sanitizedArgs.apiBaseUrl) process.env.API_BASE_URL = sanitizedArgs.apiBaseUrl; if (sanitizedArgs.downloadsDir) process.env.DOWNLOADS_DIR = sanitizedArgs.downloadsDir; if (sanitizedArgs.logLevel) process.env.LOG_LEVEL = sanitizedArgs.logLevel; return sanitizedArgs; } async function validateConfiguration(strictMode = false) { try { logger.info('Validating configuration...'); await secureConfigManager.validateCurrentConfig({ validateOnLoad: true, strictMode, logSecurityIssues: true, throwOnValidationError: strictMode, environment: process.env.NODE_ENV || 'production' }); const securityReport = secureConfigManager.generateSecurityReport(); if (securityReport.validationStatus === 'failed') { logger.warn(`Configuration validation issues: ${securityReport.securityIssuesCount} security issues detected`); if (securityReport.recommendations.length > 0) { logger.warn('Security recommendations:'); securityReport.recommendations.forEach(rec => logger.warn(` - ${rec}`)); } if (strictMode) { throw new ConfigurationError('Configuration validation failed in strict mode'); } } else { logger.info('✅ Configuration validation passed'); } return securityReport; } catch (error) { logger.error('Configuration validation error:', error); throw error; } } async function initializeServices() { try { logger.info('Initializing secure service layer...'); await serviceManager.initialize(); const systemHealth = await serviceManager.getSystemHealth(); logger.info(`Service manager initialized - ${systemHealth.summary.healthyServices}/${systemHealth.summary.totalServices} services healthy`); if (!systemHealth.isHealthy) { const unhealthyServices = Object.entries(systemHealth.services) .filter(([_, service]) => !service.isHealthy) .map(([name, _]) => name); logger.warn(`Unhealthy services: ${unhealthyServices.join(', ')}`); } } catch (error) { logger.error('Service initialization failed:', error); throw error; } } async function createSecureServer() { const server = new Server({ name: "secure-survey-mcp-server", version: "2.0.0" }, { capabilities: { resources: { read: true, list: true, templates: true }, tools: { list: true, call: true }, prompts: { list: true, get: true } } }); logger.info("Secure server initialized", { version: "2.0.0", capabilities: ['resources', 'tools', 'prompts'], security_enabled: true }); return server; } async function setupServerHandlers(server) { const secureToolHandler = new SecureToolHandler(); // Resource handlers server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: resourceList }; }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { try { const content = await handleReadResource(request.params.uri); return { contents: [{ type: "text", text: content }] }; } catch (error) { logger.error('Resource read error:', error); throw error; } }); server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { return { templates: [] }; }); // Tool handlers with security server.setRequestHandler(ListToolsRequestSchema, async (request) => { try { logger.info("Handling secure ListToolsRequest", { request: request.params, securityEnabled: true }); let tools = [...toolDefinitions]; const params = request.params || {}; const category = params.category; const search = params.search; const includeDeprecated = params.include_deprecated; // Apply category filter if provided if (category) { tools = tools.filter(tool => { const toolName = tool.name.toLowerCase(); return toolName.includes(category.toLowerCase()); }); } // Apply search filter if provided if (search) { const searchTerm = search.toLowerCase(); tools = tools.filter(tool => tool.name.toLowerCase().includes(searchTerm) || (tool.description?.toLowerCase() || '').includes(searchTerm)); } // Filter out deprecated tools unless explicitly requested if (!includeDeprecated) { tools = tools.filter(tool => !tool.name.includes('deprecated')); } return { tools }; } catch (error) { logger.error('List tools error:', error); throw error; } }); // Secure tool call handler server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const result = await secureToolHandler.handleCallTool(request.params.name, request.params.arguments || {}, { enableAuditLogging: true, enableMetrics: true, maxExecutionTime: 60000, skipValidation: false, skipSanitization: false }); return { content: result }; } catch (error) { logger.error('Secure tool call error:', error); throw error; } }); // Prompt handlers server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: promptList }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { try { return handleGetPrompt(request.params.name, request.params.arguments); } catch (error) { logger.error('Prompt get error:', error); throw error; } }); logger.info('Secure server handlers configured'); } async function startHealthMonitoring() { // Start periodic health monitoring setInterval(async () => { try { const systemHealth = await serviceManager.getSystemHealth(); if (!systemHealth.isHealthy) { logger.warn('System health check failed', { unhealthyServices: systemHealth.summary.unhealthyServices, successRate: systemHealth.summary.overallSuccessRate }); } else { logger.debug('System health check passed', { healthyServices: systemHealth.summary.healthyServices, successRate: systemHealth.summary.overallSuccessRate }); } } catch (error) { logger.error('Health monitoring error:', error); } }, 60000); // Every minute logger.info('Health monitoring started'); } async function main() { try { logger.info("🚀 Starting Secure Survey MCP Server..."); // Parse and validate command line arguments const cliOptions = await parseAndValidateCommandLineArgs(); // Handle special commands if (cliOptions.securityReport) { await validateConfiguration(true); const report = secureConfigManager.generateSecurityReport(); console.log('\n=== Security Report ==='); console.log(`Validation Status: ${report.validationStatus.toUpperCase()}`); console.log(`Security Issues: ${report.securityIssuesCount}`); console.log(`Last Validation: ${report.lastValidation?.toISOString() || 'Never'}`); if (report.recommendations.length > 0) { console.log('\nRecommendations:'); report.recommendations.forEach(rec => console.log(` - ${rec}`)); } process.exit(0); } // Validate configuration await validateConfiguration(cliOptions.strictValidation); // Initialize secure service layer await initializeServices(); // Create and configure secure server const server = await createSecureServer(); await setupServerHandlers(server); // Start health monitoring await startHealthMonitoring(); // Connect to transport and start server const transport = new StdioServerTransport(); await server.connect(transport); logger.info("✅ Secure Survey MCP Server started successfully", { version: "2.0.0", securityEnabled: true, servicesHealthy: true }); } catch (error) { logger.error("💥 Fatal error starting secure server:", error); // Attempt graceful shutdown try { await serviceManager.gracefulShutdown(); } catch (shutdownError) { logger.error("Error during graceful shutdown:", shutdownError); } process.exit(1); } } // Graceful shutdown handling process.on('SIGTERM', async () => { logger.info('Received SIGTERM, starting graceful shutdown...'); try { await serviceManager.gracefulShutdown(); logger.info('Graceful shutdown completed'); process.exit(0); } catch (error) { logger.error('Error during graceful shutdown:', error); process.exit(1); } }); process.on('SIGINT', async () => { logger.info('Received SIGINT, starting graceful shutdown...'); try { await serviceManager.gracefulShutdown(); logger.info('Graceful shutdown completed'); process.exit(0); } catch (error) { logger.error('Error during graceful shutdown:', error); process.exit(1); } }); // Enhanced error handling process.on('uncaughtException', async (error) => { logger.error('Uncaught exception:', error); try { await serviceManager.gracefulShutdown(); } catch (shutdownError) { logger.error('Error during emergency shutdown:', shutdownError); } process.exit(1); }); process.on('unhandledRejection', async (reason, promise) => { logger.error('Unhandled promise rejection:', { reason, promise }); try { await serviceManager.gracefulShutdown(); } catch (shutdownError) { logger.error('Error during emergency shutdown:', shutdownError); } process.exit(1); }); // Start the server main().catch((error) => { logger.error("Fatal startup error:", error); process.exit(1); }); //# sourceMappingURL=index-secure.js.map