TL;DR — Demande : « envoie-moi un message Telegram via le bot didou, via VFGate, via MCP, via le
modèle local, sans cloud. » L'audit a montré que trois maillons sur quatre existaient déjà : le
modèle local tournait, un serveur MCP vfg-gate était déjà branché dans Bionic, et le bot Telegram
était vivant côté production. Il manquait un outil d'envoi — et il s'est avéré que le MCP
portait déjà le jeton capable de l'autoriser. Un seul outil ajouté, 8 tests verts, image
rebâtie, message reçu. Aucun secret n'a jamais été lu sur le poste.
La demande, et le piège qu'elle cachait
Quatre « via » empilés, et un « no cloud ». Le réflexe : aller voir si la plomberie tient. J'ai mesuré les maillons un par un, sans rien supposer :
| Maillon | État mesuré |
|---|---|
| Modèle local | ✅ google/gemma-4-12b-qat chargé, API :1234 |
| MCP branché dans Bionic | ✅ un serveur vfg-gate déjà dans ~/.lmstudio/mcp.json |
| VFGate (port universel) | ✅ « ok », adaptateurs listés |
| Bot Telegram | ✅ vivant — getMe renvoie didoumaloubot |
| Outil MCP d'envoi | ❌ absent — le MCP ne savait faire qu'OSC et « wizz » |
Le piège était là : on croit qu'il faut « tout construire ». En réalité, un seul maillon manquait.
Le jeton, jamais lu
Le réflexe dangereux, sur un envoi de message, c'est de récupérer le jeton du bot pour le passer à la main. C'est exactement ce qu'il ne faut pas faire : il finit dans un fichier, un log, un commit.
J'ai donc cherché qui attend quel jeton, et j'ai comparé des empreintes, pas des valeurs :
- le MCP
vfg-gatereçoit dans son environnement un jetonGATE_TOKEN_VFGATE; - la route d'envoi de VFGate exige un en-tête dont la valeur est
VFGATE_SEND_TOKEN; - les deux empreintes SHA-256 étaient identiques.
Autrement dit : le serveur MCP savait déjà s'autoriser. Aucun secret neuf à créer, aucune valeur à transporter — le jeton vit dans l'environnement d'un conteneur, jamais sur le poste, jamais dans le code. (Un test d'envoi réel, exécuté sur le serveur, a confirmé la livraison : le message est arrivé, l'identifiant Telegram incrémenté.)
Un outil de vingt lignes, testé avant d'être déployé
Le MCP vfg-gate expose un petit catalogue d'outils dans un fichier gate-core.js. J'y ai ajouté
un outil, additif et rétro-compatible :
vfg_telegram_send { text, channel?, adapterId?, lot? }
Trois idées dedans :
- Repli ordonné : on essaie l'adaptateur demandé, puis le bot didou, puis un adaptateur de secours.
- Faits seulement : le message est du texte, jamais un secret, jamais un prompt brut.
- Erreurs muettes sur les valeurs : en cas d'échec, on ne journalise que le code HTTP, jamais l'en-tête ni l'URL complète.
Les tests sont écrits avant le déploiement, avec un fetch simulé : l'outil qui envoie vraiment,
l'outil qui refuse un texte vide, et le repli quand le premier adaptateur répond 401. 8/8 verts
côté fichier, 271/271 sur l'ensemble du paquet.
Déployer, puis figer — la nuance qui coûte cher
Premier réflexe : copier le fichier corrigé dans le conteneur qui tourne, redémarrer. Ça marche. Ça marche jusqu'au prochain redéploiement, qui repart de l'image et efface le correctif. Un hotswap est un pansement, pas un soin.
La bonne méthode — celle du dépôt — reconstruit l'image :
- on copie la source dans le dossier de build du serveur,
- on retague l'ancienne image (
…:pre-telegram-…) pour pouvoir revenir en arrière, - on reconstruit (
docker build), puis on recrée les services (docker compose up -d).
Après quoi le fichier dans le conteneur est bit pour bit identique à la source — comparaison de
sha256 à l'appui — et le bot Telegram, porté par un autre conteneur, est resté intact : la
reconstruction n'a touché que les trois services MCP.
Faire rédiger le message par le modèle local
L'outil envoyait. Restait le « via le modèle local ». Un court script a été livré, avec deux modes :
--direct: on passe un texte, aucun modèle n'est appelé — reproductible, zéro token.--model: le modèle local rédige le message, puis on l'envoie — un appel.
Deux leçons sont sorties de là, et elles sont outillées :
- La troncature : un modèle « qui réfléchit » peut épuiser son budget en raisonnement et rendre une
réponse coupée. Le script double le budget et réessaie une fois si la réponse est marquée
« longueur atteinte ». C'est la règle connue du local :
max_tokens ≥ 1500. - Le shell qui mange les chemins : sur Windows, un chemin passé en argument a perdu ses barres
obliques — un fichier s'est retrouvé nommé
DGitRepos…au lieu deD:\GitRepos\…. Correctif : on n'écrit plus le chemin en argument, on fait lire le contenu au script (--fileou un pipe). Le message est alors composé de plusieurs lignes propres, et la vitesse (tok/s) mesurée est jointe au message.
Un geste répétable ne s'arrête pas à « ça marche ». Il se demande : qu'est-ce qui casse la deuxième fois ? Ici : le redéploiement (image), la troncature (budget), le shell (quoting).
Ce que la leçon générale donne
Trois règles, réutilisables bien au-delà de Telegram :
- Mesurer les maillons avant de construire. Trois des quatre « via » existaient. On a ajouté un outil, pas une chaîne.
- Comparer des empreintes, pas des secrets. « Est-ce le même jeton ? » se répond par un
sha256. Un agent n'a jamais besoin de lire la valeur. - Figer, pas rustiner. Un correctif qui ne survit pas au redéploiement n'est pas un correctif.
Pour rejouer
# le bot vit côté production : on vérifie sa santé, pas ses secrets
curl http://<vps>:8083/api/vfgate/v1/health
# envoi direct (aucun modèle) — reproductible
node D:\GitRepos\vfg-dispatcher\scripts\mcp-telegram.mjs --direct "test" --to chat:<id>
# message rédigé par le modèle local, vitesse jointe
node D:\GitRepos\vfg-dispatcher\scripts\mcp-telegram.mjs --model "résume la session en 3 lignes"
La chaîne est courte, chaque maillon est un composant qui existait, et le seul secret en jeu n'a jamais quitté l'environnement d'un conteneur. C'est peut-être ça, la vraie livraison : un « je peux envoyer un message » qui ne dépend d'aucun cloud et ne confie aucun secret à celui qui écrit.