Guillaume Duvernay

Tu MCP no acepta archivos binarios. Así puedes solucionarlo

MCPagentssecurityAI Glot

Mi SaaS AI Glot traduce archivos (CSV, XLSX, Word, PDF y varias decenas de formatos más) y tiene un servidor MCP. La primera versión tenía una limitación que conocía: el agente podía enviarme un enlace público a un archivo o un pequeño fragmento de texto, pero no había forma de incorporar un archivo que estuviera en tu ordenador.

Lo solucioné la semana pasada. El truco es sencillo, un poco chapucero, y creo que sirve para cualquier MCP que necesite recibir un archivo. Aquí lo tienes.

Por qué un archivo no cabe en una llamada a una herramienta

Los argumentos de una herramienta MCP son JSON. Para incluir un archivo binario, hay que codificarlo en base64, que es texto, y el modelo tiene que escribir ese texto, carácter a carácter, dentro de la llamada a la herramienta.

Los 2,7 millones corresponden al propio base64: cuatro caracteres por cada tres bytes. No lo he convertido a tokens, porque eso depende del modelo.

Es el mismo problema que con las 200 filas CSV de por qué dejé de usar MCP para casi todo mi trabajo, salvo que aquí una sola llamada ya tiene que hacerlo todo.

Y eso suponiendo que el agente pueda leer tu archivo. Conectar un servidor MCP no da al agente acceso a tus adjuntos ni a los archivos de tu disco. Eso depende de la aplicación que lo rodea.

La solución: el MCP da una URL y el agente envía allí el archivo

El agente no tiene que enviar el archivo a través de MCP. Solo necesita saber adónde enviarlo. El MCP devuelve una URL de subida y el agente transfiere el archivo por su cuenta mediante una petición HTTP normal.

El modelo solo escribe pequeñas cantidades de JSON. El archivo va directamente de tu equipo al almacenamiento y el modelo nunca lo lee.

En AI Glot, la primera herramienta es create_upload_url. Hay que indicar el nombre del archivo y su tamaño exacto en bytes:

{ "filename": "catalogue.xlsx", "size_bytes": 15961 }

Devuelve un upload_id, una URL, el método (PUT), encabezados temporales, dos plazos e instrucciones para el agente. Después, el agente ejecuta algo parecido a esto:

curl -X PUT "$UPLOAD_URL" \
  -H "Authorization: $TEMP_UPLOAD_TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @catalogue.xlsx

Después, llama a create_translation con solo el upload_id. Sin nombre de archivo, contenido ni base64. Obtiene un plan, te pide confirmación y approve_translation consume los créditos.

Lo probé en directo el día del lanzamiento con un XLSX de 15,961 bytes que contenía dos palabras. La petición PUT devolvió 200, el plan contabilizó dos palabras y, tras aprobarlo, la traducción costó un crédito. Crear la subida y el plan no costó nada.

La otra opción y por qué no basta

AI Glot ya aceptaba file_url: le das un enlace y mi servidor descarga el archivo. Funciona bien cuando el archivo ya está en línea.

Pero la mayoría de las veces el archivo está en tu ordenador. Para usar file_url, primero tendrías que subirlo a algún sitio, hacer que fuera de acceso público y pegar el enlace. Son demasiados pasos para alguien que solo quiere decir «traduce esta hoja de cálculo». Los archivos de texto pequeños se pueden enviar directamente como contenido, y listo.

Así que ahora hay tres formas de hacerlo: contenido de texto, una URL pública y la URL de subida para el archivo que solo existe en tu equipo.

Cuando el agente no puede enviar una solicitud PUT

Este es el límite. El agente tiene que leer el archivo y enviar una solicitud HTTP. Claude Code o Codex pueden hacerlo porque tienen acceso a una shell. Una ventana de chat sin un entorno de ejecución de código no puede, aunque tenga el MCP conectado.

Por eso el MCP tiene que dejarlo claro en todos los sitios que vaya a leer el agente: la descripción de la herramienta, el resultado de la herramienta, la documentación de la API y llms.txt. En mi caso, le indican al agente que, si no puede enviar el archivo adjunto, se lo explique claramente al usuario y le sugiera otras opciones: la aplicación web, la CLI o un enlace público. También le dicen que nunca afirme que se ha subido un archivo si no es cierto, ni apruebe un pago sin haber verificado el archivo y el plan.

Sin esto, un agente que no puede enviar la solicitud PUT simplemente falla sin avisar o, peor aún, finge que ha funcionado.

