UNPKG

mcp-quiz-server

Version:

🧠 AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

770 lines (769 loc) 33.7 kB
"use strict"; /** * @moduleName: Modular MCP Handler - Production Architecture * @version: 2.0.0 * @since: 2025-07-25 * @lastUpdated: 2025-07-25 * @projectSummary: MCP handler with full modular architecture and registry integration * @techStack: TypeScript, JSON-RPC 2.0, MCP Protocol, AdvancedToolRegistry * @dependency: BaseMCPHandler, AdvancedToolRegistry, SecurityUtils * @interModuleDependency: All modular tool implementations, types, security * @requirementsTraceability: * {@link Requirements.REQ_MCP_001} (JSON-RPC 2.0 MCP Protocol) * {@link Requirements.REQ_MCP_002} (Dynamic Tool Registry) * @briefDescription: Full-featured MCP handler extending BaseMCPHandler with registry-based tool management * @methods: processRequest, initialize, getCapabilities, handleToolsCall, handleInitialize * @contributors: Claude Code Agent * @examples: * - const handler = new ModularMCPHandler(config); * - await handler.initialize(); * - const response = await handler.handleRequest(request); * @vulnerabilitiesAssessment: Registry-based validation, secure tool execution, comprehensive error handling */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.simplifiedModularHandler = exports.ModularMCPHandler = void 0; const security_config_1 = require("../../config/security-config"); const security_monitor_1 = require("../../monitoring/security-monitor"); const advanced_tool_registry_1 = require("../registry/advanced-tool-registry"); const audit_logger_1 = require("../security/audit-logger"); const sandbox_manager_1 = require("../security/sandbox-manager"); const secure_tool_executor_1 = require("../security/secure-tool-executor"); const mcp_types_1 = require("../types/mcp-types"); const base_mcp_handler_1 = require("./base-mcp-handler"); // Import modular utils components (now available!) const metrics_tracker_1 = require("./utils/metrics-tracker"); const request_validator_1 = require("./utils/request-validator"); const response_builder_1 = require("./utils/response-builder"); /** * Modular MCP handler with full infrastructure integration */ class ModularMCPHandler extends base_mcp_handler_1.BaseMCPHandler { constructor(configOrIntegration) { // Handle both old constructor signature and new integration options let config = {}; let integrationOptions = {}; if (configOrIntegration) { // Check if it's integration options (has our specific properties) if ('authService' in configOrIntegration || 'enableAuthentication' in configOrIntegration) { integrationOptions = configOrIntegration; } else { config = configOrIntegration; } } // Provide default config if none provided const defaultConfig = { serverInfo: { name: 'mcp-quiz-server', version: '2.0.0', description: 'MCP Quiz Server with modular architecture', }, enableLogging: process.env.NODE_ENV === 'development', logLevel: 'info', maxRequestSize: 1024 * 1024, // 1MB requestTimeout: 30000, // 30 seconds ...config, }; super(defaultConfig); this.initialized = false; this.secureExecutor = null; this.securityMonitor = null; this.authService = null; this.auditLogger = null; this.enableAuthentication = false; this.registry = advanced_tool_registry_1.toolRegistry; this.securityConfig = security_config_1.securityConfig; // Initialize modular components (use provided or create defaults) this.metricsTracker = integrationOptions.metricsTracker || new metrics_tracker_1.MetricsTracker(); this.requestValidator = integrationOptions.requestValidator || request_validator_1.RequestValidator; this.responseBuilder = integrationOptions.responseBuilder || response_builder_1.ResponseBuilder; // Initialize auth integration if provided this.authService = integrationOptions.authService || null; this.auditLogger = integrationOptions.auditLogger || null; this.enableAuthentication = integrationOptions.enableAuthentication || false; console.log(`🔧 MCP Handler Integration Status:`); console.log(` • Authentication: ${this.enableAuthentication ? '✅ Enabled' : '❌ Disabled'}`); console.log(` • Metrics Tracking: ✅ Enabled`); console.log(` • Request Validation: ✅ Enabled`); console.log(` • Response Building: ✅ Enabled`); if (this.auditLogger) console.log(` • Audit Logging: ✅ Enabled`); // Initialize security components if enabled this.initializeSecurity(); // Initialize database asynchronously in background (non-blocking) this.initializeDatabase(); } /** * Initialize security components based on configuration */ initializeSecurity() { try { console.log(`🔒 Initializing security: ${this.securityConfig.sandbox.mode.toUpperCase()} mode`); if (this.securityConfig.sandbox.enabled) { // Initialize sandbox manager and secure executor const sandboxManager = new sandbox_manager_1.SandboxManager(); const auditLogger = new audit_logger_1.AuditLogger(); this.secureExecutor = new secure_tool_executor_1.SecureToolExecutor(sandboxManager, auditLogger, { globalResourceLimits: this.securityConfig.sandbox.defaultResourceLimits, defaultCapabilities: this.securityConfig.sandbox.defaultCapabilities, toolOverrides: {}, rateLimits: { maxExecutionsPerMinute: 60, maxConcurrentExecutions: 10, }, violationResponse: { blockAfterViolations: this.securityConfig.sandbox.blockAfterViolations, blockDurationMs: this.securityConfig.sandbox.blockDurationMs, }, }); console.log('✅ Secure tool executor initialized'); } if (this.securityConfig.monitoring.enabled) { // Initialize security monitoring this.securityMonitor = (0, security_monitor_1.getSecurityMonitor)(this.securityConfig); console.log('✅ Security monitoring initialized'); } console.log(`📊 Security status: Sandbox=${this.securityConfig.sandbox.enabled ? 'ON' : 'OFF'}, ` + `Auth=${this.securityConfig.authentication.enabled ? 'ON' : 'OFF'}, ` + `Monitoring=${this.securityConfig.monitoring.enabled ? 'ON' : 'OFF'}`); } catch (error) { console.error('❌ Security initialization failed:', error); // Continue without security in development, fail in production if (this.securityConfig.environment === 'production') { throw new Error(`Critical: Security initialization failed in production: ${error}`); } } } /** * Execute tool securely through SecureToolExecutor */ async executeToolSecurely(toolName, args) { if (!this.secureExecutor) { throw new Error('Secure executor not initialized'); } // Get tool handler from registry const toolHandler = this.registry.getToolHandler(toolName); if (!toolHandler) { throw new Error(`Tool handler not found: ${toolName}`); } // Get tool definition from registry const toolDef = this.registry.getAllTools().find(t => t.name === toolName); if (!toolDef) { throw new Error(`Tool definition not found: ${toolName}`); } // Register tool with secure executor if not already registered try { this.secureExecutor.registerTool(toolDef, toolHandler); } catch (error) { // Tool might already be registered, continue const errorMessage = error instanceof Error ? error.message : String(error); if (!errorMessage.includes('already registered')) { throw error; } } // Execute tool securely return await this.secureExecutor.executeTool(toolName, args); } /** * Generate unique execution ID for tracking */ generateExecutionId() { return `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Initialize database in background without blocking constructor */ initializeDatabase() { // Run database initialization in background (async () => { try { const { initializeDatabaseSafe } = await Promise.resolve().then(() => __importStar(require('../../database/config'))); const dataSource = await initializeDatabaseSafe(); if (dataSource) { this.log('info', 'Database initialized successfully for MCP handler'); } else { this.log('warn', 'Database initialization failed - MCP handler will work without database'); } } catch (dbError) { this.log('warn', 'Database initialization error - continuing without database', { error: dbError instanceof Error ? dbError.message : dbError, }); } })(); // Mark as initialized immediately (don't wait for database) this.initialized = true; this.log('info', 'Modular MCP Handler initialization complete'); } /** * Initialize the handler and registry (now synchronous) */ initialize() { if (this.initialized) { return; } try { this.log('info', 'Initializing Modular MCP Handler'); // Registry auto-initializes in constructor, just verify const stats = this.registry.getStats(); this.log('info', `Registry initialized with ${stats.totalTools} tools in ${stats.totalCategories} categories`); this.initialized = true; this.log('info', 'Modular MCP Handler initialization complete'); } catch (error) { this.log('error', 'Failed to initialize handler', { error: error instanceof Error ? error.message : error, }); throw new Error(`Handler initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get handler capabilities */ getCapabilities() { return { tools: { listChanged: true, supportsProgress: false, }, resources: { subscribe: false, listChanged: false, }, prompts: { listChanged: false, }, logging: this.config.enableLogging ? { level: this.config.logLevel || 'info', } : undefined, }; } /** * Override handleRequest to support context parameter for auth integration */ async handleRequest(request, context) { return this.processRequest(request, context); } /** * Process MCP requests with full integration (auth, validation, metrics, audit) */ async processRequest(request, context) { var _a, _b; const startTime = Date.now(); const sessionId = this.metricsTracker.generateSessionId(); this.log('debug', `Processing request: ${request.method}`, { id: request.id, sessionId }); try { // Step 1: Request validation using modular validator const validationError = request_validator_1.RequestValidator.validate(request); if (validationError) { const response = response_builder_1.ResponseBuilder.createError(request.id, validationError); this.metricsTracker.recordError(); this.metricsTracker.recordAnalytics(sessionId, request, Date.now() - startTime, false, validationError.code); return response; } // Step 2: Authentication check if enabled if (this.enableAuthentication && this.authService) { const authResult = await this.authenticateRequest(request, context); if (!authResult.success) { const response = response_builder_1.ResponseBuilder.createError(request.id, { code: mcp_types_1.MCPErrorCode.INVALID_REQUEST, // Using closest available error code message: authResult.error || 'Authentication required', }); this.metricsTracker.recordError(); if (this.auditLogger) { // Note: AuditLogger methods need to be implemented - using basic logging for now console.warn(`🔐 Authentication failure: ${authResult.error} for ${request.method}`); } return response; } // Add user context from auth result if (context) { context.user = authResult.user; context.permissions = authResult.permissions; } } // Step 3: Process the actual request let response; switch (request.method) { case 'initialize': response = this.handleInitialize(request); break; case 'tools/list': response = this.handleToolsList(request); break; case 'tools/call': response = await this.handleToolsCall(request, context); break; case 'resources/list': response = this.handleResourcesList(request); break; case 'resources/read': response = await this.handleResourcesRead(request); break; case 'prompts/list': response = this.handlePromptsList(request); break; case 'ping': response = this.handlePing(request); break; default: response = response_builder_1.ResponseBuilder.createMethodNotFound(request.id, request.method); break; } // Step 4: Record successful metrics const executionTime = Date.now() - startTime; this.metricsTracker.recordRequest(request.method, executionTime); this.metricsTracker.recordAnalytics(sessionId, request, executionTime, true); // Step 5: Audit logging if enabled if (this.auditLogger) { // Note: AuditLogger methods need to be implemented - using basic logging for now console.log(`✅ Request successful: ${request.method} in ${executionTime}ms by ${((_a = context === null || context === void 0 ? void 0 : context.user) === null || _a === void 0 ? void 0 : _a.username) || 'anonymous'}`); } return response; } catch (error) { // Error handling with full integration const executionTime = Date.now() - startTime; this.metricsTracker.recordError(); this.metricsTracker.recordAnalytics(sessionId, request, executionTime, false); if (this.auditLogger) { // Note: AuditLogger methods need to be implemented - using basic logging for now console.error(`❌ Request error: ${request.method} failed for ${((_b = context === null || context === void 0 ? void 0 : context.user) === null || _b === void 0 ? void 0 : _b.username) || 'anonymous'}: ${error}`); } this.log('error', `Request processing failed: ${error}`, { id: request.id, method: request.method, sessionId, executionTime, }); return response_builder_1.ResponseBuilder.createInternalError(request.id, 'Request processing failed'); } } /** * Authenticate request using integrated auth service */ async authenticateRequest(request, context) { try { if (!(context === null || context === void 0 ? void 0 : context.authToken)) { return { success: false, error: 'No authentication token provided' }; } const authResult = await this.authService.validateToken(context.authToken); if (!authResult.isAuthenticated || !authResult.user) { return { success: false, error: 'Invalid or expired token' }; } return { success: true, user: authResult.user, permissions: authResult.permissions || [], }; } catch (error) { return { success: false, error: `Authentication failed: ${error}` }; } } /** * Check if user has permission to execute a tool */ checkToolPermission(toolName, permissions) { // Basic permission mapping - can be enhanced based on requirements const toolPermissionMapping = { create_quiz: ['quiz:create', 'admin'], delete_quiz: ['quiz:delete', 'admin'], list_quizzes: ['quiz:read', 'quiz:create', 'admin'], get_quiz: ['quiz:read', 'quiz:create', 'admin'], submit_quiz_result: ['quiz:submit', 'quiz:read', 'admin'], get_quiz_analytics: ['analytics:read', 'admin'], generate_quiz_urls: ['quiz:create', 'admin'], start_web_server: ['admin'], stop_web_server: ['admin'], server_status: ['quiz:read', 'admin'], }; const requiredPermissions = toolPermissionMapping[toolName] || ['admin']; // Check if user has any of the required permissions return requiredPermissions.some(required => permissions.includes(required) || permissions.includes('*')); } /** * Handle initialize request */ handleInitialize(request) { const result = { protocolVersion: '2024-11-05', capabilities: this.getCapabilities(), serverInfo: this.config.serverInfo, }; this.log('info', 'Client initialized', { protocolVersion: result.protocolVersion, toolCount: this.registry.getStats().totalTools, }); return this.createResponse(request.id, result); } /** * Handle tools/list request */ handleToolsList(request) { try { const allTools = this.registry.getAllTools(); this.log('debug', `Returning ${allTools.length} tools`); return this.createResponse(request.id, { tools: allTools, }); } catch (error) { this.log('error', 'Failed to list tools', { error: error instanceof Error ? error.message : error, }); throw new Error(`Failed to list tools: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Handle tools/call request with registry execution and auth context */ async handleToolsCall(request, context) { var _a; const params = request.params; if (!params || !params.name) { const error = { code: mcp_types_1.MCPErrorCode.INVALID_PARAMS, message: 'Tool name is required', }; return this.createErrorResponse(request.id, error); } const toolName = params.name; const args = params.arguments || {}; try { // Validate tool exists if (!this.registry.hasTool(toolName)) { const error = { code: mcp_types_1.MCPErrorCode.METHOD_NOT_FOUND, message: `Tool '${toolName}' not found`, }; return this.createErrorResponse(request.id, error); } // Check permissions if auth is enabled if (this.enableAuthentication && (context === null || context === void 0 ? void 0 : context.user)) { const hasPermission = this.checkToolPermission(toolName, context.permissions || []); if (!hasPermission) { const error = { code: mcp_types_1.MCPErrorCode.INVALID_PARAMS, // Using closest available error code message: `Insufficient permissions for tool '${toolName}'`, }; if (this.auditLogger) { // Note: AuditLogger methods need to be implemented - using basic logging for now console.warn(`🚫 Permission denied: ${((_a = context.user) === null || _a === void 0 ? void 0 : _a.username) || 'user'} lacks permission for tool '${toolName}'`); } return this.createErrorResponse(request.id, error); } } // Update metrics this.updateToolCallMetrics(toolName); // Record security event if (this.securityMonitor) { this.securityMonitor.recordEvent({ type: security_monitor_1.SecurityEventType.TOOL_EXECUTION_STARTED, severity: 'low', source: 'ModularMCPHandler', details: { toolName, executionId: this.generateExecutionId(), metadata: { args: Object.keys(args) }, }, }); } // Execute tool - use secure executor if enabled, otherwise direct registry this.log('info', `Executing tool: ${toolName}`, { args: Object.keys(args), secure: !!this.secureExecutor, }); let result; if (this.secureExecutor && this.securityConfig.sandbox.enabled) { // Secure sandboxed execution const secureResult = await this.executeToolSecurely(toolName, args); result = secureResult.result; // Log security metadata if (this.securityMonitor) { this.securityMonitor.recordEvent({ type: secureResult.success ? security_monitor_1.SecurityEventType.TOOL_EXECUTION_COMPLETED : security_monitor_1.SecurityEventType.TOOL_EXECUTION_FAILED, severity: secureResult.success ? 'low' : 'medium', source: 'SecureToolExecutor', details: { toolName, executionId: secureResult.metadata.executionId, resourceUsage: secureResult.metadata.resourceUsage, errorMessage: secureResult.error, metadata: { executionTime: secureResult.metadata.executionTime, securityEvents: secureResult.metadata.securityEvents, }, }, }); } if (!secureResult.success) { throw new Error(secureResult.error || 'Secure tool execution failed'); } } else { // Direct registry execution (fallback) result = await this.registry.executeTool(toolName, args); if (this.securityMonitor) { this.securityMonitor.recordEvent({ type: security_monitor_1.SecurityEventType.TOOL_EXECUTION_COMPLETED, severity: 'low', source: 'AdvancedToolRegistry', details: { toolName, metadata: { mode: 'direct', sandbox: false }, }, }); } } this.log('debug', `Tool execution completed: ${toolName}`); return this.createResponse(request.id, result); } catch (error) { this.log('error', `Tool execution failed: ${toolName}`, { error: error instanceof Error ? error.message : error, }); const mcpError = { code: mcp_types_1.MCPErrorCode.TOOL_EXECUTION_ERROR, message: `Tool execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`, data: this.config.enableLogging ? { toolName, args } : undefined, }; return this.createErrorResponse(request.id, mcpError); } } /** * Handle resources/list request */ handleResourcesList(request) { this.log('debug', 'Resources list requested'); const resources = [ { uri: 'quiz://quizzes/list', name: 'All Quizzes', description: 'Complete list of all available quizzes', mimeType: 'application/json', }, { uri: 'quiz://analytics/global', name: 'Global Analytics', description: 'Platform-wide quiz analytics and statistics', mimeType: 'application/json', }, ]; return this.createResponse(request.id, { resources, }); } /** * Handle resources/read request */ async handleResourcesRead(request) { var _a; const params = request.params; if (!params || !params.uri) { const error = { code: mcp_types_1.MCPErrorCode.INVALID_PARAMS, message: 'Resource URI is required', }; return this.createErrorResponse(request.id, error); } const uri = params.uri; this.log('debug', `Reading resource: ${uri}`); try { let contents = []; switch (uri) { case 'quiz://quizzes/list': // Get list of all quizzes through registry with fallback try { const listResult = await this.registry.executeTool('list_quizzes', { limit: 100 }); contents = [ { type: 'text', text: ((_a = listResult.content[0]) === null || _a === void 0 ? void 0 : _a.text) || '{"quizzes": []}', }, ]; } catch (error) { // Fallback when database is not available this.log('warn', 'Database not available for quiz list, using fallback', { error: error instanceof Error ? error.message : error, }); contents = [ { type: 'text', text: JSON.stringify({ quizzes: [], total: 0, message: 'Database not initialized - no quizzes available', lastUpdated: new Date().toISOString(), }, null, 2), }, ]; } break; case 'quiz://analytics/global': // Get global analytics contents = [ { type: 'text', text: JSON.stringify({ totalQuizzes: 0, totalAttempts: 0, averageScore: 0, lastUpdated: new Date().toISOString(), }, null, 2), }, ]; break; default: { const error = { code: mcp_types_1.MCPErrorCode.RESOURCE_NOT_FOUND, message: `Resource not found: ${uri}`, }; return this.createErrorResponse(request.id, error); } } this.updateResourceAccessMetrics(uri); this.log('debug', `Resource read completed: ${uri}`); return this.createResponse(request.id, { contents, }); } catch (error) { this.log('error', `Resource read failed: ${uri}`, { error: error instanceof Error ? error.message : error, }); const mcpError = { code: mcp_types_1.MCPErrorCode.INTERNAL_ERROR, message: `Resource read failed: ${error instanceof Error ? error.message : 'Unknown error'}`, data: this.config.enableLogging ? { uri } : undefined, }; return this.createErrorResponse(request.id, mcpError); } } /** * Handle prompts/list request */ handlePromptsList(request) { this.log('debug', 'Prompts list requested'); const prompts = [ { name: 'create-quiz-advanced', description: 'Create a new quiz with advanced features and AI assistance', arguments: [ { name: 'topic', description: 'The main topic or subject area for the quiz', required: true, }, { name: 'difficulty', description: 'Difficulty level (easy, medium, hard)', required: false, }, { name: 'questionCount', description: 'Number of questions to generate (1-20)', required: false, }, ], }, ]; return this.createResponse(request.id, { prompts, }); } /** * Handle ping request for health checks */ handlePing(request) { this.log('debug', 'Ping received'); return this.createResponse(request.id, { status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime(), memory: process.memoryUsage(), registry: this.registry.getStats(), }); } /** * Get enhanced handler statistics */ getEnhancedStats() { return { handler: { initialized: this.initialized, config: this.config, metrics: this.getMetrics(), }, registry: this.registry.getStats(), }; } /** * Search tools using registry search functionality */ searchTools(query) { return this.registry.searchTools(query); } /** * Get tools by category using registry */ getToolsByCategory(category) { return this.registry.getToolsByCategory(category); } /** * Get all categories with tool counts */ getAllCategories() { return this.registry.getAllCategories(); } } exports.ModularMCPHandler = ModularMCPHandler; /** * Export singleton instance for easy import */ exports.simplifiedModularHandler = new ModularMCPHandler(); /** * Export class for custom configurations */ exports.default = ModularMCPHandler;