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.

341 lines (340 loc) 13.2 kB
"use strict"; /** * @moduleName: Quiz Analytics and Results Tools * @version: 2.0.0 * @since: 2025-07-25 * @lastUpdated: 2025-07-25 * @projectSummary: Enhanced MCP Quiz Server - Modular Architecture * @techStack: TypeScript, JSON-RPC 2.0, MCP Protocol, Analytics Engine * @dependency: @types/node * @interModuleDependency: ../../types/mcp-types, ../../security/security-utils * @requirementsTraceability: * {@link Requirements.REQ_DATA_002} (Append-Only Results Storage) * {@link Requirements.REQ_MCP_004} (Quiz Management Tools) * {@link Requirements.REQ_ROUTES_002} (Enhanced MCP URL Generation) * @briefDescription: MCP tools for quiz result submission, analytics viewing, and URL generation * @methods: submitQuizResult, getQuizAnalytics, generateQuizUrls * @contributors: GitHub Copilot, Claude Code Agent * @examples: * - Submit quiz results with session tracking * - Get detailed quiz analytics with time-based filtering * @vulnerabilitiesAssessment: Quiz ID validation prevents unauthorized access, result validation prevents data injection */ Object.defineProperty(exports, "__esModule", { value: true }); exports.analyticsHandlers = exports.analyticsTools = exports.handleGenerateQuizUrls = exports.handleGetQuizAnalytics = exports.handleSubmitQuizResult = exports.generateQuizUrlsTool = exports.getQuizAnalyticsTool = exports.submitQuizResultTool = void 0; const security_utils_1 = require("../../security/security-utils"); /** * Simple logging function for operations */ const logOperation = (emoji, message) => { console.log(`${emoji} ${message}`); }; /** * Submit quiz result tool definition */ exports.submitQuizResultTool = { name: 'submit_quiz_result', description: 'Submit quiz results with detailed tracking', inputSchema: { type: 'object', properties: { quizId: { type: 'string', description: 'The quiz ID', }, answers: { type: 'array', description: 'Array of user answers', items: { type: 'string', }, }, sessionId: { type: 'string', description: 'Session tracking ID', }, timeSpent: { type: 'number', description: 'Time spent on quiz in seconds', }, userId: { type: 'string', description: 'Optional user identifier', }, }, required: ['quizId', 'answers'], }, }; /** * Get quiz analytics tool definition */ exports.getQuizAnalyticsTool = { name: 'get_quiz_analytics', description: 'Get detailed analytics for a specific quiz', inputSchema: { type: 'object', properties: { quizId: { type: 'string', description: 'The quiz ID to analyze', }, timeRange: { type: 'string', description: 'Time range for analytics', enum: ['24h', '7d', '30d', 'all'], default: '7d', }, }, required: ['quizId'], }, }; /** * Generate quiz URLs tool definition */ exports.generateQuizUrlsTool = { name: 'generate_quiz_urls', description: 'Generate secure, shareable URLs for a quiz', inputSchema: { type: 'object', properties: { quizId: { type: 'string', description: 'The quiz ID', }, urlType: { type: 'string', description: 'Type of URL to generate', enum: ['public', 'token', 'embed', 'short', 'all'], default: 'public', }, requireToken: { type: 'boolean', description: 'Require access token for quiz access', default: false, }, expiresIn: { type: 'string', description: 'Token expiration time (e.g., "1h", "1d", "1w")', default: '24h', }, embedSupport: { type: 'boolean', description: 'Generate embed-ready URLs', default: true, }, }, required: ['quizId'], }, }; /** * Handler for submitting quiz results */ const handleSubmitQuizResult = async (args) => { logOperation('📊', 'Submitting quiz result...'); try { const { quizId, answers, sessionId, timeSpent, userId } = args || {}; // Validate required fields if (!quizId || !answers) { throw new Error('Missing required fields: quizId and answers are required'); } if (!Array.isArray(answers)) { throw new Error('Answers must be an array'); } // Validate quiz ID format (UUID) const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!uuidRegex.test(quizId)) { throw new Error('Invalid quiz ID format'); } // Mock result submission const result = { submissionId: security_utils_1.SecurityUtils.generateSecureId(), quizId, answers, sessionId: sessionId || security_utils_1.SecurityUtils.generateSecureId(), timeSpent: timeSpent || 0, userId: userId || 'anonymous', timestamp: new Date().toISOString(), status: 'submitted', }; logOperation('✅', `Quiz result submitted for quiz ${quizId}`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: '🎉 Quiz result submitted successfully!', data: { submissionId: result.submissionId, quizId, answersCount: answers.length, timeSpent: timeSpent || 0, timestamp: result.timestamp, status: 'submitted', }, }, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error submitting quiz result'; logOperation('❌', `Failed to submit quiz result: ${errorMessage}`); throw new Error(`Submit quiz result failed: ${errorMessage}`); } }; exports.handleSubmitQuizResult = handleSubmitQuizResult; /** * Handler for getting quiz analytics */ const handleGetQuizAnalytics = async (args) => { logOperation('📈', 'Retrieving quiz analytics...'); try { const { quizId, timeRange = '7d' } = args || {}; if (!quizId) { throw new Error('Quiz ID is required'); } // Validate quiz ID format const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!uuidRegex.test(quizId)) { throw new Error('Invalid quiz ID format'); } // Mock analytics data const analytics = { quizId, timeRange, totalAttempts: 45, uniqueUsers: 38, averageScore: 78.5, averageTimeSpent: 145, // seconds completionRate: 92.3, // percentage topQuestions: [ { questionIndex: 0, correctRate: 95.5, avgTime: 12 }, { questionIndex: 1, correctRate: 89.2, avgTime: 18 }, { questionIndex: 2, correctRate: 67.8, avgTime: 25 }, ], scoreDistribution: { '0-20%': 2, '21-40%': 3, '41-60%': 8, '61-80%': 15, '81-100%': 17, }, recentActivity: [ { date: '2025-07-25', attempts: 12 }, { date: '2025-07-24', attempts: 8 }, { date: '2025-07-23', attempts: 15 }, ], }; logOperation('✅', `Analytics retrieved for quiz ${quizId} (${timeRange} range)`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: '📊 Quiz analytics retrieved successfully!', data: { quizId, timeRange, analytics, }, }, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error retrieving analytics'; logOperation('❌', `Failed to get quiz analytics: ${errorMessage}`); throw new Error(`Get quiz analytics failed: ${errorMessage}`); } }; exports.handleGetQuizAnalytics = handleGetQuizAnalytics; /** * Handler for generating quiz URLs */ const handleGenerateQuizUrls = async (args) => { logOperation('🔗', 'Generating quiz URLs...'); try { const { quizId, urlType = 'public', requireToken = false, expiresIn = '24h', embedSupport = true, } = args || {}; if (!quizId) { throw new Error('Quiz ID is required'); } // Validate quiz ID format const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!uuidRegex.test(quizId)) { throw new Error('Invalid quiz ID format'); } const baseUrl = 'http://localhost:3000'; const urls = {}; // 🎯 NEW: User-facing URLs that serve HTML interfaces if (urlType === 'public' || urlType === 'all') { urls.public = `${baseUrl}/quiz/preview/${quizId}`; // Shows modal interface urls.directStart = `${baseUrl}/quiz/start/${quizId}?autoStart=true`; // Bypasses modal urls.apiEndpoint = `${baseUrl}/quiz/${quizId}`; // Raw JSON (for API consumers) } if ((urlType === 'token' || urlType === 'all') && requireToken) { const token = security_utils_1.SecurityUtils.generateSecureId(); urls.secureStart = `${baseUrl}/quiz/start/${quizId}?token=${token}&expires=${expiresIn}&autoStart=true`; urls.securePreview = `${baseUrl}/quiz/preview/${quizId}?token=${token}&expires=${expiresIn}`; } if ((urlType === 'embed' || urlType === 'all') && embedSupport) { urls.embed = `${baseUrl}/quiz/embed/${quizId}?theme=minimal&controls=simple`; urls.embedFull = `${baseUrl}/quiz/embed/${quizId}?theme=full&controls=advanced`; } if (urlType === 'short' || urlType === 'all') { const shortId = security_utils_1.SecurityUtils.generateSecureId().substring(0, 8); urls.short = `${baseUrl}/q/${shortId}`; // Redirects to preview } // 🆕 User Type Specific URLs if (urlType === 'all') { urls.guestMode = `${baseUrl}/quiz/guest/${quizId}`; // Simplified interface urls.studentMode = `${baseUrl}/quiz/start/${quizId}?type=student&timer=15`; // With timer urls.instructorPreview = `${baseUrl}/quiz/preview/${quizId}?type=instructor&analytics=true`; // With analytics } logOperation('✅', `URLs generated for quiz ${quizId} (type: ${urlType})`); const urlList = Object.entries(urls) .map(([type, url]) => `• ${type.toUpperCase()}: ${url}`) .join('\n'); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: '🔗 Quiz URLs generated successfully!', data: { quizId, urlType, urls, configuration: { requireToken, expiresIn, embedSupport, }, }, }, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error generating URLs'; logOperation('❌', `Failed to generate quiz URLs: ${errorMessage}`); throw new Error(`Generate quiz URLs failed: ${errorMessage}`); } }; exports.handleGenerateQuizUrls = handleGenerateQuizUrls; /** * Export all quiz analytics and results tools */ exports.analyticsTools = [exports.submitQuizResultTool, exports.getQuizAnalyticsTool, exports.generateQuizUrlsTool]; /** * Export all quiz analytics and results handlers */ exports.analyticsHandlers = { submit_quiz_result: exports.handleSubmitQuizResult, get_quiz_analytics: exports.handleGetQuizAnalytics, generate_quiz_urls: exports.handleGenerateQuizUrls, };