---
title: "Tu MCP no acepta archivos binarios. Así puedes solucionarlo"
description: "Los argumentos de una herramienta son texto, así que no hay dónde enviar un PDF o un XLSX. La solución: el MCP devuelve una URL de subida de un solo uso y el agente envía allí el archivo mediante una petición HTTP normal. Así lo he implementado en mi SaaS, AI Glot, y así puedes protegerlo."
date: 2026-10-02
language: es
canonical: https://gduv.club/es/articles/mcp-file-upload
source: gduv.club
---
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.

| Step | What happens | Cost |
| :--- | :--- | ---: |
| Leer el archivo | Está en el disco, ocupa 2 MB | barato |
| Codificarlo en base64 | Los datos binarios se convierten en texto, un tercio más grande | ~2,7M de caracteres |
| El modelo escribe cada carácter | En una sola llamada a la herramienta. Un carácter incorrecto corrompe el archivo | todo el contenido |
| El servidor lo decodifica | Si la llamada cabe en el contexto | 1 llamada |
| **Lo que hace falta** |  | **más de lo que cabe en la mayoría de las ventanas de contexto** |

_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](/articles/mcp-vs-direct-apis), 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.

1. **Pedir una URL**. Llamada MCP: nombre y tamaño exacto del archivo
2. **Enviar los bytes con PUT**. HTTP normal, fuera de MCP
3. **Indicar el archivo**. Llamada MCP con el upload_id
4. **Aprobar**. El único paso que consume créditos

_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:

```json
{ "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:

```bash
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 secreto de subida** (Se 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 trabajo** (La 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

_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.

**El ciclo de vida de una sesión de subida, desde que se emite hasta que se consume, con el estado fallida como salida de la subida.**

  <div class="dg-chips">
    <span class="dg-chip">emitida</span>
    <span class="dg-chip dg-chip--mark">subiendo</span>
    <span class="dg-chip">subida</span>
    <span class="dg-chip">consumiendo</span>
    <span class="dg-chip dg-chip--good">consumida</span>
    <span class="dg-chip dg-chip--bad">fallida</span>
  </div>

## 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

- [AI Glot: crear una sesión de carga](https://ai-glot.com/docs/api/uploads/create)
- [AI Glot: enviar los bytes del archivo](https://ai-glot.com/docs/api/uploads/send)
- [Cloudflare R2: URL prefirmadas](https://developers.cloudflare.com/r2/api/s3/presigned-urls/)
- [Model Context Protocol: especificación de herramientas](https://modelcontextprotocol.io/specification/2025-06-18/server/tools)