Guillaume Duvernay

Dein MCP kann keine Binärdateien annehmen? So löst du das Problem

MCPagentssecurityAI Glot

Mein SaaS AI Glot übersetzt Dateien (CSV, XLSX, Word, PDF und einige Dutzend weitere Formate) und hat einen MCP-Server. In der ersten Version gab es eine Lücke, die mir bekannt war: Der Agent konnte mir einen öffentlichen Link zu einer Datei oder einen kurzen Text schicken, aber Dateien auf deinem Computer ließen sich nicht übermitteln.

Letzte Woche habe ich das behoben. Der Kniff ist einfach, ein bisschen hacky, und ich denke, er funktioniert für jedes MCP, das Dateien empfangen muss. Also, hier ist er.

Warum eine Datei nicht in einen Tool-Aufruf passt

Die Argumente eines MCP-Tools sind JSON. Um eine Binärdatei darin unterzubringen, musst du sie als Base64 codieren, also in Text umwandeln. Anschließend muss das Modell diesen Text selbst Zeichen für Zeichen in den Tool-Aufruf schreiben.

Die 2.7 Millionen beziehen sich auf die Base64-Zeichen selbst: vier Zeichen für je drei Bytes. Ich habe sie nicht in Tokens umgerechnet, da das vom Modell abhängt.

Dasselbe Problem wie bei den 200 CSV-Zeilen in warum ich MCP für den Großteil meiner Arbeit nicht mehr nutze, nur ist hier ein einziger Aufruf bereits die ganze Aufgabe.

Und das setzt voraus, dass der Agent deine Datei überhaupt lesen kann. Ein MCP-Server gewährt dem Agenten keinen Zugriff auf deine Anhänge oder deine Festplatte. Das hängt von der App ab, in der er läuft.

Der Kniff: Das MCP liefert eine URL, der Agent sendet die Datei dorthin

Der Agent muss die Datei nicht über MCP senden. Er muss nur wissen, wohin er sie schicken soll. Deshalb gibt das MCP eine Upload-URL zurück, und der Agent überträgt die Datei selbst mit einem normalen HTTP-Request.

Das Modell schreibt nur kleines JSON. Die Datei geht direkt von deinem Rechner in den Speicher, und das Modell liest sie nie.

In AI Glot heißt das erste Tool create_upload_url. Du gibst den Dateinamen und die exakte Größe in Bytes an:

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

Es gibt eine upload_id, eine URL, die Methode (PUT), temporäre Header, zwei Fristen und Anweisungen für den Agenten zurück. Anschließend führt der Agent etwa Folgendes aus:

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

Danach ruft er create_translation nur mit der upload_id auf. Kein Dateiname, kein Inhalt, kein Base64. Er erhält einen Plan, bittet dich um Bestätigung, und mit approve_translation werden die Credits abgezogen.

Am Tag der Veröffentlichung habe ich es live mit einer 15,961 Byte großen XLSX-Datei getestet, die zwei Wörter enthielt. Der PUT-Aufruf lieferte 200 zurück, der Plan zählte zwei Wörter und nach der Freigabe kostete die Übersetzung einen Credit. Das Erstellen des Uploads und des Plans war kostenlos.

Der andere Weg und warum er nicht ausreicht

AI Glot akzeptierte bereits eine file_url: Du gibst einen Link an, und mein Server lädt die Datei selbst herunter. Das funktioniert gut, wenn die Datei schon online ist.

Meistens liegt die Datei aber auf deinem Computer. Um file_url zu verwenden, müsstest du sie erst irgendwo hochladen, öffentlich zugänglich machen und den Link einfügen. Das sind ganz schön viele Schritte für jemanden, der einfach nur sagen möchte: „Übersetze diese Tabelle.“ Kleine Textdateien können weiterhin direkt als Inhalt übermittelt werden, und das war’s.

Damit gibt es jetzt drei Möglichkeiten: Textinhalt, eine öffentliche URL und die Upload-URL für eine Datei, die nur auf deinem Rechner liegt.

Wenn der Agent kein PUT senden kann

Hier liegt die Grenze. Der Agent muss deine Datei lesen und eine HTTP-Anfrage senden können. Claude Code oder Codex können das, weil sie Zugriff auf eine Shell haben. Ein Chatfenster ohne Code-Sandbox kann es nicht, selbst wenn die MCP-Verbindung steht.

Deshalb muss der MCP es überall klar sagen, wo der Agent die Information lesen kann: in der Toolbeschreibung, im Tool-Ergebnis, in der API-Dokumentation und in llms.txt. Meine Hinweise sagen dem Agenten: Wenn er den Anhang nicht senden kann, soll er das dem Nutzer klar mitteilen und andere Wege vorschlagen: die Web-App, die CLI oder einen öffentlichen Link. Außerdem weisen sie ihn an, niemals zu behaupten, eine Datei sei hochgeladen worden, wenn das nicht stimmt, und niemals eine Zahlung ohne verifizierte Datei und Tarif freizugeben.

Ohne diese Funktion scheitert ein Agent, der den PUT nicht senden kann, einfach stillschweigend oder behauptet im schlimmsten Fall, es hätte funktioniert.

So sicherst du es ab

Du lässt einen unbekannten HTTP-Client in deinen Speicher schreiben. Mit diesem Teil habe ich mich deshalb am längsten beschäftigt. Die Idee: Das zurückgegebene Geheimnis kann genau eine Sache tun und sonst nichts.

