import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { loadManifest, validateSlotContent, correctSlotContent, summarizeValidationIssues, } from "@mandujs/core"; import { getProjectPaths, isInsideProject } from "../utils/project.js"; import path from "path"; export const slotToolDefinitions: Tool[] = [ { name: "mandu.slot.read", annotations: { readOnlyHint: true, }, description: "Read the TypeScript source of a route's slot file and validate its structure.", inputSchema: { type: "object", properties: { routeId: { type: "string", description: "The route ID whose slot file to read (use mandu_list_routes to find IDs)", }, }, required: ["routeId"], }, }, { name: "mandu.slot.validate", annotations: { readOnlyHint: true, }, description: "Validate TypeScript slot content against structural rules without writing files. Returns errors, warnings, and auto-fix previews.", inputSchema: { type: "object", properties: { content: { type: "string", description: "The TypeScript slot source code to validate", }, }, required: ["content"], }, }, ]; export function slotTools(projectRoot: string) { const paths = getProjectPaths(projectRoot); const handlers: Record) => Promise> = { "mandu.slot.read": async (args: Record) => { const { routeId } = args as { routeId: string }; // Load manifest to find the route const manifestResult = await loadManifest(paths.manifestPath); if (!manifestResult.success || !manifestResult.data) { return { error: manifestResult.errors }; } const route = manifestResult.data.routes.find((r) => r.id === routeId); if (!route) { return { error: `Route not found: ${routeId}` }; } if (!route.slotModule) { return { error: `Route '${routeId}' does not have a slotModule defined`, tip: "Add slotModule to the route spec or use mandu_update_route", }; } const slotPath = path.join(projectRoot, route.slotModule); // Security check if (!isInsideProject(slotPath, projectRoot)) { return { error: "Slot path is outside project directory" }; } try { const file = Bun.file(slotPath); if (!(await file.exists())) { return { exists: false, slotPath: route.slotModule, message: "Slot file does not exist. Run mandu.generate to create it.", }; } const content = await file.text(); // Validate existing slot content structure const validation = validateSlotContent(content); return { exists: true, slotPath: route.slotModule, content, lineCount: content.split("\n").length, validation: { valid: validation.valid, summary: summarizeValidationIssues(validation.issues), issues: validation.issues, }, }; } catch (error) { return { error: `Failed to read slot file: ${error instanceof Error ? error.message : String(error)}`, }; } }, "mandu.slot.validate": async (args: Record) => { const { content } = args as { content: string }; const validation = validateSlotContent(content); // Classify issues by whether they can be auto-fixed const autoFixable = validation.issues.filter((i) => i.autoFixable); const manualFix = validation.issues.filter((i) => !i.autoFixable); // Generate correction preview for auto-fixable issues let correctionPreview = null; if (autoFixable.length > 0) { const correction = correctSlotContent(content, validation.issues); correctionPreview = { wouldFix: correction.appliedFixes.length, fixes: correction.appliedFixes, resultingContent: correction.corrected ? `(${correction.content.split("\n").length} lines after correction)` : null, }; } return { valid: validation.valid, summary: summarizeValidationIssues(validation.issues), errors: validation.issues.filter((i) => i.severity === "error"), warnings: validation.issues.filter((i) => i.severity === "warning"), autoFixable: autoFixable.map((i) => ({ code: i.code, message: i.message, line: i.line, })), manualFixRequired: manualFix.map((i) => ({ code: i.code, message: i.message, suggestion: i.suggestion, line: i.line, })), correctionPreview, tip: validation.valid ? "Content is valid. Use Edit tool to write the slot file." : autoFixable.length > 0 ? "Auto-fixable issues found. Apply corrections and use Edit tool to write." : "Manual fixes required before writing. Use Edit tool after fixing.", }; }, }; // Backward-compatible aliases handlers["mandu_read_slot"] = handlers["mandu.slot.read"]; handlers["mandu_validate_slot"] = handlers["mandu.slot.validate"]; return handlers; }