UNPKG

mcp-chain-of-draft-server

Version:

A Model Context Protocol server which provides Chain of Draft style thinking

239 lines (238 loc) 10.7 kB
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { TOOL_NAME, TOOL_SCHEMA, TOOL_DESCRIPTION } from "./codeReviewLensParams.js"; import { SessionManagerFactory } from "../utils/sessionManagerFactory.js"; const reviewHistory = {}; const activeCritiques = {}; export const codeReviewLensTool = (server) => { const processReviewRequest = async (input) => { try { const validatedInput = validateCodeReviewData(input); if (!validatedInput) { throw new McpError(ErrorCode.InvalidParams, "Invalid code review data"); } // Validate draft progression if (validatedInput.draft_number > validatedInput.total_drafts) { validatedInput.total_drafts = validatedInput.draft_number; } // Get session manager and retrieve/create session const sessionManager = SessionManagerFactory.getInstance().getCodeReviewManager(); const session = await sessionManager.getSession(validatedInput.review_id); // Initialize session data if needed if (!session.data.reviewHistory) { session.data.reviewHistory = []; } if (!session.data.activeCritiques) { session.data.activeCritiques = []; } // Store the code review in history session.data.reviewHistory.push(validatedInput); // Handle critique tracking if (validatedInput.is_critique && validatedInput.critique_focus) { session.data.activeCritiques.push(validatedInput.critique_focus); } // Update session await sessionManager.updateSession(validatedInput.review_id, session.data); // Format response return { content: [{ type: "text", text: JSON.stringify({ reviewId: validatedInput.review_id, pullRequestId: validatedInput.pull_request_id, repository: validatedInput.repository, draftNumber: validatedInput.draft_number, totalDrafts: validatedInput.total_drafts, dimensions: validatedInput.review_dimensions, fileCount: validatedInput.files.length, findingCount: validatedInput.findings.length, nextStepNeeded: validatedInput.next_step_needed, isCritique: validatedInput.is_critique, critiqueFocus: validatedInput.critique_focus, revisionInstructions: validatedInput.revision_instructions, activeCritiques: session.data.activeCritiques, reviewHistoryLength: session.data.reviewHistory.length, isFinalDraft: validatedInput.is_final_draft, sessionMetadata: session.metadata }, null, 2) }] }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InternalError, error instanceof Error ? error.message : String(error)); } }; server.tool(TOOL_NAME, TOOL_DESCRIPTION, TOOL_SCHEMA, async (args, extra) => ({ content: [{ type: "text", text: JSON.stringify(await processReviewRequest(args)) }] })); }; const validateCodeReviewData = (input) => { const data = input; // Check required fields first if (typeof data !== "object" || data === null || !("review_id" in data) || !("pull_request_id" in data) || !("repository" in data) || !("review_dimensions" in data) || !("files" in data) || !("findings" in data) || !("draft_number" in data) || !("total_drafts" in data) || !("next_step_needed" in data)) { throw new Error("Missing required fields"); } // Validate dimensions if (!Array.isArray(data.review_dimensions)) { throw new Error("Review dimensions must be an array"); } const validDimensions = ["performance", "security", "maintainability", "readability", "testability", "correctness", "documentation"]; const validSeverities = ["info", "suggestion", "warning", "critical"]; const dimensions = []; for (const dim of data.review_dimensions) { const dimStr = String(dim); if (!validDimensions.includes(dimStr)) { throw new Error(`Invalid review dimension: ${dimStr}. Must be one of: ${validDimensions.join(", ")}`); } dimensions.push(dimStr); } // Validate files if (!Array.isArray(data.files)) { throw new Error("Files must be an array"); } const files = []; for (const file of data.files) { if (typeof file !== "object" || file === null || !("path" in file) || !("content" in file) || !("language" in file) || !("line_count" in file)) { throw new Error("Each file must have path, content, language, and line_count"); } const lineCount = Number(file.line_count); if (isNaN(lineCount) || lineCount < 0) { throw new Error("Line count must be a non-negative number"); } files.push({ path: String(file.path), content: String(file.content), language: String(file.language), line_count: lineCount }); } // Validate findings if (!Array.isArray(data.findings)) { throw new Error("Findings must be an array"); } const findings = []; for (const finding of data.findings) { if (typeof finding !== "object" || finding === null || !("file" in finding) || !("line_range" in finding) || !("dimension" in finding) || !("severity" in finding) || !("description" in finding)) { throw new Error("Each finding must have file, line_range, dimension, severity, and description"); } // Validate line range if (!Array.isArray(finding.line_range) || finding.line_range.length !== 2) { throw new Error("Line range must be an array of two numbers [start, end]"); } const startLine = Number(finding.line_range[0]); const endLine = Number(finding.line_range[1]); if (isNaN(startLine) || isNaN(endLine) || startLine < 1 || endLine < startLine) { throw new Error("Invalid line range: start line must be >= 1 and end line must be >= start line"); } // Validate dimension const dimStr = String(finding.dimension); if (!validDimensions.includes(dimStr)) { throw new Error(`Invalid finding dimension: ${dimStr}. Must be one of: ${validDimensions.join(", ")}`); } // Validate severity const sevStr = String(finding.severity); if (!validSeverities.includes(sevStr)) { throw new Error(`Invalid finding severity: ${sevStr}. Must be one of: ${validSeverities.join(", ")}`); } const reviewFinding = { file: String(finding.file), line_range: [startLine, endLine], dimension: dimStr, severity: sevStr, description: String(finding.description) }; // Optional fields if ("suggested_fix" in finding) reviewFinding.suggested_fix = String(finding.suggested_fix); if ("justification" in finding) reviewFinding.justification = String(finding.justification); findings.push(reviewFinding); } // Create and validate code review data object const reviewData = { review_id: String(data.review_id), pull_request_id: String(data.pull_request_id), repository: String(data.repository), review_dimensions: dimensions, files: files, findings: findings, draft_number: Number(data.draft_number), total_drafts: Number(data.total_drafts), next_step_needed: Boolean(data.next_step_needed) }; // Optional fields if ("is_critique" in data) reviewData.is_critique = Boolean(data.is_critique); if ("critique_focus" in data) reviewData.critique_focus = String(data.critique_focus); if ("revision_instructions" in data) reviewData.revision_instructions = String(data.revision_instructions); if ("is_final_draft" in data) reviewData.is_final_draft = Boolean(data.is_final_draft); // Additional validations if (reviewData.draft_number <= 0) { throw new Error("Draft number must be positive"); } if (reviewData.total_drafts <= 0) { throw new Error("Total drafts must be positive"); } if (reviewData.draft_number > reviewData.total_drafts) { throw new Error("Draft number cannot exceed total drafts"); } // Validate critique-specific fields if (reviewData.is_critique && !reviewData.critique_focus) { throw new Error("Critique focus required when is_critique is true"); } // Validate revision-specific fields if (reviewData.is_critique === false && !reviewData.revision_instructions) { throw new Error("Revision instructions required when is_critique is false"); } // Validate file and finding relationships const filePaths = files.map(f => f.path); for (const finding of findings) { if (!filePaths.includes(finding.file)) { throw new Error(`Finding references file '${finding.file}' which is not in the files array`); } } return reviewData; }; const formatCodeReview = (review) => { return JSON.stringify({ reviewId: review.review_id, pullRequestId: review.pull_request_id, repository: review.repository, dimensions: review.review_dimensions, fileCount: review.files.length, findingCount: review.findings.length, findingsBySeverity: { info: review.findings.filter(f => f.severity === "info").length, suggestion: review.findings.filter(f => f.severity === "suggestion").length, warning: review.findings.filter(f => f.severity === "warning").length, critical: review.findings.filter(f => f.severity === "critical").length }, draftNumber: review.draft_number }, null, 2); };