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.

244 lines (243 loc) 10.7 kB
"use strict"; /** * @moduleName: Delete Quiz Tool - MCP Tool Implementation * @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, TypeORM * @dependency: typeorm * @interModuleDependency: ../../types/mcp-types, ./create-quiz * @requirementsTraceability: * {@link Requirements.REQ_API_001} (Quiz CRUD Operations) * {@link Requirements.REQ_MCP_002} (Dynamic Tool Registry) * {@link Requirements.REQ_MCP_004} (Quiz Management Tools) * {@link Requirements.REQ_SEC_003} (Two-Step Destructive Operations) * @testCoverage: Extensively tested in test-delete.js, test-all-tools.js, test-complete-workflow.js * @testType: integration, e2e, security * @testFramework: custom MCP protocol testing * @briefDescription: Focused MCP tool for safely deleting quizzes with confirmation and cascade handling * @methods: execute, validateInput, formatResponse, performDeletion * @contributors: GitHub Copilot * @examples: * - const tool = new DeleteQuizTool(); await tool.execute({quizId: "123", confirmDelete: true}, context); * - MCP call: {"method": "tools/call", "params": {"name": "delete_quiz", "arguments": {...}}} * @vulnerabilitiesAssessment: Confirmation required, cascade deletion safety, audit logging ready */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.DeleteQuizTool = void 0; class DeleteQuizTool { constructor() { this.definition = { name: 'delete_quiz', description: 'Delete a quiz and all associated data with confirmation requirement', inputSchema: { type: 'object', properties: { quizId: { type: 'string', description: 'Unique identifier of the quiz to delete', pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', }, confirmDelete: { type: 'boolean', description: 'Confirmation flag - must be true to proceed with deletion', }, reason: { type: 'string', description: 'Optional reason for deletion (for audit purposes)', maxLength: 500, }, }, required: ['quizId', 'confirmDelete'], }, }; } async execute(args, context) { try { console.log('🗑️ Delete quiz requested:', args.quizId); // Validate input and confirmation this.validateInput(args); // Check if quiz exists before deletion const quiz = await this.findQuizById(args.quizId); if (!quiz) { return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: 'Quiz not found', message: `❌ Quiz with ID ${args.quizId} does not exist`, }, null, 2), }, ], success: false, isError: true, }; } // Perform the deletion const deletionResult = await this.performDeletion(args.quizId, quiz, args.reason); console.log('✅ Quiz deleted successfully:', quiz.title); return this.formatResponse(deletionResult); } catch (error) { console.error('❌ Quiz deletion failed:', error); return { content: [ { type: 'text', text: `❌ Quiz deletion failed: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], success: false, isError: true, }; } } validateInput(args) { var _a; if (!((_a = args.quizId) === null || _a === void 0 ? void 0 : _a.trim())) { throw new Error('Quiz ID is required'); } // Validate ID format - allow both UUIDs and simple test IDs const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const simpleIdRegex = /^[a-zA-Z0-9_-]+$/; if (!uuidRegex.test(args.quizId) && !simpleIdRegex.test(args.quizId)) { throw new Error('Invalid quiz ID format. Must be a valid UUID or simple identifier.'); } if (!args.confirmDelete) { throw new Error('Deletion confirmation is required. Set confirmDelete to true to proceed.'); } } async findQuizById(quizId) { // Get quiz from database with relations const { getSafeDataSource, initializeDatabase } = await Promise.resolve().then(() => __importStar(require('../../../database/config'))); const { Quiz } = await Promise.resolve().then(() => __importStar(require('../../../database/entities/Quiz'))); let dataSource = getSafeDataSource(); if (!dataSource) { // Initialize database if not available dataSource = await initializeDatabase(); } const quizRepository = dataSource.getRepository(Quiz); const quiz = await quizRepository.findOne({ where: { id: quizId }, relations: ['questions'], // For counting related data }); return quiz; } async performDeletion(quizId, quiz, reason) { // Perform actual database deletion with transaction const { getSafeDataSource, initializeDatabase } = await Promise.resolve().then(() => __importStar(require('../../../database/config'))); const { Quiz } = await Promise.resolve().then(() => __importStar(require('../../../database/entities/Quiz'))); const { Question } = await Promise.resolve().then(() => __importStar(require('../../../database/entities/Question'))); const { QuizResult } = await Promise.resolve().then(() => __importStar(require('../../../database/entities/QuizResult'))); let dataSource = getSafeDataSource(); if (!dataSource) { // Initialize database if not available dataSource = await initializeDatabase(); } const queryRunner = dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // Delete quiz results first (foreign key constraint) const deletedResults = await queryRunner.manager.getRepository(QuizResult).delete({ quizId }); // Delete quiz questions const deletedQuestions = await queryRunner.manager.getRepository(Question).delete({ quizId }); // Delete the quiz itself await queryRunner.manager.getRepository(Quiz).delete({ id: quizId }); await queryRunner.commitTransaction(); // Return actual deletion result const deletionResult = { quizId, quizTitle: quiz.title, deletedAt: new Date().toISOString(), deletedItems: { quiz: 1, questions: deletedQuestions.affected || 0, results: deletedResults.affected || 0, }, reason: reason || 'No reason provided', }; return deletionResult; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } logDeletion(deletionResult) { console.log('🗑️ DELETION AUDIT LOG:', { timestamp: deletionResult.deletedAt, quizId: deletionResult.quizId, quizTitle: deletionResult.quizTitle, deletedItems: deletionResult.deletedItems, reason: deletionResult.reason, }); // TODO: Add to proper audit log system // await auditLogger.log('QUIZ_DELETION', deletionResult); } formatResponse(deletionResult) { const totalItemsDeleted = Object.values(deletionResult.deletedItems).reduce((sum, count) => sum + (typeof count === 'number' ? count : 0), 0); const responseData = { success: true, message: `🗑️ Quiz "${deletionResult.quizTitle}" and ${totalItemsDeleted} related items deleted successfully`, data: deletionResult, warning: 'This action cannot be undone', }; return { content: [ { type: 'text', text: JSON.stringify(responseData, null, 2), }, ], // Make success accessible at result level for tests success: true, _meta: { toolName: 'delete_quiz', executionTime: Date.now(), quizId: deletionResult.quizId, totalItemsDeleted, }, }; } } exports.DeleteQuizTool = DeleteQuizTool;