Files
agenticSystem/mcp-server/tools/modules/setExampleData.js
Jordan Diaz 76221ee1d4 refactor: set_module_example_data escribe via el server Python
La tool llamaba a `action_ws=setStaticVars`, que hace un file_put_contents
del builder.json ENTERO en la web para cambiar una sola clave. Eso se
saltaba las dos garantias que protegen ese fichero: el bloqueo de escritura
de la API de ficheros (BLOCKED_GENERATED_FILENAMES en handlers/files.py) y
la allowlist de /api/modules/update-metadata.

El riesgo no era teorico. El builder.json es la unica memoria de que
variable vive en que columna de builder_custom: si una compilacion caia
entre la lectura y la escritura de setStaticVars, esta devolvia el mapeo
var->columna anterior encima del recien generado y el contenido guardado
quedaba colgado de la variable equivocada en TODAS las paginas que usan el
modulo.

Ahora delega en /api/modules/update-metadata, que ya es la via quirurgica
para la metadata y acepta staticVars en su allowlist (objeto, <=64KB,
<=200 claves).

Se aprovecha para quitar el volcado del schema y del payload completos por
consola en cada llamada.

Companion en el repo de Forge: compiler.js reenvia staticVars en cada
compilacion. Antes no lo mandaba, asi que el CMS lo tiraba al regenerar el
builder.json — solo 5 de 4870 modulos lo conservaban.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:21:16 +00:00

102 lines
5.6 KiB
JavaScript

import { z } from "zod";
import { withAuth } from "../../auth/index.js";
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { pythonPost } from "../helpers/pythonServerClient.js";
import { getCurrentProjectInfo } from "../files/helpers.js";
// Antes esta tool llamaba a `action_ws=setStaticVars`, que hacia un
// file_put_contents del builder.json ENTERO en la web para cambiar una sola
// clave. Eso se saltaba el bloqueo de escritura de builder.json y, si caia una
// compilacion entre su lectura y su escritura, devolvia el mapeo var->columna
// anterior encima del recien generado (contenido rotado en todas las paginas
// que usan el modulo). Ahora delega en el endpoint quirurgico de Forge, que
// escribe SOLO las claves de su allowlist.
export function registerSetModuleExampleDataTool(server) {
server.tool(
"set_module_example_data",
`Define datos de ejemplo para el preview del módulo en el editor. Antes de llamar, lee el builder.json del módulo (con 'acai-view') o usa 'get_module_config_vars' para conocer las variables exactas. Rellena TODAS las variables del schema.
Reglas críticas:
- Uploads SIEMPRE como [{ urlPath: "..." }] (nunca strings ni objetos sueltos).
- 'multiv2' como array con al menos 2 items para que el preview se vea representativo.
- Los nombres de variables se derivan de 'data-field-label' (minúsculas, sin espacios ni acentos).
- Para URLs de imagen usa 'generate_image' o un placeholder (e.g. https://placehold.co/800x600).
Si dudas del formato exacto, lee 'read_doc({ name: "01-builder-fields" })'.`,
withAuthParams({
moduleId: z.string().describe("ID del módulo"),
moduleSchema: z.object({}).passthrough().describe("Schema completo del módulo (del builder.json o de 'get_module_config_vars')"),
exampleData: z.object({}).passthrough().describe("Datos de ejemplo para TODAS las variables del schema. La estructura debe coincidir exactamente."),
}),
{ readOnlyHint: false, destructiveHint: false },
withAuth(async ({ moduleId, moduleSchema, exampleData }, extra) => {
try {
// Validate required parameters
const validationError = validateRequired({ moduleId, exampleData }, ['moduleId', 'exampleData'], 'set_module_example_data');
if (validationError) return validationError;
// Validate that all schema variables are present in exampleData
if (moduleSchema && moduleSchema.codeVars) {
const schemaVars = Object.keys(moduleSchema.codeVars);
const dataVars = Object.keys(exampleData);
const missingVars = schemaVars.filter(v => !dataVars.includes(v));
if (missingVars.length > 0) {
console.warn(`[set_module_example_data] WARNING: Missing variables in exampleData: ${missingVars.join(', ')}`);
}
// Check for upload fields that are not arrays
for (const [varName, varInfo] of Object.entries(moduleSchema.codeVars)) {
if (varInfo.type === 'upload' && exampleData[varName]) {
if (!Array.isArray(exampleData[varName])) {
console.error(`[set_module_example_data] ERROR: Upload field '${varName}' is not an array! Current value: ${JSON.stringify(exampleData[varName])}`);
console.error(`[set_module_example_data] Upload fields MUST be arrays with urlPath objects: [{"urlPath": "..."}]`);
} else if (exampleData[varName].length > 0 && !exampleData[varName][0].urlPath) {
console.error(`[set_module_example_data] ERROR: Upload field '${varName}' items missing 'urlPath' property!`);
}
}
}
}
console.error(`[set_module_example_data] Module ID: ${moduleId}, vars: ${Object.keys(exampleData).length}`);
const { projectSlug } = getCurrentProjectInfo();
const result = await pythonPost("/api/modules/update-metadata", {
project: projectSlug,
module: moduleId,
staticVars: exampleData,
});
if (!result?.success) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
error: result?.error || "Could not set module example data",
}),
}],
isError: true,
};
}
return {
content: [{
type: "text", text: JSON.stringify({
success: true,
message: `Example data set successfully for module '${moduleId}'`,
moduleId: result.module || moduleId,
dataCount: Object.keys(exampleData).length,
schemaVarsCount: moduleSchema?.codeVars ? Object.keys(moduleSchema.codeVars).length : 0,
}, null, 2)
}],
};
} catch (error) {
return handleToolError(error, 'set_module_example_data', { moduleId });
}
})
);
}