Der Agent hat beide, sie werden aber nie ausgetauscht. Die Datei wird mit dem schwachen Schlüssel übertragen, die Anweisungen mit dem starken.

Was ich umgesetzt habe und auf jedem Server wieder so machen würde:

  • Das Geheimnis steht in einem Header, niemals in der URL. URLs landen in Protokollen und im Verlauf. Die URL allein gewährt keinen Zugriff.
  • Es gilt nur für eine Sitzung. Es erlaubt ein PUT an den Endpunkt dieser Sitzung und sonst nichts. Ich habe es geprüft: An die Account-API gesendet, liefert es einen 401-Fehler zurück.
  • Ich speichere einen Hash, nicht das Geheimnis. Es ist ein HMAC mit der Sitzungs-ID als Salt. Der Klartext wird einmalig mit Cache-Control: no-store zurückgegeben, in meinen Protokollen stehen nur IDs und Statusangaben.
  • Ein Versuch pro Sitzung. Ein zweites PUT liefert einen 409-Fehler zurück und kann die Datei nicht ersetzen. Schlägt der erste Versuch fehl, fordert der Agent eine neue Sitzung an.
  • Die Größe wird angegeben und anschließend überprüft. Der Server lehnt nicht unterstützte Dateiendungen und zu große Dateien ab, bevor er etwas ausgibt. Er zählt jedes empfangene Byte, weist komprimierte, zu kurze oder zu lange Inhalte zurück und bricht nach 60 Sekunden ab.
  • Alles läuft ab. Das Geheimnis nach 10 Minuten, die Datei nach einer Stunde, wenn sie nicht für eine Übersetzung verwendet wird. Ein täglicher Job löscht, was nach 24 Stunden noch übrig ist.
  • Der Aufrufer bestimmt nicht, wo die Datei landet. In der Anfrage gibt es weder Dateiname noch Pfad noch Workspace. Den Speicherschlüssel erzeuge ich innerhalb des Präfixes des jeweiligen Workspaces.
  • Für den Upload ist nur dieselbe Zugangsdatenverbindung zulässig. Versucht eine andere Verbindung oder ein anderer Workspace, damit eine Übersetzung zu erstellen, wird der Vorgang abgelehnt.
  • Ein Widerruf greift. Wird eine Verbindung oder ihre Berechtigung entfernt, funktionieren ihre ungenutzten Upload-Geheimnisse nicht mehr.
  • Kontingente. 20 ausstehende Uploads pro Workspace und 120 pro Stunde, zusätzlich zu den normalen Ratenlimits.
  • Der Upload kostet nichts. Credits werden erst bei der Freigabe abgezogen, damit niemand durch Uploads ein Guthaben aufbrauchen kann.

Ich habe keine native, vorab signierte Speicher-URL verwendet, weil sie bis zum Ablauf wiederverwendet werden kann. Ich wollte genau einen Versuch mit einer Größenbegrenzung ermöglichen. Deshalb sitzt vor dem Bucket ein kleiner Worker, der diesen Status in der Datenbank verwaltet.

Jeder Pfeil entspricht einem Compare-and-Set in der Datenbank. So können nicht zwei Anfragen dieselbe Sitzung für sich beanspruchen, und kein Zustand führt zurück zu einem früheren.

Verlorene Antworten

Die meisten Fehler, über die ich mir Gedanken machen musste, betrafen Anfragen, die erfolgreich waren, ohne dass der Aufrufer davon erfuhr.

Geht die Antwort auf das PUT des Agenten verloren, lädt er die Datei nicht erneut hoch. Stattdessen versucht er zuerst, mit derselben upload_id create_translation aufzurufen. Ist die Datei angekommen, funktioniert das. Andernfalls fordert er eine neue Sitzung an. Eine alte Datei lässt sich nicht überschreiben.

Auch das Erstellen der Übersetzung ist idempotent: Die upload_id dient als Schlüssel, daher liefert ein wiederholter Aufruf dieselbe Übersetzung zurück. Ich habe es in meinem Test wiederholt und genau eine erhalten. Bei einer Wiederholung wird auch mit geänderten Anweisungen kein neuer Plan erstellt. Dafür gibt es ein separates Tool.

Und wenn der Server nicht weiß, was passiert ist, löscht er die Datei niemals. Falls ein Datenbank-Commit möglicherweise durchgegangen ist, bleiben die Bytes erhalten, bis der Bereinigungsjob sich später darum kümmert.

Wenn du es übernehmen möchtest

Das Mindeste, was ich umsetzen würde:

  1. Ein Tool, das eine Upload-URL und die zu verwendenden Header zurückgibt, damit das Modell die Dateibytes nie zu sehen bekommt.
  2. Ein einmal verwendbares, kurzlebiges Geheimnis, das an eine Sitzung gebunden ist und nicht in der URL steht.
  3. Eine Größe, die der Server durchsetzt, und ein Speicherpfad, den der Server festlegt.
  4. Anweisungen im Tool-Ergebnis für Agents, die kein PUT senden können.
  5. Die Erstellung wird über die Upload-ID eindeutig festgelegt, damit Wiederholungsversuche sicher sind.

Die MCP-Seite umfasst nur wenige Zeilen. Die meiste Arbeit steckte in den Fehlerfällen.

Quellen