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
JavaScript
/**
* 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