Envoi et téléchargement de fichiers dans les outils MCP
Un outil MCP peut recevoir un fichier que l'utilisateur a joint à une conversation, et renvoyer un fichier que l'utilisateur peut télécharger. QAnswer est le client MCP : il appelle votre serveur en streamable HTTP, et le fichier voyage dans les arguments et le résultat de l'outil. Votre serveur n'a besoin d'aucun point d'accès supplémentaire et ne gère jamais de lien de téléchargement.
Les exemples proviennent de poc-file-mcp, un petit serveur FastMCP doté d'un seul outil, uppercase_file, qui lit un fichier texte et en renvoie une copie en majuscules.
L'enveloppe de fichier
Un fichier voyage toujours sous la forme du même objet JSON, dans les deux sens :
{
"file_name": "hello.txt",
"mime_type": "text/plain",
"base64": "aGVsbG8gd29ybGQ="
}file_name: le nom du fichier, avec son extension.mime_type: le type de média, par exempletext/plain.base64: le contenu du fichier, encodé en base64.
Cette enveloppe est une convention de QAnswer transportée dans du contenu texte MCP ordinaire. Elle ne fait pas partie de la spécification MCP.
Recevoir un fichier
Dans le schéma d'entrée de l'outil, donnez au paramètre de fichier le type File. Voici le schéma de uppercase_file :
{
"type": "object",
"properties": {
"source_file": {
"type": "File",
"description": "The text file to upper-case."
}
},
"required": [
"source_file"
]
}Pour accepter plusieurs fichiers, déclarez un tableau d'éléments File :
{
"source_files": {
"type": "array",
"items": {
"type": "File"
},
"description": "The files to merge."
}
}Ce qui se passe lors de l'appel de l'outil
- Le modèle voit le paramètre comme une simple chaîne, ou un tableau de chaînes, et le remplit avec le nom d'un fichier joint à la conversation, par exemple
hello.txt. - Avant d'appeler votre outil, QAnswer remplace chaque nom par l'enveloppe du fichier, sérialisée en chaîne JSON. Un paramètre tableau reçoit une liste de ces chaînes, dans le même ordre.
- Votre outil analyse la valeur avec
json.loads: il reçoit une chaîne, pas un objet.
Pour hello.txt, qui contient hello world, uppercase_file reçoit ces arguments :
{
"source_file": "{\"file_name\":\"hello.txt\",\"mime_type\":\"text/plain\",\"base64\":\"aGVsbG8gd29ybGQ=\"}"
}Règles
- Seul le premier paramètre de type
Filed'un outil est rempli. Placez tous les fichiers dans un seul paramètre, sous forme de tableau si besoin. - Le modèle ne peut choisir que des fichiers de la conversation en cours : les documents et images joints par l'utilisateur, et les fichiers renvoyés par des appels d'outils précédents.
- Si un nom ne correspond à aucun fichier joint, si un fichier dépasse 25 Mo ou si plus de 10 fichiers sont donnés, votre outil n'est pas appelé. Le modèle reçoit une erreur listant les fichiers joints et peut réessayer avec un nom correct.
- QAnswer enregistre le schéma d'entrée de chaque outil lors de la synchronisation des outils du serveur. Synchronisez-les à nouveau après avoir modifié un schéma.
Renvoyer un fichier
Pour renvoyer un fichier, retournez son enveloppe comme texte d'un unique bloc de contenu texte, comme le fait uppercase_file. Plusieurs fichiers vont dans une liste sous files, ou attachments :
{
"files": [
{
"file_name": "summary.csv",
"mime_type": "text/csv",
"base64": "…"
},
{
"file_name": "chart.png",
"mime_type": "image/png",
"base64": "…"
}
]
}Tout outil peut renvoyer des fichiers, y compris un outil sans paramètre File, comme un générateur de graphiques ou de rapports.
Ce que voient l'utilisateur et le modèle
- Chaque fichier renvoyé est enregistré dans la conversation et affiché sous forme de carte de fichier sous la réponse de l'assistant, où l'utilisateur peut le prévisualiser et le télécharger.
- Le fichier est servi par
GET /api/conversation/{conversation_id}/files/{file_id}, avec les identifiants habituels de l'utilisateur. Il n'existe aucun lien public. - Le modèle ne voit jamais le contenu du fichier. QAnswer remplace tout le résultat de l'outil par un court résumé des fichiers joints et de ceux en échec ; tout autre texte du résultat est donc ignoré. Mettez dans le fichier tout ce dont l'utilisateur a besoin.
- Un fichier sans
file_nameest nomméprocessed-file, et un nom sans extension en reçoit une déduite demime_type. - Les fichiers renvoyés sont stockés, pas indexés : on ne peut pas y faire de recherche comme dans les documents envoyés par l'utilisateur.
- Les images renvoyées sous forme de contenu image MCP sont également jointes à la conversation, et le modèle les voit aussi.
- Les ressources intégrées, les liens de ressource et le contenu audio ne sont pas transformés en fichiers.
@mcp.tool, renvoyez une chaîne JSON, pas un dict. FastMCP envoie aussi un dict renvoyé comme contenu structuré, et le fichier serait joint deux fois.La même règle s'applique avec le décorateur @mcp.tool. Cet outil ne prend aucun fichier et renvoie un fichier CSV :
@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"),
}
)Exemple : uppercase_file
Le serveur poc-file-mcp complet. Il ne nécessite que 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")Installez FastMCP et démarrez le serveur. Il sert le streamable HTTP sur http://localhost:8811/mcp :
pip install "fastmcp==3.2.0"
python server.pyLimites
- 25 Mo par fichier, dans chaque sens. Le base64 rend la charge utile environ un tiers plus lourde.
- 10 fichiers par paramètre tableau.
- Seuls les fichiers de la conversation en cours peuvent être transmis à un outil.
- Aucun type de média n'est refusé. Un fichier sans type est traité comme
application/octet-stream. - Le serveur bas niveau du SDK Python MCP vérifie par défaut les arguments par rapport au schéma d'entrée et rejette chaque appel, car
Filen'est pas un type JSON Schema. Enregistrez son gestionnaire aveccall_tool(validate_input=False). FastMCP ne vérifie pas le schéma par défaut.