Datei-Upload und -Download in MCP-Tools

Ein MCP-Tool kann eine Datei entgegennehmen, die der Benutzer an eine Unterhaltung angehängt hat, und eine Datei zurückgeben, die der Benutzer herunterladen kann. QAnswer ist der MCP-Client: Es ruft Ihren Server über Streamable HTTP auf, und die Datei reist in den Argumenten und im Ergebnis des Tools. Ihr Server braucht keinen zusätzlichen Endpunkt und verwaltet nie einen Download-Link.

Die Beispiele stammen aus poc-file-mcp, einem kleinen FastMCP-Server mit einem einzigen Tool, uppercase_file, das eine Textdatei liest und eine Kopie in Großbuchstaben zurückgibt.

Der Datei-Umschlag

Eine Datei reist in beide Richtungen immer als dasselbe JSON-Objekt:

{
  "file_name": "hello.txt",
  "mime_type": "text/plain",
  "base64": "aGVsbG8gd29ybGQ="
}
  • file_name: der Dateiname mit Endung.
  • mime_type: der Medientyp, zum Beispiel text/plain.
  • base64: der Dateiinhalt, base64-kodiert.

Dieser Umschlag ist eine QAnswer-Konvention, die in gewöhnlichem MCP-Textinhalt übertragen wird. Er ist nicht Teil der MCP-Spezifikation.

Eine Datei entgegennehmen

Geben Sie dem Dateiparameter im Eingabeschema des Tools den Typ File. Das ist das Schema von uppercase_file:

{
  "type": "object",
  "properties": {
    "source_file": {
      "type": "File",
      "description": "The text file to upper-case."
    }
  },
  "required": [
    "source_file"
  ]
}

Um mehrere Dateien anzunehmen, deklarieren Sie ein Array von File-Elementen:

{
  "source_files": {
    "type": "array",
    "items": {
      "type": "File"
    },
    "description": "The files to merge."
  }
}

Was beim Aufruf des Tools passiert

  1. Das Modell sieht den Parameter als einfachen String oder als Array von Strings und füllt ihn mit dem Namen einer an die Unterhaltung angehängten Datei, zum Beispiel hello.txt.
  2. Bevor Ihr Tool aufgerufen wird, ersetzt QAnswer jeden Namen durch den Umschlag der Datei, als JSON-String serialisiert. Ein Array-Parameter erhält eine Liste dieser Strings in derselben Reihenfolge.
  3. Ihr Tool liest den Wert mit json.loads ein: Es erhält einen String, kein Objekt.

Für hello.txt mit dem Inhalt hello world erhält uppercase_file diese Argumente:

{
  "source_file": "{\"file_name\":\"hello.txt\",\"mime_type\":\"text/plain\",\"base64\":\"aGVsbG8gd29ybGQ=\"}"
}

Regeln

  • Nur der erste Parameter vom Typ File eines Tools wird gefüllt. Legen Sie alle Dateien in einen Parameter, bei Bedarf als Array.
  • Das Modell kann nur Dateien der aktuellen Unterhaltung auswählen: vom Benutzer angehängte Dokumente und Bilder sowie Dateien, die frühere Tool-Aufrufe zurückgegeben haben.
  • Passt ein Name zu keiner angehängten Datei, ist eine Datei größer als 25 MB oder werden mehr als 10 Dateien angegeben, wird Ihr Tool nicht aufgerufen. Das Modell erhält einen Fehler mit der Liste der angehängten Dateien und kann es mit einem korrekten Namen erneut versuchen.
  • QAnswer speichert das Eingabeschema jedes Tools, wenn die Tools des Servers synchronisiert werden. Synchronisieren Sie sie nach einer Schemaänderung erneut.

Eine Datei zurückgeben

Um eine Datei zurückzugeben, liefern Sie ihren Umschlag als Text eines einzigen Textinhaltsblocks, wie es uppercase_file tut. Mehrere Dateien kommen in eine Liste unter files oder attachments:

{
  "files": [
    {
      "file_name": "summary.csv",
      "mime_type": "text/csv",
      "base64": "…"
    },
    {
      "file_name": "chart.png",
      "mime_type": "image/png",
      "base64": "…"
    }
  ]
}

Jedes Tool kann Dateien zurückgeben, auch eines ohne File-Parameter, etwa ein Diagramm- oder Berichtsgenerator.

