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.

364 lines (363 loc) • 13.6 kB
"use strict"; /** * @fileoverview Quiz Command Handler - Application Layer * @version 1.0.0 * @since 2025-07-30 * @lastUpdated 2025-07-30 * @module QuizCommandHandler * @description Command handler for quiz operations integrating legacy MCP tools with Clean Architecture. * Bridges the gap between transport layer and existing quiz functionality. * @contributors Claude Code Agent * @dependencies BaseCommandHandler, QuizService (legacy), MCP tools * @requirements SECURITY_001 (Transport Integration), REQ-QUIZ-001 * @testCoverage Unit tests for all quiz operations with security validation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.QuizCommandHandler = exports.QuizCommandType = void 0; const ICommandBus_1 = require("../commands/ICommandBus"); /** * Quiz Command Types */ var QuizCommandType; (function (QuizCommandType) { QuizCommandType["CREATE_QUIZ"] = "CreateQuiz"; QuizCommandType["GET_QUIZ"] = "GetQuiz"; QuizCommandType["UPDATE_QUIZ"] = "UpdateQuiz"; QuizCommandType["DELETE_QUIZ"] = "DeleteQuiz"; QuizCommandType["LIST_QUIZZES"] = "ListQuizzes"; QuizCommandType["START_QUIZ_SESSION"] = "StartQuizSession"; QuizCommandType["SUBMIT_ANSWER"] = "SubmitAnswer"; QuizCommandType["END_QUIZ_SESSION"] = "EndQuizSession"; })(QuizCommandType || (exports.QuizCommandType = QuizCommandType = {})); /** * Quiz Command Handler * * @description Handles all quiz-related commands with security validation and MCP integration. * Bridges Clean Architecture with existing quiz functionality. * * @example * ```typescript * const handler = new QuizCommandHandler(quizService); * const result = await handler.handle(createQuizCommand, securityContext); * ``` * * @since 2025-07-30 * @author Claude Code Agent * @requirements SECURITY_001, REQ-QUIZ-001 (Transport Integration, Quiz Operations) */ class QuizCommandHandler extends ICommandBus_1.BaseCommandHandler { constructor(legacyQuizService, sessionManager) { super(); this.legacyQuizService = legacyQuizService; this.sessionManager = sessionManager; } /** * Handle quiz commands */ async handle(command, context) { return this.executeWithLogging(command, context, async () => { switch (command.commandType) { case QuizCommandType.CREATE_QUIZ: return this.handleCreateQuiz(command.data, context); case QuizCommandType.GET_QUIZ: return this.handleGetQuiz(command.data, context); case QuizCommandType.UPDATE_QUIZ: return this.handleUpdateQuiz(command.data, context); case QuizCommandType.DELETE_QUIZ: return this.handleDeleteQuiz(command.data, context); case QuizCommandType.LIST_QUIZZES: return this.handleListQuizzes(command.data, context); case QuizCommandType.START_QUIZ_SESSION: return this.handleStartQuizSession(command.data, context); case QuizCommandType.SUBMIT_ANSWER: return this.handleSubmitAnswer(command.data, context); case QuizCommandType.END_QUIZ_SESSION: return this.handleEndQuizSession(command.data, context); default: throw new Error(`Unsupported command type: ${command.commandType}`); } }); } /** * Get supported command type */ getCommandType() { return 'Quiz'; // Base type - specific commands are handled via switch } /** * Check if can handle command */ canHandle(command) { return QuizCommandHandler.SUPPORTED_COMMANDS.includes(command.commandType); } /** * Validate quiz command data */ validateCommandData(command) { const errors = []; const warnings = []; switch (command.commandType) { case QuizCommandType.CREATE_QUIZ: this.validateCreateQuizData(command.data, errors, warnings); break; case QuizCommandType.GET_QUIZ: this.validateGetQuizData(command.data, errors, warnings); break; case QuizCommandType.UPDATE_QUIZ: this.validateUpdateQuizData(command.data, errors, warnings); break; case QuizCommandType.DELETE_QUIZ: this.validateDeleteQuizData(command.data, errors, warnings); break; case QuizCommandType.START_QUIZ_SESSION: this.validateStartQuizSessionData(command.data, errors, warnings); break; case QuizCommandType.SUBMIT_ANSWER: this.validateSubmitAnswerData(command.data, errors, warnings); break; } return { errors, warnings }; } /** * Handle create quiz command */ async handleCreateQuiz(data, context) { var _a; // Check permissions if (!this.hasPermission(context, 'quiz:create')) { throw new Error('Insufficient permissions to create quiz'); } // Create quiz using legacy service const quiz = await this.legacyQuizService.createQuiz({ title: data.title, description: data.description || '', questions: data.questions, createdBy: context.user.id, settings: data.settings, }); return { quizId: quiz.id, title: quiz.title, description: quiz.description, createdAt: quiz.createdAt, questionCount: ((_a = quiz.questions) === null || _a === void 0 ? void 0 : _a.length) || 0, }; } /** * Handle get quiz command */ async handleGetQuiz(data, context) { // Check permissions (users can read quizzes they have access to) if (!this.hasPermission(context, 'quiz:read')) { throw new Error('Insufficient permissions to read quiz'); } const quiz = await this.legacyQuizService.getQuiz(data.quizId); if (!quiz) { throw new Error(`Quiz not found: ${data.quizId}`); } // Filter sensitive data based on user permissions return this.filterQuizData(quiz, context); } /** * Handle update quiz command */ async handleUpdateQuiz(data, context) { var _a; // Check permissions if (!this.hasPermission(context, 'quiz:update')) { throw new Error('Insufficient permissions to update quiz'); } const updatedQuiz = await this.legacyQuizService.updateQuiz(data.quizId, { title: data.title, description: data.description, questions: data.questions, settings: data.settings, updatedBy: context.user.id, updatedAt: new Date(), }); return { quizId: updatedQuiz.id, title: updatedQuiz.title, description: updatedQuiz.description, updatedAt: updatedQuiz.updatedAt, questionCount: ((_a = updatedQuiz.questions) === null || _a === void 0 ? void 0 : _a.length) || 0, }; } /** * Handle delete quiz command */ async handleDeleteQuiz(data, context) { // Check permissions if (!this.hasPermission(context, 'quiz:delete')) { throw new Error('Insufficient permissions to delete quiz'); } await this.legacyQuizService.deleteQuiz(data.quizId); return { quizId: data.quizId, deleted: true, deletedAt: new Date(), deletedBy: context.user.id, }; } /** * Handle list quizzes command */ async handleListQuizzes(data, context) { // Check permissions if (!this.hasPermission(context, 'quiz:read')) { throw new Error('Insufficient permissions to list quizzes'); } const quizzes = await this.legacyQuizService.listQuizzes({ limit: data.limit || 20, offset: data.offset || 0, searchTerm: data.searchTerm, userId: context.user.id, // Filter by user's accessible quizzes }); return { quizzes: quizzes.map(quiz => this.filterQuizData(quiz, context)), total: quizzes.length, limit: data.limit || 20, offset: data.offset || 0, }; } /** * Handle start quiz session command */ async handleStartQuizSession(data, context) { // Check permissions if (!this.hasPermission(context, 'quiz:take')) { throw new Error('Insufficient permissions to start quiz session'); } const session = await this.sessionManager.startSession({ quizId: data.quizId, userId: context.user.id, configuration: data.sessionConfig, startedAt: new Date(), }); return { sessionId: session.id, quizId: data.quizId, startedAt: session.startedAt, configuration: session.configuration, }; } /** * Handle submit answer command */ async handleSubmitAnswer(data, context) { // Validate session ownership const session = await this.sessionManager.getSession(data.sessionId); if (!session || session.userId !== context.user.id) { throw new Error('Invalid or unauthorized session'); } const result = await this.sessionManager.submitAnswer({ sessionId: data.sessionId, questionId: data.questionId, answer: data.answer, submittedAt: new Date(), }); return { questionId: data.questionId, correct: result.correct, points: result.points, feedback: result.feedback, }; } /** * Handle end quiz session command */ async handleEndQuizSession(data, context) { // Validate session ownership const session = await this.sessionManager.getSession(data.sessionId); if (!session || session.userId !== context.user.id) { throw new Error('Invalid or unauthorized session'); } const finalResults = await this.sessionManager.endSession(data.sessionId); return { sessionId: data.sessionId, completedAt: new Date(), totalQuestions: finalResults.totalQuestions, correctAnswers: finalResults.correctAnswers, score: finalResults.score, timeSpent: finalResults.timeSpent, }; } /** * Check if user has permission */ hasPermission(context, permission) { return context.permissions.includes(permission) || context.permissions.includes('*'); } /** * Filter quiz data based on user permissions */ filterQuizData(quiz, context) { // Hide correct answers unless user has admin permissions if (!this.hasPermission(context, 'quiz:admin')) { if (quiz.questions) { quiz.questions = quiz.questions.map((q) => ({ ...q, correctAnswer: undefined, })); } } return quiz; } /** * Validation methods for each command type */ validateCreateQuizData(data, errors, warnings) { if (!data.title || data.title.trim().length === 0) { errors.push('Quiz title is required'); } if (!data.questions || data.questions.length === 0) { errors.push('At least one question is required'); } if (data.questions) { data.questions.forEach((q, index) => { if (!q.text || q.text.trim().length === 0) { errors.push(`Question ${index + 1} text is required`); } if (!q.type) { errors.push(`Question ${index + 1} type is required`); } }); } } validateGetQuizData(data, errors, warnings) { if (!data.quizId || data.quizId.trim().length === 0) { errors.push('Quiz ID is required'); } } validateUpdateQuizData(data, errors, warnings) { if (!data.quizId || data.quizId.trim().length === 0) { errors.push('Quiz ID is required'); } if (data.title !== undefined && data.title.trim().length === 0) { errors.push('Quiz title cannot be empty'); } } validateDeleteQuizData(data, errors, warnings) { if (!data.quizId || data.quizId.trim().length === 0) { errors.push('Quiz ID is required'); } } validateStartQuizSessionData(data, errors, warnings) { if (!data.quizId || data.quizId.trim().length === 0) { errors.push('Quiz ID is required'); } } validateSubmitAnswerData(data, errors, warnings) { if (!data.sessionId || data.sessionId.trim().length === 0) { errors.push('Session ID is required'); } if (!data.questionId || data.questionId.trim().length === 0) { errors.push('Question ID is required'); } if (data.answer === undefined || data.answer === null) { errors.push('Answer is required'); } } } exports.QuizCommandHandler = QuizCommandHandler; QuizCommandHandler.SUPPORTED_COMMANDS = Object.values(QuizCommandType);