Cómo protegerlo

Estás permitiendo que un cliente HTTP desconocido escriba en tu almacenamiento, así que esta es la parte a la que más tiempo dediqué. La idea es que el secreto que devuelves sirva para una sola cosa y nada más.

El agente tiene ambas y nunca se intercambian. El archivo viaja con la débil y las instrucciones, con la fuerte.

Lo que hice y lo que volvería a hacer en cualquier servidor:

  • El secreto va en una cabecera, nunca en la URL. Las URL acaban en los registros y en el historial. La URL por sí sola no da acceso a nada.
  • Solo sirve para una sesión. Permite enviar una solicitud PUT al endpoint de esa sesión y nada más. Lo comprobé: si se envía a la API de la cuenta, devuelve un 401.
  • Guardo un hash, no el secreto. Es un HMAC con el ID de sesión como sal. El texto sin cifrar se devuelve una sola vez, con Cache-Control: no-store, y mis registros solo contienen ID y estados.
  • Un intento por sesión. Una segunda solicitud PUT devuelve un 409 y no puede sustituir el archivo. Si falla la primera, el agente solicita una sesión nueva.
  • El tamaño se declara y luego se comprueba. El servidor rechaza las extensiones no admitidas y los archivos demasiado grandes antes de emitir nada, cuenta cada byte que recibe, rechaza los cuerpos comprimidos, cortos o con datos adicionales y cancela la operación al cabo de 60 segundos.
  • Todo caduca. El secreto, a los 10 minutos; el archivo, al cabo de una hora si no se usa para ninguna traducción. Un proceso diario elimina lo que quede después de 24 horas.
  • Quien llama no decide dónde se guarda el archivo. La solicitud no incluye nombre de archivo, ruta ni espacio de trabajo. Genero la clave de almacenamiento dentro del prefijo del propio espacio de trabajo.
  • Solo se puede usar la subida con la misma credencial. Si otra conexión u otro espacio de trabajo intenta crear una traducción a partir de ella, se rechaza.
  • La revocación funciona. Si alguien elimina una conexión o sus permisos, sus secretos de subida sin usar dejan de funcionar.
  • Cuotas. 20 subidas pendientes por espacio de trabajo y 120 por hora, además de los límites de frecuencia habituales.
  • Subir no cuesta nada. Solo se gastan créditos al aprobar, así que nadie puede agotar un saldo a base de subidas.

No usé una URL de almacenamiento presignada nativa porque se puede reutilizar hasta que caduca, y yo quería limitarla a un solo intento y fijar un tamaño máximo. Por eso hay un pequeño Worker delante del bucket, que guarda ese estado en la base de datos.

Cada flecha corresponde a una operación compare-and-set en la base de datos, así que dos solicitudes no pueden completar la misma sesión y nunca se vuelve a un estado anterior.

Respuestas perdidas

La mayoría de los errores que tuve que prever eran solicitudes que se completaban sin que quien las había enviado llegara a saberlo.

Si el agente pierde la respuesta a su solicitud PUT, no vuelve a subir el archivo. Primero intenta ejecutar create_translation con el mismo upload_id. Si el archivo ha llegado, funciona. Si no, solicita una sesión nueva. No se puede sobrescribir un archivo anterior.

La creación de la traducción también es idempotente: la clave es upload_id, así que repetir la llamada devuelve la misma traducción. Lo probé y solo obtuve una. Además, repetirla nunca vuelve a planificar la traducción con instrucciones distintas. Para eso hay otra herramienta.

Y cuando el servidor no sabe qué ha ocurrido, nunca borra el archivo. Si es posible que se haya confirmado la transacción en la base de datos, los datos se conservan y el proceso de limpieza se ocupa de ellos más tarde.

Si quieres replicarlo

Lo mínimo que implementaría:

  1. Una herramienta que devuelve una URL de carga y las cabeceras que hay que usar, para que el modelo nunca vea los bytes.
  2. Un secreto de un solo uso, de corta duración, vinculado a una sesión y que no aparece en la URL.
  3. Un tamaño que impone el servidor y una ruta de almacenamiento que elige el servidor.
  4. Instrucciones en el resultado de la herramienta para los agentes que no pueden enviar un PUT.
  5. La creación se identifica con el ID de carga, para que sea seguro reintentarla.

La parte de MCP son unas pocas líneas. La mayor parte del trabajo estuvo en los casos de error.

Fuentes