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
JavaScript
"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;