import fs from "node:fs/promises"; import path from "node:path"; /** * Lectura directa de los markdown del knowledge base desde el filesystem. * * El MCP server corre dentro del container `agentic` junto al FastAPI, asi * que los .md viven en `/app/docs/` (la imagen los copia ahi). * * En caso de override por entorno, respeta `ACAI_DOCS_DIR`. En desarrollo * fuera del container, fallback a paths relativos al cwd. */ function resolveDocsDir() { const override = process.env.ACAI_DOCS_DIR; if (override) return override; // Container path return "/app/docs"; } export async function listDocs() { const dir = resolveDocsDir(); let files; try { files = await fs.readdir(dir); } catch (err) { const error = new Error(`No se pudo leer el directorio de docs (${dir}): ${err.message}`); error.code = "DOCS_DIR_NOT_FOUND"; throw error; } const mdFiles = files.filter((f) => f.endsWith(".md")).sort(); const docs = []; for (const file of mdFiles) { const id = file.replace(/\.md$/i, ""); const filePath = path.join(dir, file); const content = await fs.readFile(filePath, "utf8"); const lines = content.split("\n"); const titleLine = lines.find((l) => l.trim().startsWith("# ")) || id; const title = titleLine.replace(/^#+/, "").trim(); // Summary: primeras 30 lineas no-heading, capeado a 500 chars const summaryLines = []; for (const line of lines.slice(0, 30)) { const t = line.trim(); if (t && !t.startsWith("#")) summaryLines.push(t); if (summaryLines.join(" ").length > 500) break; } const summary = summaryLines.join(" ").slice(0, 500); docs.push({ id, title, summary, chars: content.length, }); } return docs; } export async function readDoc(name, section) { const dir = resolveDocsDir(); const safeName = String(name).replace(/\.md$/i, "").replace(/[\/\\]/g, ""); const filePath = path.join(dir, `${safeName}.md`); let content; try { content = await fs.readFile(filePath, "utf8"); } catch (err) { const error = new Error(`Doc '${safeName}' no encontrado en ${dir}`); error.code = "DOC_NOT_FOUND"; throw error; } const titleLine = content.split("\n").find((l) => l.trim().startsWith("# ")) || safeName; const title = titleLine.replace(/^#+/, "").trim(); const availableSections = listSections(content); if (section) { const sectionContent = extractSection(content, section); if (sectionContent === null) { return { id: safeName, title, section_requested: section, section_found: false, available_sections: availableSections, chars: 0, content: "", }; } return { id: safeName, title, section, section_found: true, chars: sectionContent.length, content: sectionContent, }; } return { id: safeName, title, section: null, section_found: true, chars: content.length, available_sections: availableSections, content, }; } function listSections(content) { const sections = []; for (const line of content.split("\n")) { const t = line.trimStart(); if (t.startsWith("## ") && !t.startsWith("### ")) { sections.push(t.slice(3).trim()); } } return sections; } function extractSection(content, query) { const q = String(query).toLowerCase().trim(); if (!q) return null; const lines = content.split("\n"); const captured = []; let capturing = false; for (const line of lines) { const t = line.trimStart(); const isH2 = t.startsWith("## ") && !t.startsWith("### "); if (isH2) { const heading = t.slice(3).trim(); if (capturing) break; if (heading.toLowerCase().includes(q)) { capturing = true; captured.push(line); continue; } } if (capturing) captured.push(line); } if (captured.length === 0) return null; return captured.join("\n").trimEnd(); }