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
JavaScript
;
/**
* @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,
};