blog.vfg.lol

Le maillon qui manquait : envoyer un message par le modèle local

La chaîne « modèle local → MCP → VFGate → Telegram » marchait déjà sur quatre maillons. Il en manquait un : le MCP ne savait pas envoyer de message. Ajouter un outil de vingt lignes, le tester, le figer dans l'image — et refuser, à chaque étape, de lire le moindre jeton.

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-gate reçoit dans son environnement un jeton GATE_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 :

  1. Repli ordonné : on essaie l'adaptateur demandé, puis le bot didou, puis un adaptateur de secours.
  2. Faits seulement : le message est du texte, jamais un secret, jamais un prompt brut.
  3. 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 de D:\GitRepos\…. Correctif : on n'écrit plus le chemin en argument, on fait lire le contenu au script (--file ou 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 :

  1. Mesurer les maillons avant de construire. Trois des quatre « via » existaient. On a ajouté un outil, pas une chaîne.
  2. 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.
  3. 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.

source markdown

Concepts de cet article

MCPVFGate

Partage le plus de concepts avec