Votre MCP ne peut pas recevoir de fichiers binaires : voici la solution
Mon SaaS AI Glot traduit des fichiers (CSV, XLSX, Word, PDF et quelques dizaines d’autres formats) et dispose d’un serveur MCP. La première version avait une lacune que je connaissais : l’agent pouvait m’envoyer un lien public vers un fichier ou un petit extrait de texte, mais il n’y avait aucun moyen d’envoyer un fichier stocké sur votre ordinateur.
J’ai corrigé ça la semaine dernière. L’astuce est simple, un peu bricolée, et je pense qu’elle fonctionne pour tout MCP qui doit recevoir un fichier. La voici.
Pourquoi un fichier ne peut pas être transmis dans un appel d’outil
Les arguments d’un outil MCP sont au format JSON. Pour y intégrer un fichier binaire, il faut l’encoder en base64, donc le convertir en texte. Le modèle doit alors générer lui-même ce texte, caractère par caractère, dans l’appel d’outil.
- Lire le fichierIl est sur le disque, 2 MBfaible
- L’encoder en base64Le binaire devient du texte, un tiers plus volumineux~2.7M caractères
- Le modèle écrit chaque caractèreDans un seul appel d’outil, et un caractère erroné corrompt le fichierle tout
- Le serveur le décodeSi l’appel tient dans la fenêtre de contexte1 appel
Ce qu’il fautplus que ne peuvent contenir la plupart des fenêtres
C’est le même problème que les 200 lignes CSV dans pourquoi j’ai cessé d’utiliser MCP pour la plupart de mes tâches, sauf qu’ici un seul appel suffit déjà à accomplir toute la tâche.
Et encore, à condition que l’agent puisse lire votre fichier. Connecter un serveur MCP ne donne pas à l’agent accès à vos pièces jointes ni à votre disque. C’est l’application qui l’entoure qui le permet.
L’astuce : le MCP fournit une URL, l’agent y envoie le fichier
L’agent n’a pas besoin de faire transiter le fichier par MCP. Il doit simplement savoir où l’envoyer. Le MCP renvoie donc une URL d’envoi, et l’agent transfère lui-même le fichier avec une requête HTTP classique.
- 1Demander une URLAppel MCP : nom du fichier et taille exacte
- 2Envoyer les octets avec PUTHTTP classique, en dehors de MCP
- 3Référencer le fichierAppel MCP avec l’upload_id
- 4ApprouverLa seule étape qui coûte des crédits
Dans AI Glot, le premier outil est create_upload_url. Il faut lui fournir le nom du fichier et sa taille exacte en octets :
{ "filename": "catalogue.xlsx", "size_bytes": 15961 }Il renvoie un upload_id, une URL, la méthode (PUT), des en-têtes temporaires, deux échéances et des instructions destinées à l’agent. Celui-ci exécute ensuite une commande du genre :
curl -X PUT "$UPLOAD_URL" \
-H "Authorization: $TEMP_UPLOAD_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @catalogue.xlsxEnsuite, il appelle create_translation en fournissant uniquement l’upload_id. Pas de nom de fichier, pas de contenu, pas de base64. Il récupère un plan, vous demande de le confirmer, puis approve_translation dépense les crédits.
Je l’ai testé en direct le jour de la sortie avec un XLSX de 15,961 octets contenant deux mots. Le PUT a renvoyé 200, le plan a compté deux mots et, après approbation, la traduction a coûté un crédit. La création de l’envoi et du plan n’a rien coûté.
L’autre méthode, et pourquoi elle ne suffit pas
AI Glot acceptait déjà un file_url : vous fournissez un lien et mon serveur télécharge le fichier lui-même. Cette méthode fonctionne bien quand le fichier est déjà en ligne.
Mais la plupart du temps, le fichier se trouve sur votre ordinateur. Pour utiliser file_url, il faudrait d’abord le téléverser quelque part, le rendre accessible au public, puis coller le lien. Ça fait beaucoup d’étapes à demander à quelqu’un qui veut simplement dire « traduis ce tableur ». Les petits fichiers texte peuvent toujours être transmis directement comme contenu, et c’est tout.
Il y a donc maintenant trois façons de fournir un fichier : le contenu texte, une URL publique et l’URL de téléversement pour un fichier qui n’existe que sur votre machine.
Quand l’agent ne peut pas envoyer de PUT
C’est là que se situe la limite. L’agent doit lire votre fichier et envoyer une requête HTTP. Claude Code ou Codex le peuvent, car ils disposent d’un shell. Une fenêtre de chat sans environnement de code isolé ne le peut pas, même si le MCP est connecté.
Le MCP doit donc l’indiquer partout où l’agent est susceptible de lire : dans la description de l’outil, le résultat de l’outil, la documentation de l’API et llms.txt. Les miens indiquent à l’agent que s’il ne peut pas envoyer la pièce jointe, il doit le dire clairement à l’utilisateur et proposer les autres solutions : l’application web, la CLI ou un lien public. Ils lui disent aussi de ne jamais prétendre qu’un fichier a été téléversé si ce n’est pas le cas, et de ne jamais valider un paiement sans fichier et forfait vérifiés.
Sans cela, un agent incapable d’envoyer la requête PUT échoue sans rien signaler, ou pire, fait croire que tout a fonctionné.
Comment sécuriser le système
Vous autorisez un client HTTP inconnu à écrire dans votre stockage : c’est donc cette partie qui m’a demandé le plus de travail. L’idée, c’est que le secret renvoyé ne permette de faire qu’une seule chose, et rien d’autre.
Le secret de téléversementRenvoyé une seule fois, avec l’URL
- Un seul PUT, pour une seule session
- Expire après dix minutes
- Ne permet ni de lire, ni de lister, ni de traduire quoi que ce soit
- Inutilisable comme identifiant d’API ou de MCP
Une clé API d’espace de travailCe que l’agent possède déjà
- Tout ce qu’autorisent ses portées
- Valide longtemps, jusqu’à sa révocation
- Reste entre les mains de l’agent
- N’est jamais envoyée au point de terminaison de téléversement
Voici ce que j’ai fait et ce que je referais sur n’importe quel serveur :
- Le secret se trouve dans un en-tête, jamais dans l’URL. Les URL se retrouvent dans les journaux et l’historique. L’URL seule ne donne accès à rien.
- Il ne fonctionne que pour une seule session. Il autorise un PUT sur le point de terminaison de cette session, et rien d’autre. J’ai vérifié : envoyé à l’API du compte, il renvoie une réponse 401.
- Je stocke une empreinte, pas le secret. C’est un HMAC salé avec l’identifiant de session. Le texte en clair est renvoyé une seule fois avec
Cache-Control: no-store, et mes journaux ne contiennent que des identifiants et des états. - Une seule tentative par session. Un deuxième PUT renvoie une réponse 409 et ne peut pas remplacer le fichier. Si la première tentative échoue, l’agent demande une nouvelle session.
- La taille est déclarée, puis vérifiée. Le serveur rejette les extensions non prises en charge et les fichiers trop volumineux avant d’émettre quoi que ce soit. Il compte chaque octet reçu, refuse les corps compressés, trop courts ou trop longs, et abandonne après 60 secondes.
- Tout expire. Le secret après 10 minutes, le fichier après une heure s’il n’est pas utilisé pour une traduction, et une tâche quotidienne supprime ce qui reste après 24 heures.
- L’appelant ne choisit pas la destination. Aucun nom de fichier, chemin ni espace de travail dans la requête. Je génère la clé de stockage dans le préfixe propre à cet espace de travail.
- Seul le même identifiant peut utiliser le téléversement. Si une autre connexion ou un autre espace de travail tente de créer une traduction à partir de ce fichier, la demande est refusée.
- La révocation fonctionne. Si quelqu’un supprime une connexion ou ses autorisations, les secrets de téléversement inutilisés associés cessent de fonctionner.
- Des quotas. 20 téléversements en attente par espace de travail et 120 par heure, en plus des limites de débit habituelles.
- Le téléversement ne coûte rien. Seule la validation consomme des crédits : personne ne peut donc vider un solde en téléversant des fichiers.
Je n’ai pas utilisé d’URL de stockage pré-signée standard, car elle peut être réutilisée jusqu’à son expiration, et je voulais limiter les tentatives à une seule et imposer une limite de taille. J’ai donc placé un petit Worker devant le compartiment de stockage, qui conserve cet état dans la base de données.
Réponses perdues
La plupart des bugs auxquels j’ai dû réfléchir concernaient des requêtes qui avaient abouti sans que l’appelant le sache.
Si l’agent perd la réponse à son PUT, il ne téléverse pas le fichier une deuxième fois. Il essaie d’abord create_translation avec le même upload_id. Si le fichier est arrivé, l’opération réussit. Sinon, il demande une nouvelle session. Impossible de remplacer un ancien fichier.
La création de la traduction est également idempotente : upload_id sert de clé, et répéter l’appel renvoie la même traduction. J’ai répété l’opération dans mon test et je n’en ai obtenu qu’une seule. Un nouvel appel ne relance jamais non plus la planification avec des instructions modifiées. Pour cela, il existe un outil distinct.
Et lorsque le serveur ne sait pas ce qui s’est passé, il ne supprime jamais le fichier. Si une transaction a pu être validée dans la base de données, les octets sont conservés et la tâche de nettoyage s’en occupe plus tard.
Si vous voulez reprendre cette approche
Voici le minimum que je mettrais en place :
- Un outil qui renvoie une URL d’envoi et les en-têtes à utiliser, pour que le modèle ne voie jamais les octets.
- Un secret à usage unique, à durée de vie courte, associé à une session et absent de l’URL.
- Une taille limite imposée par le serveur, et un chemin de stockage choisi par le serveur.
- Des instructions dans le résultat de l’outil pour les agents incapables d’envoyer une requête PUT.
- Une création associée à l’ID d’envoi, afin que les nouvelles tentatives soient sans risque.
La partie MCP tient en quelques lignes. L’essentiel du travail a consisté à gérer les cas d’échec.

