Un serveur MCP a envoyé une capture d'écran et votre modèle n'a vu qu'un chemin de fichier

Vous connectez un serveur MCP capable de capturer l’écran — le genre qui prend la capture lui-même et la renvoie comme résultat. Vous demandez à Hermes de regarder ce qui est à l’écran maintenant. Il se met docilement au travail puis rapporte : tout ce que je vois, c’est un chemin de fichier. Alors vous commencez à suspecter la config du serveur, puis les permissions du répertoire de cache, puis si le modèle prend en charge la vision tout court. Vous finissez par découvrir que l’image était là depuis le début — elle n’a simplement jamais franchi la couche des résultats d’outil pour arriver dans les mains du modèle. Deux changements fusionnés le 6 octobre ont bouché ce trou, et le correctif n’est pas « ouvrir une porte dérobée pour MCP » : c’est ramener quatre chemins à une seule décision.
Ce qui se passait avant
L’ImageContent d’un outil MCP est écrit dans le cache d’images de Hermes et renvoyé dans le résultat sous forme de ligne MEDIA:/path/to/image.png. C’est réellement utile en soi — toute surface capable de pièces jointes vous laisse extraire le fichier — mais l’image elle-même n’était jamais attachée au résultat d’outil. Cela produisait un état gênant :
- votre modèle principal peut voir les images (
supports_vision=True) ; - le serveur MCP a renvoyé une capture d’écran ;
- le modèle a reçu une chaîne de chemin, et vous a dit en toute vérité qu’il ne peut pas voir l’image.
Le cas le plus embarrassant est un serveur MCP cua-driver brut : son métier entier est de capturer l’écran pour vous, et il ne pouvait pas voir l’écran du tout. Le même problème se trouvait dans les captures computer_use, dont la route ne vérifiait que « le provider transporte-t-il bien des médias de résultat d’outil » — elle ne regardait jamais agent.image_input_mode, ni un verdict de catalogue indiquant que le modèle ne peut pas voir les images. Donc sur un modèle texte uniquement (le commit cite deepseek-v3.2 sur OpenRouter), la capture passait en natif, le filet de sécurité de l’agent remplaçait la capture par un message « basculez vers un modèle capable de vision », et le modèle de vision auxiliaire que vous aviez configuré ne tournait jamais. image_input_mode: text était aussi ignoré pour les captures d’écran.
Maintenant : un seul point de contrôle pour tout ça
Le correctif ramène la décision à une seule fonction : tools.vision_tools._native_tool_result_images(provider, model, cfg). Elle répond à une seule question — cette image peut-elle vraiment transmettre des pixels au modèle ? — et deux conditions doivent être réunies : le routage d’images se résout en native (décidé ensemble par agent.image_input_mode, un backend auxiliary.vision explicitement configuré, et le catalogue de modèles) et la route accepte les images à l’intérieur des résultats d’outil.
Sa docstring le dit simplement : « Un seul prédicat, pour que la voie ne dépende jamais de l’outil qui a produit l’image. » Quatre sources passent désormais par ce même point de contrôle :
| Source | Avant | Maintenant |
|---|---|---|
vision_analyze |
déjà correct | inchangé |
| Captures d’écran du navigateur | déjà correct | inchangé |
Captures computer_use |
ignorait image_input_mode, passait en natif même sur les modèles texte uniquement |
suit le point de contrôle ; les modèles texte uniquement reçoivent la description de vision auxiliaire |
MCP ImageContent |
chemin seulement | pixels attachés dès que la route peut les prendre en natif |
Pour le chemin MCP en particulier : l’image est prise depuis l’écriture en cache de cet appel lui-même (les chemins ne sont jamais extraits du texte du serveur), ré-encodée dans un format accepté par le provider et dimensionnée selon vision.embed_target_bytes (256 KiB par défaut, borné entre 64 KiB et 4 MiB) et un plafond de 1568 px, au plus 4 par résultat — parce que l’image est renvoyée à chaque requête suivante. Le redimensionnement s’exécute sur le thread de l’outil, hors de la boucle MCP partagée. La moitié texte conserve ses chemins MEDIA:, donc « donne-moi le fichier » fonctionne toujours, et les plugins qui appellent ctx.call_mcp reçoivent toujours une chaîne.
Trois modes, trois comportements
Avant de raisonner sur le point de contrôle, connaissez les trois valeurs de agent.image_input_mode, qui décident si les pixels entrent ou non dans l’historique de conversation :
agent:
image_input_mode: auto # auto (défaut) | native | text
auto(défaut) : natif quand le modèle rapportesupports_vision=Trueet que vous n’avez pas défini explicitementauxiliary.vision.provider; sinon la route texte.native: attache toujours l’image au résultat d’outil. Un modèle texte uniquement échoue alors soit chez le provider, soit via un repli texte de dernière chance — donc ne l’activez pas pour les modèles qui ne peuvent pas voir.text: pré-analyse toujours avecvision_analyzeet préfixe la description. L’intérêt de cette voie est que les pixels n’entrent jamais dans l’historique — le bon réglage quand vous ne voulez pas que les captures d’écran aillent à votre modèle principal, ou quand votre provider facture par image.
Une règle plus ancienne mérite d’être retenue : les alias de proxy que le catalogue ne connaît pas vont au modèle de vision auxiliaire sous auto. Pour les garder en natif, mettez native explicitement.
Une conclusion pratique : si vous faites tourner un serveur MCP de type capture d’écran et que votre modèle principal voit déjà les images, le auto par défaut fonctionnera désormais tout seul. Et si vous aviez basculé image_input_mode sur text pour contourner « le modèle ne voit pas l’image », vous pouvez le remettre sur auto pour qu’il obtienne les pixels — au prix de transporter cette image à chaque requête.
Deux détails de sécurité faciles à manquer
Les résultats MCP sont des entrées non fiables, donc ces deux points comptent :
- Les caractères TAG invisibles sont supprimés. La plage U+E0000–U+E007F ne s’affiche pas dans les terminaux et les interfaces de chat mais est parfaitement visible pour le modèle — le canal de contrebande classique pour l’injection de prompt depuis un serveur malveillant ou compromis. Hermes les retire des résultats d’outil, du contenu des ressources et des descriptions d’outils, tandis que les séquences de tag emoji légitimes (drapeaux régionaux comme 🏴) sont préservées.
- Le
_metadu fournisseur est exposé ; les clés réservées au protocole ne le sont pas. Quand un serveur attache un mapping_meta(espaces de noms de fournisseur commecom.example/handoff), il est transmis au modèle aux côtés du résultat. Les clés sous des préfixes réservés au protocole tels quemodelcontextprotocoloumcpsont écartées, et s’il ne reste rien de tourné vers le modèle, le champ_metaest entièrement omis.
Une courte liste d’auto-vérification
- Le modèle ne voit pas les images MCP → confirmez quelle voie prend cette route :
agent.image_input_modeest-il réglé surtext, etauxiliary.vision.providerest-il défini explicitement (ce qui force la description auxiliaire) ? - Il dit que l’image est trop petite pour être lue → augmentez
vision.embed_target_bytes(jusqu’à 4 MiB), en vous rappelant qu’elle est renvoyée à chaque tour et consomme du contexte dans les longues conversations. - Un résultat porte beaucoup d’images → au plus 4 sont attachées ; les autres restent sous forme de chemins
MEDIA:dans le texte, donc demandez celle dont vous avez besoin viavision_analyze. - Vous voulez savoir ce qui est à l’écran sans laisser la capture dans l’historique → réglez
image_input_mode: text.
Pour conclure
- MCP
ImageContentne rapportait auparavant qu’un chemin, donc même un modèle capable de vision ne lisait aucun pixel. tools.vision_tools._native_tool_result_imagesest désormais le point de contrôle unique, partagé parvision_analyze, les captures d’écran du navigateur, les capturescomputer_useet les images MCP.- Les trois valeurs d’
image_input_mode:autosuit la capacité du modèle,nativeattache toujours,textgarde les pixels hors de l’historique via le descripteur auxiliaire. - Les résultats MCP voient leurs caractères TAG invisibles supprimés et leurs clés
_metaréservées au protocole retenues.
Deux autres articles s’accordent bien avec celui-ci : les variables de contexte dans la config MCP pour aligner la config sur le contexte, et les frontières de sécurité du catalogue MCP pour durcir une fois que des serveurs tiers entrent en jeu.