ajustes
This commit is contained in:
151
mcp-server/tools/docs/_docsReader.js
Normal file
151
mcp-server/tools/docs/_docsReader.js
Normal file
@@ -0,0 +1,151 @@
|
||||
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();
|
||||
}
|
||||
20
mcp-server/tools/docs/index.js
Normal file
20
mcp-server/tools/docs/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { registerListDocsTool } from './list_docs.js';
|
||||
import { registerReadDocTool } from './read_doc.js';
|
||||
|
||||
/**
|
||||
* Tools para consultar la documentación del proyecto bajo demanda.
|
||||
*
|
||||
* El knowledge_base del context engine ya inyecta los docs más relevantes
|
||||
* en cada turno por similitud semántica, pero hay dos casos en los que el
|
||||
* agente necesita pedir un doc explícitamente:
|
||||
* - El doc cayó fuera del top-K y solo aparece en el title-index del KB.
|
||||
* - Se necesita una sección puntual con detalle sin cargar todo el doc.
|
||||
*
|
||||
* Estas tools delegan en endpoints `/api/knowledge/...` del server Python,
|
||||
* que leen los docs del memory store (Redis) — la misma fuente que alimenta
|
||||
* el knowledge_base.
|
||||
*/
|
||||
export function registerDocsTools(server) {
|
||||
registerListDocsTool(server);
|
||||
registerReadDocTool(server);
|
||||
}
|
||||
29
mcp-server/tools/docs/list_docs.js
Normal file
29
mcp-server/tools/docs/list_docs.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { handleToolError } from "../helpers/errorHandler.js";
|
||||
import { listDocs } from "./_docsReader.js";
|
||||
|
||||
export function registerListDocsTool(server) {
|
||||
server.tool(
|
||||
"list_docs",
|
||||
`Lista todos los docs disponibles en el knowledge base con su id, título y un summary corto. Útil cuando necesitas saber qué documentación existe antes de llamar a 'read_doc'. El system prompt y el knowledge_base ya cargan los docs más relevantes a tu tarea — usa 'list_docs' / 'read_doc' solo si necesitas un doc que no apareció completo en el contexto o quieres una sección específica con detalle.`,
|
||||
{},
|
||||
{ readOnlyHint: true, destructiveHint: false },
|
||||
async () => {
|
||||
try {
|
||||
const docs = await listDocs();
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: docs.length,
|
||||
documents: docs,
|
||||
note: "Usa 'read_doc' con el 'id' (e.g. '05-tables-and-fields') para leer el doc completo o pasa 'section' para una sección concreta.",
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
return handleToolError(error, "list_docs", {});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
68
mcp-server/tools/docs/read_doc.js
Normal file
68
mcp-server/tools/docs/read_doc.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { z } from "zod";
|
||||
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
|
||||
import { readDoc } from "./_docsReader.js";
|
||||
|
||||
export function registerReadDocTool(server) {
|
||||
server.tool(
|
||||
"read_doc",
|
||||
`Lee un documento del knowledge base de Acai bajo demanda.
|
||||
|
||||
Cuándo usarlo:
|
||||
- El doc no se cargó completo en el contexto (aparece en "Other Available Docs" del knowledge_base).
|
||||
- Necesitas una sección concreta con detalle sin cargar todo el doc.
|
||||
- Vas a hacer una operación delicada y quieres releer las reglas exactas (e.g. crear una tabla, escribir un hook, editar el header).
|
||||
|
||||
Parámetros:
|
||||
- name: id del doc (sin extensión). Ejemplos: '01-builder-fields', '05-tables-and-fields', '09-mcp-tools-reference'. Usa 'list_docs' si dudas del id.
|
||||
- section: opcional. Match case-insensitive y parcial sobre headings H2 ('## ...'). Devuelve desde el heading hasta el siguiente H2.
|
||||
|
||||
Si la sección no existe, la respuesta incluye 'available_sections' para que reintentes con un nombre válido.
|
||||
|
||||
Docs disponibles (resumen):
|
||||
- 01-builder-fields — Campos editables (data-field-type), atributos Acai (c-if, c-for, c-class), c-form, componentes built-in.
|
||||
- 02-twig — Filtros Twig (get, queryDB, hook, module, imagec, translate, raw).
|
||||
- 03-modules-and-sections — Módulos vs secciones generales, thisrecord, multiv2, custom-{tableName}.
|
||||
- 04-pages-and-records — Builder vs Standard, menuType, apartados, reglas sobre enlace/controlador.
|
||||
- 05-tables-and-fields — Schema, create_table, create_field, tipos de campo, casos destructivos.
|
||||
- 06-hooks-and-cmsapi — Hooks PHP, CmsApi/CocoDB, hook middleware.
|
||||
- 07-css-js-conventions — Tailwind+BEM, scoping, Vue 3, componentes nativos.
|
||||
- 08-layout-and-libraries — get/set_layout_field, librerías globales, regla de no editar layout.json.
|
||||
- 09-mcp-tools-reference — Inventario completo + workflows canónicos.
|
||||
- 10-production-patterns — Patrones reales (cabecera, zigzag, FAQ, formulario, detalle).
|
||||
- 11-quick-reference — Cheat sheet con todas las reglas y formatos.`,
|
||||
{
|
||||
name: z.string().describe("ID del doc sin extensión (e.g. '05-tables-and-fields')"),
|
||||
section: z.string().optional().describe("Heading H2 a extraer (case-insensitive, parcial). Omitir para leer el doc completo."),
|
||||
},
|
||||
{ readOnlyHint: true, destructiveHint: false },
|
||||
async ({ name, section }) => {
|
||||
try {
|
||||
const validationError = validateRequired({ name }, ["name"], "read_doc");
|
||||
if (validationError) return validationError;
|
||||
|
||||
const data = await readDoc(name, section);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(data, null, 2),
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
if (error?.code === "DOC_NOT_FOUND") {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: error.message,
|
||||
hint: "Usa 'list_docs' para ver los ids disponibles. Los ids tienen prefijo numérico (e.g. '05-tables-and-fields').",
|
||||
}, null, 2),
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
return handleToolError(error, "read_doc", { name, section });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user