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