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.

300 lines (299 loc) â€ĸ 10.1 kB
"use strict"; /** * @moduleName: Base MCP Handler - Abstract Foundation * @version: 2.0.0 * @since: 2025-07-25 * @lastUpdated: 2025-07-25 * @projectSummary: Abstract base class for MCP protocol handlers with common functionality * @techStack: TypeScript, JSON-RPC 2.0, MCP Protocol * @dependency: ../types/mcp-types * @interModuleDependency: Extended by all specific MCP handlers * @requirementsTraceability: * {@link Requirements.REQ_MCP_001} (JSON-RPC 2.0 MCP Protocol) * @briefDescription: Base abstract class providing common MCP protocol functionality and validation * @methods: handleRequest, validateRequest, createResponse, createErrorResponse * @contributors: GitHub Copilot * @examples: * - class MyHandler extends BaseMCPHandler { ... } * - const response = await handler.handleRequest(request); * @vulnerabilitiesAssessment: Input validation and error handling at base level */ Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseMCPHandler = void 0; const mcp_types_1 = require("../types/mcp-types"); class BaseMCPHandler { constructor(config) { this.analyticsHistory = []; this.config = config; this.metrics = { requestCount: 0, errorCount: 0, averageResponseTime: 0, toolCallCount: {}, resourceAccessCount: {}, }; } /** * Main request handler with metrics and error handling */ async handleRequest(request) { const startTime = Date.now(); const sessionId = this.generateSessionId(); try { // Validate request format const validationError = this.validateRequest(request); if (validationError) { this.recordError(request, validationError); return this.createErrorResponse(request.id, validationError); } // Update metrics this.metrics.requestCount++; // Log request if enabled if (this.config.enableLogging) { this.log('info', `Processing ${request.method}`, { id: request.id, sessionId, }); } // Delegate to specific handler implementation const response = await this.processRequest(request); // Record analytics const executionTime = Date.now() - startTime; this.recordAnalytics(sessionId, request, executionTime, true); this.updateAverageResponseTime(executionTime); return response; } catch (error) { const executionTime = Date.now() - startTime; this.metrics.errorCount++; // Create error response const mcpError = { code: mcp_types_1.MCPErrorCode.INTERNAL_ERROR, message: error instanceof Error ? error.message : 'Unknown error', data: this.config.enableLogging ? { stack: error instanceof Error ? error.stack : undefined } : undefined, }; this.recordError(request, mcpError); this.recordAnalytics(sessionId, request, executionTime, false, mcpError.code); return this.createErrorResponse(request.id, mcpError); } } /** * Validate JSON-RPC 2.0 request format */ validateRequest(request) { if (!request) { return { code: mcp_types_1.MCPErrorCode.INVALID_REQUEST, message: 'Request cannot be null or undefined', }; } if (request.jsonrpc !== '2.0') { return { code: mcp_types_1.MCPErrorCode.INVALID_REQUEST, message: 'Invalid JSON-RPC version - must be "2.0"', }; } if (!request.method || typeof request.method !== 'string') { return { code: mcp_types_1.MCPErrorCode.INVALID_REQUEST, message: 'Method must be a non-empty string', }; } if (request.id !== null && typeof request.id !== 'string' && typeof request.id !== 'number') { return { code: mcp_types_1.MCPErrorCode.INVALID_REQUEST, message: 'ID must be a string, number, or null', }; } return null; } /** * Create a successful response */ createResponse(id, result) { return { jsonrpc: '2.0', id, result, }; } /** * Create an error response */ createErrorResponse(id, error) { return { jsonrpc: '2.0', id, error, }; } /** * Generate a unique session ID for tracking */ generateSessionId() { return `mcp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Record analytics data */ recordAnalytics(sessionId, request, executionTime, success, errorCode) { const analytics = { sessionId, timestamp: new Date(), method: request.method, executionTime, success, errorCode, metadata: { hasParams: !!request.params, paramsSize: request.params ? JSON.stringify(request.params).length : 0, }, }; this.analyticsHistory.push(analytics); // Keep only last 1000 analytics records if (this.analyticsHistory.length > 1000) { this.analyticsHistory = this.analyticsHistory.slice(-1000); } } /** * Record error occurrence */ recordError(request, error) { if (this.config.enableLogging) { this.log('error', `Request failed: ${request.method}`, { id: request.id, error: error.message, code: error.code, }); } } /** * Update average response time metric */ updateAverageResponseTime(newTime) { const totalRequests = this.metrics.requestCount; const currentAverage = this.metrics.averageResponseTime; this.metrics.averageResponseTime = (currentAverage * (totalRequests - 1) + newTime) / totalRequests; } /** * Logging utility - follows project emoji convention */ log(level, message, data) { if (!this.config.enableLogging) return; const logLevel = this.config.logLevel || 'info'; const levels = { debug: 0, info: 1, warn: 2, error: 3 }; if (levels[level] >= levels[logLevel]) { const timestamp = new Date().toISOString(); const emoji = this.getLogEmoji(level); const logMessage = `${emoji} [${timestamp}] [MCP] ${message}`; // Use appropriate console method const consoleMethod = level === 'error' ? console.error : level === 'warn' ? console.warn : level === 'info' ? console.info : console.log; if (data) { // Sanitize sensitive data for security const sanitizedData = this.sanitizeLogData(data); consoleMethod(logMessage, sanitizedData); } else { consoleMethod(logMessage); } } } /** * Get emoji for log level following project convention */ getLogEmoji(level) { const emojis = { debug: '🔍', info: 'â„šī¸', warn: 'âš ī¸', error: '❌', }; return emojis[level] || 'â„šī¸'; } /** * Sanitize sensitive data from logs for security */ sanitizeLogData(data) { if (!data || typeof data !== 'object') return data; const sensitiveKeys = ['password', 'token', 'secret', 'key', 'auth', 'authorization', 'jwt']; const sanitized = { ...data }; for (const key of Object.keys(sanitized)) { if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) { sanitized[key] = '[REDACTED]'; } } return sanitized; } /** * Get current metrics */ getMetrics() { return { ...this.metrics }; } /** * Get analytics history */ getAnalytics(limit) { if (limit) { return this.analyticsHistory.slice(-limit); } return [...this.analyticsHistory]; } /** * Reset metrics and analytics */ resetMetrics() { this.metrics = { requestCount: 0, errorCount: 0, averageResponseTime: 0, toolCallCount: {}, resourceAccessCount: {}, }; this.analyticsHistory = []; } /** * Validate tool call parameters */ validateToolParams(toolName, params, requiredParams = []) { if (!params && requiredParams.length > 0) { return { code: mcp_types_1.MCPErrorCode.INVALID_PARAMS, message: `Tool ${toolName} requires parameters: ${requiredParams.join(', ')}`, }; } for (const required of requiredParams) { if (params[required] === undefined || params[required] === null) { return { code: mcp_types_1.MCPErrorCode.INVALID_PARAMS, message: `Missing required parameter: ${required}`, }; } } return null; } /** * Update tool call metrics */ updateToolCallMetrics(toolName) { this.metrics.toolCallCount[toolName] = (this.metrics.toolCallCount[toolName] || 0) + 1; } /** * Update resource access metrics */ updateResourceAccessMetrics(uri) { this.metrics.resourceAccessCount[uri] = (this.metrics.resourceAccessCount[uri] || 0) + 1; } } exports.BaseMCPHandler = BaseMCPHandler;