Was Benutzer und Modell sehen

  • Jede zurückgegebene Datei wird in der Unterhaltung gespeichert und als Dateikarte unter der Antwort des Assistenten angezeigt, wo der Benutzer sie in der Vorschau ansehen und herunterladen kann.
  • Die Datei wird über GET /api/conversation/{conversation_id}/files/{file_id} mit den üblichen Anmeldedaten des Benutzers ausgeliefert. Es gibt keinen öffentlichen Link.
  • Das Modell sieht den Dateiinhalt nie. QAnswer ersetzt das gesamte Tool-Ergebnis durch eine kurze Zusammenfassung, welche Dateien angehängt wurden und welche fehlgeschlagen sind; anderer Text im Ergebnis entfällt daher. Legen Sie alles, was der Benutzer braucht, in die Datei.
  • Eine Datei ohne file_name heißt processed-file, und ein Name ohne Endung erhält eine aus mime_type abgeleitete.
  • Zurückgegebene Dateien werden gespeichert, nicht indexiert: Sie sind nicht durchsuchbar wie die Dokumente, die der Benutzer hochlädt.
  • Als MCP-Bildinhalt zurückgegebene Bilder werden ebenfalls an die Unterhaltung angehängt, und das Modell sieht sie auch.
  • Eingebettete Ressourcen, Ressourcen-Links und Audioinhalte werden nicht in Dateien umgewandelt.
Den Umschlag als Text zurückgeben
Geben Sie genau einen Textblock zurück: Mehrere Blöcke werden zu einem String zusammengefügt, der kein gültiges JSON mehr ist. Geben Sie mit dem Dekorator @mcp.tool einen JSON-String zurück, kein Dict. FastMCP sendet ein zurückgegebenes Dict zusätzlich als strukturierten Inhalt, und die Datei würde zweimal angehängt.

Dieselbe Regel gilt mit dem Dekorator @mcp.tool. Dieses Tool nimmt keine Datei entgegen und gibt eine CSV-Datei zurück:

@mcp.tool
def export_report() -> str:
    """Exports the sales report as a CSV file."""
    data = b"region,total\nnorth,42\nsouth,17\n"
    return json.dumps(
        {
            "file_name": "report.csv",
            "mime_type": "text/csv",
            "base64": base64.b64encode(data).decode("ascii"),
        }
    )

Beispiel: uppercase_file

Der vollständige poc-file-mcp-Server. Er benötigt nur fastmcp 3.2:

import base64
import json

from fastmcp import FastMCP
from fastmcp.tools import Tool, ToolResult
from mcp.types import TextContent


class UppercaseFileTool(Tool):
    """Reads a text file and returns a copy of it, upper-cased."""

    def __init__(self) -> None:
        super().__init__(
            name="uppercase_file",
            description=(
                "Reads a text file and returns a copy of it with its contents "
                "upper-cased."
            ),
            parameters={
                "type": "object",
                "properties": {
                    "source_file": {
                        "type": "File",
                        "description": "The text file to upper-case.",
                    }
                },
                "required": ["source_file"],
            },
        )

    async def run(self, arguments: dict) -> ToolResult:
        # QAnswer has replaced the file name with the envelope, as a JSON string
        envelope = json.loads(arguments["source_file"])
        raw = base64.b64decode(envelope["base64"])
        upper_text = raw.decode("utf-8").upper()

        # hand the new file back as one text block holding its envelope
        result_envelope = {
            "file_name": f"UPPER_{envelope['file_name']}",
            "mime_type": envelope.get("mime_type") or "text/plain",
            "base64": base64.b64encode(upper_text.encode("utf-8")).decode("ascii"),
        }
        return ToolResult(
            content=[TextContent(type="text", text=json.dumps(result_envelope))]
        )


mcp = FastMCP("poc-file-mcp")
mcp.add_tool(UppercaseFileTool())


if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8811, path="/mcp")

Installieren Sie FastMCP und starten Sie den Server. Er stellt Streamable HTTP unter http://localhost:8811/mcp bereit:

pip install "fastmcp==3.2.0"
python server.py

Grenzen

  • 25 MB pro Datei, in jede Richtung. Base64 macht die Nutzlast etwa ein Drittel größer.
  • 10 Dateien pro Array-Parameter.
  • Nur Dateien der aktuellen Unterhaltung können an ein Tool übergeben werden.
  • Kein Medientyp wird abgelehnt. Eine Datei ohne Typ wird als application/octet-stream behandelt.
  • Der Low-Level-Server des MCP-Python-SDK prüft Argumente standardmäßig gegen das Eingabeschema und lehnt jeden Aufruf ab, weil File kein JSON-Schema-Typ ist. Registrieren Sie seinen Handler mit call_tool(validate_input=False). FastMCP prüft standardmäßig nicht gegen das Schema.