From b9df5cbb54618b3091924d742225abd00f45cd06 Mon Sep 17 00:00:00 2001 From: Jordan Diaz Date: Sat, 25 Jul 2026 23:08:52 +0000 Subject: [PATCH] feat: tool set_upload_info para editar info1-info5 de uploads (registros y vars de modulo) + docs --- docs/09-mcp-tools-reference.md | 11 ++ docs/11a-decision-table.md | 1 + mcp-server/tools/media/index.js | 4 + mcp-server/tools/media/setUploadInfo.js | 115 ++++++++++++++++++ .../tools/records/getModuleConfigVars.js | 4 +- 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 mcp-server/tools/media/setUploadInfo.js diff --git a/docs/09-mcp-tools-reference.md b/docs/09-mcp-tools-reference.md index 3581903..f747305 100644 --- a/docs/09-mcp-tools-reference.md +++ b/docs/09-mcp-tools-reference.md @@ -109,8 +109,16 @@ Ver `06-hooks-and-cmsapi.md` para uso. Crear/editar el `.php` del hook se hace c | `replace_record_image` | Reemplaza un upload existente por uno nuevo | Necesita `uploadId` (de `list_record_uploads`). Borra el viejo + sube el nuevo, ambos con sync a producción | | `delete_record_upload` | Borra un upload concreto del campo | Necesita `uploadId`. Sincroniza el borrado a producción | | `reorder_record_uploads` | Cambia el orden de los uploads de un campo | Lista de `uploadIds` en el orden deseado | +| `set_upload_info` | Escribe los metadatos `info1`..`info5` de un upload YA subido | Necesita `uploadId` (de `list_record_uploads`) + al menos un `infoN`. Solo escribe las claves enviadas; `""` vacía ese info. Máx 1000 caracteres por campo | | `upload_image_to_assets` | Sube imagen a `/images/` del template (assets globales) | Acepta base64, data URI, URL. Permite resize/quality/format | +`set_upload_info` es la única vía para rellenar `info2`..`info5` (al subir solo se escribe `info1`, vía el param `alt`). Como `uploadId` es la PK de la tabla central `uploads`, la MISMA tool sirve para los dos tipos de upload — no se pasa tabla/registro/campo, el `uploadId` ya identifica la fila: + +- **Uploads de un registro normal**: el `uploadId` sale de `list_record_uploads({ tableName, recordId, fieldName })`. +- **Uploads de variables de módulo**: `get_module_config_vars` devuelve `uploadFields` (`varName` → `{ tableName: "builder_custom", recordNum, fieldName }`; `set_module_config_vars` devuelve lo mismo y además cubre las vars `multi`, con clave `"varName.subVarName"` → array `[{index, fieldName, recordNum}]`). Con eso llama a `list_record_uploads({ tableName: "builder_custom", recordId: recordNum, fieldName })` para sacar los `uploadId`. + +`info1` es por convención el **alt text**: la misma columna que escribe el param `alt` de `upload_record_image`/`replace_record_image`, así que escribirlo aquí lo sobrescribe. El significado de `info2`..`info5` lo define **cada módulo**: los declara en su `index-base.tpl` con `data-field-info1`..`data-field-info5` y quedan guardados como `infoLabels` en su `builder.json`. No hace falta abrir el fichero: cada entrada de `uploadFields` incluye `infoLabels` cuando el módulo los define — array donde la posición 0 es `info1`, la 1 es `info2`, etc. (para uploads dentro de vars `multi`, los labels salen en `varsMeta..subFields..infoLabels`). El `builder.json` (`vars..infoLabels`) sigue siendo la fuente canónica si necesitas comprobarlo. Consulta esos labels antes de escribir para no inventarte el significado de cada info. Ver `01-builder-fields.md`. + ### Navegación | Tool | Acción | @@ -211,6 +219,8 @@ Generar imagen primero: 2. Usa la URL recomendada que devuelve (`uploadUrl` o `fullUrl` en Forge; `dockerUrl` solo en local). 3. `upload_record_image` con esa URL. +Para rellenar los metadatos `info1`..`info5` de una imagen ya subida (alt text y los campos que el módulo define en sus `infoLabels`): `set_upload_info` — ver su entrada en la sección Media. + ### 4. Crear funcionalidad nueva con tabla + detalle Ejemplo: implementar "Vacantes". @@ -286,6 +296,7 @@ Según lo que pida el usuario: - **Reemplazar** una imagen concreta: `replace_record_image({ tableName, recordId, fieldName, uploadId, imageUrl, alt? })` — borra el viejo + sube el nuevo, ambos con sync a producción. - **Borrar** una imagen: `delete_record_upload({ uploadId, table? })` — sync de borrado a producción. - **Reordenar**: `reorder_record_uploads({ tableName, recordId, fieldName, uploadIds: [...] })` con la lista en el orden deseado. +- **Editar metadatos** (alt text, pie de foto, crédito…) sin tocar el fichero: `set_upload_info({ uploadId, info1?..info5? })` — ver su entrada en la sección Media. Para AÑADIR un upload nuevo (sin reemplazar nada existente), usa `upload_record_image` directamente. diff --git a/docs/11a-decision-table.md b/docs/11a-decision-table.md index b9ad846..4ff44a4 100644 --- a/docs/11a-decision-table.md +++ b/docs/11a-decision-table.md @@ -45,6 +45,7 @@ Tabla decisional para mapear la intención del usuario a la herramienta correcta | Reemplazar imagen existente | `list_record_uploads` → `replace_record_image({ uploadId, imageUrl })` | | Borrar una imagen | `list_record_uploads` → `delete_record_upload({ uploadId })` | | Reordenar galería | `list_record_uploads` → `reorder_record_uploads({ uploadIds: [...] })` | +| Editar alt/metadatos de una imagen ya subida | `list_record_uploads` → `set_upload_info({ uploadId, info1?..info5? })` | | Subir imagen a `/images/` (assets globales del template) | `upload_image_to_assets({ imageUrl, fileName })` | ## Tablas y campos (schema) diff --git a/mcp-server/tools/media/index.js b/mcp-server/tools/media/index.js index 67d7d08..4912897 100644 --- a/mcp-server/tools/media/index.js +++ b/mcp-server/tools/media/index.js @@ -2,10 +2,14 @@ import { registerUploadRecordImageTool } from './upload.js'; import { registerUploadImageToAssetsTool } from './uploadImageToAssets.js'; import { registerGenerateImageTool } from './generateImage.js'; import { registerAnalyzeImageTool } from './analyze_image.js'; +import { registerSetUploadInfoTool } from './setUploadInfo.js'; export function registerMediaTools(server) { registerUploadRecordImageTool(server); registerUploadImageToAssetsTool(server); registerGenerateImageTool(server); registerAnalyzeImageTool(server); + // Metadatos info1..info5 de uploads: son datos de contenido, no codigo, + // asi que va sin gate de canEditCode() como el resto de tools de media. + registerSetUploadInfoTool(server); } diff --git a/mcp-server/tools/media/setUploadInfo.js b/mcp-server/tools/media/setUploadInfo.js new file mode 100644 index 0000000..02abb8c --- /dev/null +++ b/mcp-server/tools/media/setUploadInfo.js @@ -0,0 +1,115 @@ +import { z } from "zod"; +import { withAuth } from "../../auth/index.js"; +import { withAuthParams } from "../helpers/authSchema.js"; +import { handleToolError, validateRequired } from "../helpers/errorHandler.js"; +import { pythonPost } from "../helpers/pythonServerClient.js"; +import { getCurrentProjectInfo } from "../files/helpers.js"; + +// Tool: set_upload_info +// +// Rellena los metadatos info1..info5 de una imagen YA subida, delegando en +// /api/uploads/set-info del server Python. +// +// El uploadId es la PK (`num`) de la tabla CENTRAL `uploads`, asi que la misma +// tool sirve tanto para uploads de registros normales como para uploads que +// viven en vars de modulo (que fisicamente cuelgan de `builder_custom`). Por +// eso no hace falta pasar tabla/registro/campo: el uploadId ya identifica la +// fila de forma univoca. +// +// Ojo con el filtrado de claves: se comprueba `!== undefined`, NO truthiness, +// para que una cadena vacia ("") viaje en el body y sirva para VACIAR un info. + +const INFO_KEYS = ["info1", "info2", "info3", "info4", "info5"]; + +// Tope por campo que ya impone el endpoint (_UPLOAD_INFO_MAX_LEN en cms_db.py). +// Se replica en el zod para que el agente reciba el error sin round-trip. +const INFO_MAX_LEN = 1000; + +export function registerSetUploadInfoTool(server) { + server.tool( + "set_upload_info", + `Set the metadata fields info1..info5 of an image that has ALREADY been uploaded. This is the only way to fill infoN metadata: uploading only ever writes info1 (via the 'alt' param of upload_record_image / replace_record_image). + +uploadId is the primary key ('num') of the CENTRAL 'uploads' table, so THIS SAME TOOL WORKS FOR BOTH KINDS OF UPLOAD — you never pass table/record/field here, the uploadId alone identifies the file: + +1) Uploads of a NORMAL record: get the uploadId with list_record_uploads(tableName, recordId, fieldName). + +2) Uploads stored in MODULE VARS: call get_module_config_vars first. Its 'uploadFields' is a map varName -> { tableName: "builder_custom", recordNum, fieldName, infoLabels? } (set_module_config_vars returns the same map and ALSO covers uploads nested inside a 'multi' var, where the key is "varName.subVarName" — e.g. "slides.imagen" — and the value is an array of { index, fieldName, recordNum }). Then call list_record_uploads with tableName: "builder_custom", recordId: and fieldName: to get the uploadIds. + +Meaning of each infoN: +- info1 is BY CONVENTION the alt text — the very same column the 'alt' param writes when uploading. Setting info1 here overwrites that alt text. +- info2..info5 are free metadata slots whose meaning is defined PER MODULE: the module declares them in its index-base.tpl with the attributes data-field-info1..data-field-info5, and they are stored as 'infoLabels' in the module's builder.json (e.g. info2 = "caption", info3 = "credit/author", info4 = "link"). You do NOT need to open that file: get_module_config_vars returns them inside each 'uploadFields' entry as 'infoLabels' whenever the module defines them — an array where position 0 is info1, position 1 is info2, etc. (for uploads inside a 'multi' var the labels come in varsMeta..subFields..infoLabels). The module's builder.json (vars..infoLabels) is still the canonical source if you need to double-check. Read those labels before writing — do NOT invent a meaning. + +At least one of info1..info5 is required. Only the keys you send are written; the ones you omit are left untouched. Passing an empty string ("") CLEARS that info field. Max 1000 characters per field.`, + withAuthParams({ + uploadId: z.string().describe("Upload ID: the 'num' PK of the central 'uploads' table (from list_record_uploads, for a normal record or for builder_custom in the case of module vars)"), + info1: z.string().max(INFO_MAX_LEN).optional().describe("info1 — by convention the ALT TEXT of the image (same column the 'alt' upload param writes). Empty string clears it. Max 1000 chars."), + info2: z.string().max(INFO_MAX_LEN).optional().describe("info2 — module-defined metadata (label at position 1 of 'infoLabels', returned by get_module_config_vars in uploadFields). Empty string clears it. Max 1000 chars."), + info3: z.string().max(INFO_MAX_LEN).optional().describe("info3 — module-defined metadata (label at position 2 of 'infoLabels', returned by get_module_config_vars in uploadFields). Empty string clears it. Max 1000 chars."), + info4: z.string().max(INFO_MAX_LEN).optional().describe("info4 — module-defined metadata (label at position 3 of 'infoLabels', returned by get_module_config_vars in uploadFields). Empty string clears it. Max 1000 chars."), + info5: z.string().max(INFO_MAX_LEN).optional().describe("info5 — module-defined metadata (label at position 4 of 'infoLabels', returned by get_module_config_vars in uploadFields). Empty string clears it. Max 1000 chars."), + }), + { readOnlyHint: false, destructiveHint: false }, + withAuth(async ({ uploadId, info1, info2, info3, info4, info5 }, _extra) => { + try { + const validationError = validateRequired( + { uploadId }, + ["uploadId"], + "set_upload_info" + ); + if (validationError) return validationError; + + const provided = { info1, info2, info3, info4, info5 }; + + // Validacion temprana: sin ninguna clave infoN no hay nada que + // escribir, asi que ni llamamos al endpoint. Se filtra por + // `undefined` y NO por truthiness, para que "" (vaciar un info) + // se considere un valor enviado. + const changedKeys = INFO_KEYS.filter((key) => provided[key] !== undefined); + if (changedKeys.length === 0) { + return handleToolError( + `Nothing to update: provide at least one of ${INFO_KEYS.join(", ")}. ` + + `info1 is the alt text; the meaning of info2..info5 is defined by the module ` + + `(read 'infoLabels' with get_module_config_vars). Pass "" to clear a field.`, + "set_upload_info", + { uploadId, editableFields: INFO_KEYS } + ); + } + + const { projectSlug } = getCurrentProjectInfo(); + + const body = { project: projectSlug, uploadId }; + for (const key of changedKeys) body[key] = provided[key]; + + const result = await pythonPost("/api/uploads/set-info", body); + + if (!result?.success) { + return { + content: [{ + type: "text", + text: JSON.stringify({ + success: false, + error: result?.error || "Could not set upload info", + }), + }], + isError: true, + }; + } + + return { + content: [{ + type: "text", + text: JSON.stringify({ + success: true, + uploadId: result.uploadId || uploadId, + updatedFields: changedKeys, + info: result.info || {}, + }, null, 2), + }], + }; + } catch (error) { + return handleToolError(error, "set_upload_info", { uploadId }); + } + }) + ); +} diff --git a/mcp-server/tools/records/getModuleConfigVars.js b/mcp-server/tools/records/getModuleConfigVars.js index 85b986b..0da2723 100644 --- a/mcp-server/tools/records/getModuleConfigVars.js +++ b/mcp-server/tools/records/getModuleConfigVars.js @@ -20,8 +20,8 @@ export function registerGetModuleConfigVarsTool(server) { `Get the current configuration variable values for a module instance on a page record. Returns: - vars: resolved values (text, HTML, etc.) for simple vars and arrays for multi/repeater vars -- varsMeta: per-var physical location { tableName: 'builder_custom', recordNum, fieldName, type }. USE THIS to know exactly which row + column to update with create_or_update_record. The variable's display name (e.g. 'titulo') is NOT the same as the physical column name (e.g. 'title2'). -- uploadFields: per-var upload location for upload_record_image / replace_record_image +- varsMeta: per-var physical location { tableName: 'builder_custom', recordNum, fieldName, type }. USE THIS to know exactly which row + column to update with create_or_update_record. The variable's display name (e.g. 'titulo') is NOT the same as the physical column name (e.g. 'title2'). Uploads inside a 'multi' var carry their labels in subFields..infoLabels. +- uploadFields: per-var upload location for upload_record_image / replace_record_image / set_upload_info. Each entry includes infoLabels when the module defines them (array; position 0 = info1). - moduleId, sectionId Required params: