Tu MCP no acepta archivos binarios. Así puedes solucionarlo
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.
- Leer el archivoEstá en el disco, ocupa 2 MBbarato
- Codificarlo en base64Los datos binarios se convierten en texto, un tercio más grande~2,7M de caracteres
- El modelo escribe cada carácterEn una sola llamada a la herramienta. Un carácter incorrecto corrompe el archivotodo el contenido
- El servidor lo decodificaSi la llamada cabe en el contexto1 llamada
Lo que hace faltamás de lo que cabe en la mayoría de las ventanas de contexto
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.
- 1Pedir una URLLlamada MCP: nombre y tamaño exacto del archivo
- 2Enviar los bytes con PUTHTTP normal, fuera de MCP
- 3Indicar el archivoLlamada MCP con el upload_id
- 4AprobarEl único paso que consume créditos
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.xlsxDespué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 secreto de subidaSe devuelve una sola vez, junto con la URL
- Una solicitud PUT en una sola sesión
- Caduca a los diez minutos
- No permite leer, listar ni traducir nada
- No sirve como credencial de API ni de MCP
Una clave de API del espacio de trabajoLa que ya tiene el agente
- Todo lo que permiten sus ámbitos
- Dura hasta que alguien la revoque
- Permanece en manos del agente
- Nunca se envía al endpoint de subida
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.
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:
- Una herramienta que devuelve una URL de carga y las cabeceras que hay que usar, para que el modelo nunca vea los bytes.
- Un secreto de un solo uso, de corta duración, vinculado a una sesión y que no aparece en la URL.
- Un tamaño que impone el servidor y una ruta de almacenamiento que elige el servidor.
- Instrucciones en el resultado de la herramienta para los agentes que no pueden enviar un PUT.
- 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.

