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