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