diff --git a/mcp-server/tools/media/upload.js b/mcp-server/tools/media/upload.js index 121fcea..cb3f444 100644 --- a/mcp-server/tools/media/upload.js +++ b/mcp-server/tools/media/upload.js @@ -65,12 +65,12 @@ async function resolveLocalImageAsBase64(imageUrl) { export function registerUploadRecordImageTool(server) { server.tool( "upload_record_image", - "Upload an image to a specific record field in Acai CMS. MANDATORY: before calling this tool, you MUST call get_table_schema with minimal=true to find the EXACT upload field name. Look for fields with type='upload'. NEVER guess field names. Table names WITHOUT 'cms_' prefix. recordId is 'num', never 'id'. If the URL came from generate_image, prefer uploadUrl (or fullUrl) over dockerUrl.", + "Upload an image to a specific record field in Acai CMS. MANDATORY: before calling this tool, you MUST call get_table_schema with minimal=true to find the EXACT upload field name. Look for fields with type='upload'. NEVER guess field names. Table names WITHOUT 'cms_' prefix. recordId is 'num', never 'id'. If the URL came from generate_image, prefer uploadUrl (or fullUrl) over dockerUrl. For a LOCAL or pasted image (a file on your machine, no public URL): save it into the synced project folder cms/uploads/chat/.ext, wait for the sync to push it, then pass its PROJECT-RELATIVE path (e.g. 'cms/uploads/chat/foto.png') as imageUrl. NEVER pass a data-URI/base64 nor spin up a localhost server.", withAuthParams({ tableName: z.string().describe("Table name without 'cms_' prefix (e.g., 'productos')"), recordId: z.string().describe("Record 'num' (primary key)"), fieldName: z.string().describe("EXACT field name from the schema. MUST match a field with type 'upload' from get_table_schema or get_module_config_vars. Do NOT guess."), - imageUrl: z.string().describe("URL of the image to upload"), + imageUrl: z.string().describe("Image to upload: an http(s) URL, OR a project-relative path to a file already synced to the project (e.g. 'cms/uploads/chat/foto.png'). For local/pasted images use the relative-path form. NOT a data-URI or base64."), alt: z.string().optional().describe("Alt text for the image (optional)"), }), { readOnlyHint: false, destructiveHint: false }, @@ -83,19 +83,33 @@ export function registerUploadRecordImageTool(server) { ); if (validationError) return validationError; - // Rechazar data-URI / base64 crudo: derivar el nombre de fichero - // del base64 produce nombres de miles de chars que revientan - // file_put_contents ("File name too long"). Exigir URL http real. + // Aceptamos: URL http(s), ruta absoluta del servidor, o RUTA + // RELATIVA del proyecto (p.ej. "cms/uploads/chat/foto.png"). Para + // una imagen local/pegada el flujo correcto es guardarla en una + // carpeta sincronizada NO truncada (cms/uploads/chat/ o + // cms/uploads/generated/), dejar que el sync la suba a test y pasar + // aquí su ruta relativa: el server lee los bytes de disco vía + // resolve_image_source (sin base64 por el modelo). + // Seguimos rechazando data-URI / base64 crudo: derivar el nombre + // de un base64 gigante revienta file_put_contents ("File name too + // long"). El tope de longitud + charset de ruta lo descartan. const trimmedImage = imageUrl.trim(); const isHttpUrl = /^https?:\/\//i.test(trimmedImage); - const isFsPath = trimmedImage.startsWith("/") && !trimmedImage.startsWith("//"); - if (!isHttpUrl && !isFsPath) { + const isAbsPath = trimmedImage.startsWith("/") && !trimmedImage.startsWith("//"); + const isRelPath = !isHttpUrl && !isAbsPath + && !/^[a-z][a-z0-9+.-]*:/i.test(trimmedImage) // sin esquema (data:, file:...) + && !trimmedImage.includes("..") + && trimmedImage.length <= 512 + && /^[\w./ -]+$/.test(trimmedImage); // charset de ruta (no base64) + if (!isHttpUrl && !isAbsPath && !isRelPath) { return { content: [{ type: "text", text: JSON.stringify({ - error: "imageUrl must be an http(s) URL, not a data URI or raw base64. " + - "First upload the image with the 'upload_image_to_assets' tool and pass the returned imageUrl here." + error: "imageUrl debe ser una URL http(s) o una ruta relativa del proyecto " + + "(p.ej. 'cms/uploads/chat/foto.png'), no un data-URI ni base64 crudo. " + + "Para una imagen local/pegada: guárdala en cms/uploads/chat/ (sincronizada a test), " + + "espera a que el sync la suba y pasa su ruta relativa." }, null, 2) }], isError: true, @@ -221,7 +235,7 @@ export function registerUploadRecordImageTool(server) { recordId: z.string().describe("Record 'num' (primary key)"), fieldName: z.string().describe("Upload field name"), uploadId: z.string().describe("Upload ID to replace (get from list_record_uploads)"), - imageUrl: z.string().describe("URL of the new image to upload"), + imageUrl: z.string().describe("New image: an http(s) URL, OR a project-relative path to a file already synced (e.g. 'cms/uploads/chat/foto.png'). For local/pasted images use the relative-path form. NOT a data-URI or base64."), alt: z.string().optional().describe("Alt text for the image (optional)"), }), { readOnlyHint: false, destructiveHint: false },