Un serveur MCP pour mapper sous Hammer, vérifié par oracle

hammer-mcp, mon serveur MCP open source pour les maps Source : comment j'ai rendu vérifiables les chiffres qu'il donne à un agent de code.

Open source — hammer-mcpMon rôle : Auteur

Sommaire · 5 sections

En juillet 2026, des créateurs de map Garry’s Mod m’ont demandé s’il était possible de mapper sous Hammer avec un agent de code. J’ai cherché sur GitHub et sur le web, et à l’époque je n’ai rien trouvé qui lise un .vmf, lance les compilateurs et rende à un agent des chiffres sur lesquels il puisse s’appuyer. C’est de là qu’est parti hammer-mcp, un serveur MCP open source pour le travail de map sur le moteur Source. Le 2 août 2026, avant d’écrire le moindre outil, je vérifiais d’abord que les compilateurs de Garry’s Mod acceptaient de tourner sous Wine.

Le problème : un agent ne doute jamais d’un chiffre

Un agent de code qui travaille sur une map ne voit pas la map. Il voit ce que les outils lui renvoient : un pourcentage de remplissage, un sealed: true, un clean: true. Si ce chiffre est faux, il ne le soupçonne pas, il construit dessus. Le CONTRIBUTING.md du dépôt le pose en une phrase : un outil qui renvoie un mauvais chiffre est pire qu’un outil qui n’existe pas, parce que l’appelant n’a aucun moyen de s’en douter.

Le format Source rend ce piège facile. Un .bsp compilé contient 64 lumps, chacun avec un plafond tiré des en-têtes du SDK 2013, et c’est contre ces plafonds que l’outil calculait ses pourcentages. Le truc, c’est que les compilateurs réellement utilisés ne sont pas ceux du SDK 2013. Sur rp_nycity_day, une map de 1,13 Go qui se charge tous les jours sur un serveur GMod RP en production, le lump MODELS contient 1218 entrées pour un plafond officiel de 1024, soit 119 %. Un outil naïf afficherait une erreur sur une map qui marche. Celui-ci dit autre chose : la map tourne, donc les compilateurs qui l’ont produite relèvent ce plafond.

Si tu branches un agent sur tes propres outils, le problème est le même avec un autre décor : une limite atteinte doit se lire comme une limite, jamais comme un succès. J’en ai fait un article à part pour le budget de tokens.

L’approche : rien n’est cru avant d’avoir été vu faux

Le dépôt tient sur une règle que j’appelle l’oracle : pour chaque chiffre qu’un outil renvoie, il faut un moyen indépendant de le vérifier, sinon l’outil ne sort pas. Concrètement, ça s’est fait dans cet ordre.

Ce que l'oracle a demandéDu premier test de faisabilité à la première map construite par un agentProuver que les compilateurs tournent avant d’écrire un outilSaboter chaque test avant de le croireMesurer contre le plafond que le compilateur applique vraimentFaire construire une vraie map par un agent sans contexte

Prouver que les compilateurs tournent avant d’écrire un outil

Le premier « gate » date du 2 août 2026. Les compilateurs vbsp, vvis et vrad de Garry’s Mod (build du 22 juillet 2026) tournent sous Wine 9.0, à condition de forcer le répertoire de travail sur bin pour que tier0.dll soit trouvée. La compilation complète d’une map sonde (une pièce scellée, un info_player_start, une lumière, un info_target) sort en code 0 à chaque étape, et vvis donne 4 portails et 16 clusters visibles. Le .bsp de 56 236 octets est depuis une fixture du dépôt.

Ce gate a surtout trouvé un piège qui aurait faussé tout le reste : le chemin passé aux compilateurs doit être un chemin Windows absolu (Z:\...). Avec un chemin relatif, Wine le résout depuis son propre répertoire et compile en silence le mauvais fichier. Sans ce test préalable, chaque outil construit au-dessus aurait rendu des mesures exactes sur une autre map que celle demandée.

Saboter chaque test avant de le croire

edit_vmf modifie un .vmf par épissure : il remplace des plages d’octets et ne touche pas au reste. Il avait quatre tests qui vérifiaient l’égalité des octets après une modification. Pour voir s’ils servaient à quelque chose, j’ai remplacé l’épissure par une resérialisation complète du fichier, un sabotage total, et aucun des quatre n’a échoué. Les fixtures étaient du Hammer canonique, que le formateur reproduit à l’identique, donc les tests passaient contre une implémentation qui avait jeté toute la technique.

Ce qu’une resérialisation perd vraiment, ce sont les commentaires //, les lignes vides et l’indentation qui ne suit pas une tabulation par niveau. Les éditeurs tiers et les maps retouchées à la main en sont pleins, et un mapper aurait perdu ses annotations sans aucun message. Depuis, chaque test est saboté, regardé passer au rouge, puis remis, et le commit dit ce qui a été cassé. Le modèle de PR du dépôt a une section obligatoire pour ça, « The negative control ». Même logique que pour une RLS contournée par le rôle : un test de sécurité qu’on n’a jamais vu échouer ne protège rien.

Le skip est l’autre moitié du piège. La suite pilote de vrais compilateurs, un vrai sidecar Python et de vraies maps, dont aucun n’est livré avec le dépôt, donc un test qui ne trouve pas ce qu’il lui faut se met en skip. Un it.skipIf trop large ressemble alors exactement à un succès. Tous les prédicats de disponibilité vivent dans test/support/env.ts, un run annonce en clair ce qu’il n’a pas pu tester, et la CI refuse un run vert où trop peu de tests ont tourné. Un contributeur externe l’a vérifié sans le vouloir en apportant le support Windows natif, avec sa propre table de contrôles négatifs : des tests jusque-là skippés se sont mis à tourner sur Windows, certains échouent, et c’est suivi dans une issue ouverte. Le skip masquait ces échecs.

Mesurer contre le plafond que le compilateur applique vraiment

Le 15 août 2026, j’ai fabriqué des maps exprès pour dépasser les plafonds : une coque scellée remplie d’un bloc de cellules adjacentes de 32 unités. Le résultat contredit la table que l’outil utilisait pour tous ses pourcentages.

LimiteSDK 2013vbsp de Garry’s Modvbsp Hammer++
MAX_MAP_BRUSHES8192non appliquée, 27 006 écritsnon appliquée, 17 582 écrits
MAX_MAP_BRUSHSIDES65 536non appliquée, 162 036 écritsappliquée à 131 072
MAX_MAP_VERTS65 536appliquée à 65 536non atteinte par ces sondes

Sur l’audit d’une map de serveur RP, cette table m’avait fait tirer deux conclusions fausses d’affilée. D’abord BRUSHES à 8192 sur 8192, lu comme un mur. Ensuite BRUSHSIDES à 89 %, désigné comme le chiffre à surveiller, alors que ses 58 389 faces représentent 45 % du plafond que Hammer++ défend vraiment. L’outil avertissait déjà sur chaque dépassement, mais un 89 % reste « presque plein » quelle que soit la prose autour. Le dépôt dit maintenant que ces pourcentages sont un signal relatif, pas une distance à un mur.

La même mesure a trouvé un second mensonge. La première sonde, faite de boîtes isolées, a échoué sur les sommets à 9006 boîtes : vbsp s’est arrêté avec le code de sortie 1 sur Too many unique verts, max = 65536, et run_compile a déclaré cette étape clean: true, sans aucune trouvaille. Un agent qui lit l’étape en échec y lit une compilation propre.

Faire construire une vraie map par un agent sans contexte

Le 13 août 2026, le dépôt avait 70 outils, un tableau de parité avec Hammer et une liste « prouvé » presque entièrement verte, mais personne n’avait construit de map avec : la seule produite était la sonde de six brushes. Alors j’ai fait construire une petite épicerie, hmcp_bodega, par un agent sans contexte, avec le serveur et ses skills tels quels. Le .vmf n’est jamais édité à la main, un journal de friction est tenu au moment où ça coince, et « fini » veut dire trois verts : read_vmf_leak scellé, check_vmf_rules sans violation, run_compile sans erreur. Le journal d’audit du serveur donne l’autre moitié, celle qui n’est pas une opinion : appels répétés à l’identique, outils jamais appelés, taux d’échec par outil.

La première trouvaille est arrivée avant la première brique. Le serveur servait un build plus vieux de douze outils que le code source, sans rien signaler : aucune erreur, aucun avertissement, juste des outils en moins. health dit maintenant combien d’outils il sert et si son build est périmé. Sur le premier tour, le journal d’audit compte 53 appels, dont 12 à read_vmf_rooms et 9 identiques : l’agent reposait la même question parce que la réponse ne disait jamais pourquoi deux pièces avaient fusionné. D’après son propre journal, une ligne d’explication aurait ramené quatre heures à un appel.

L’épicerie a été reconstruite sur trois tours avec le même brief. Les appels sont passés de 53 à 51 puis 41, les échecs journalisés de 2 à 1 puis 0, pour vingt trouvailles dont seize corrigées. Une maison avec son jardin, un immeuble de deux étages et une citerne ronde ont suivi. Sur la citerne, read_vmf_leak a dit « scellé » et vbsp a dit « fuite » : une lentille de quelques unités entre un mur courbe et un mur droit, invisible à 16 unités par cellule. Ce n’est pas un bug, l’outil préfère rater une fuite qu’en inventer une, et c’est précisément pour ça que la compilation garde sa place dans la définition de « fini ».

Stack et choix techniques

Le serveur est en TypeScript sur Node 20 ou plus, testé avec Vitest. Les lectures de .vmf, de FGD et de pakfile passent par un sidecar Python qui s’appuie sur srctools. Les compilateurs tournent sous Wine, la chaîne Hammer++ par défaut, la chaîne d’origine en repli, et l’outil dit laquelle a tourné. Tout se lit par offsets : une map réelle fait parfois plus d’un gigaoctet, et la charger d’un bloc tue le transport MCP, ce que l’appelant voit comme un blocage, pas comme une erreur. L’en-tête et le répertoire de lumps de rp_nycity_day se lisent en 1 ms, et le test de visibilité sur cette map passe 551 775 paires de points en 387 ms.

Le serveur ne parle jamais à un moteur en cours d’exécution. Il lit et écrit des fichiers et lance des compilateurs, rien d’autre, ce qui lui permet de tourner à côté d’un serveur en production sans le déranger. Les outils qui écrivent ou exécutent demandent confirm: true ou une liste d’autorisation, et chacun des cinq outils qui écrivent un .vmf est appelé en test sur une map de l’arborescence serveur, où il doit refuser. Le code est sous licence MIT, sur le dépôt GitHub de hammer-mcp.

C’est le même lecteur que dans mon étude sur Obscura, le navigateur headless auquel je contribue : quelqu’un qui donne des outils à un agent et qui doit pouvoir croire ce qu’ils lui renvoient.

Résultat

IndicateurValeur
Premier gate de faisabilité (compilateurs sous Wine)02/08/2026
Outils au début du dogfood70 (13/08/2026)
Fichiers de test73 (08/11/2026)
Maps construites par un agent sans contexte4
Appels MCP pour finir l’épicerie, tours 1 à 353 → 51 → 41
Échecs journalisés, tours 1 à 32 → 1 → 0
Trouvailles sur l’épicerie / corrigées20 / 16
Lecture d’en-tête d’une map de 1,13 Go1 ms

Ce sont des chiffres de construction et de mesure, relevés dans le dépôt. Je n’ai aucun chiffre d’usage. Ce que le dépôt montre : 32 étoiles au 08/11/2026, et un contributeur externe qui a fait tourner l’outil sous Windows avec Counter-Strike: Source, ce qui a ouvert deux issues de suivi.

Questions fréquentes

Un agent de code peut-il construire une map Source qui compile ?

Oui, pour des petites maps et avec un protocole strict. L’épicerie du premier tour a atteint tous ses verts : scellée selon read_vmf_leak et selon le compilateur, 7 règles sur 7 vérifiées, vbsp, vvis et vrad sortis en 0, pour 18 brushes et 8 entités. Sur la citerne, ouverte en jeu, le rendu hors ligne ne montrait pas le lissage des facettes : une fois en jeu, c’était la texture qui cassait la courbe, pas les groupes de lissage.

Pourquoi Wine plutôt que Windows ?

Les compilateurs sont des exécutables Windows livrés avec le client du jeu, jamais avec un serveur dédié. Tous les gates du dépôt ont été passés sous Wine 9.0, ce qui permet de compiler sur la même machine que le reste de la chaîne. Le support Windows natif est arrivé par un contributeur externe, et la revue lui a demandé de retirer un commit hors sujet, ce qu’il a fait en quelques heures.

Peut-on le brancher à côté d’un serveur de jeu en ligne ?

Oui, parce qu’il ne parle jamais au moteur. Quand un serveur a été touché, c’était pour le gate B, un test de faisabilité : un patch d’entités (gm_construct_l_0.lmp) déployé par le script de synchronisation, lu par le moteur, puis retiré, et le serveur remis sur la map où il avait été trouvé.


Moi, sur tout outil que je donne à un agent, je pose maintenant la règle de ce dépôt : pas d’outil sans un moyen indépendant de vérifier ce qu’il renvoie. Ça a un prix visible. read_vprof n’est pas écrit, faute d’échantillon réel pour le calibrer, et la densité de luxels est mesurée mais rendue skipped parce qu’aucun seuil n’est calibré. Je préfère qu’un agent lise « je ne sais pas » qu’un 89 % qui ressemble à un mur. Si tu veux la suite de ce genre de retours, erreurs comprises, c’est ce que j’envoie dans Sycode Dispatch.

UtileBien vuJe testeÀ creuser

Un projet similaire ?

30 minutes pour le cadrer, sans engagement.

Réserver un appel

Cet article t'a servi ?

Un projet similaire ?

30 minutes pour le cadrer, sans engagement.

Réserver un appel

Écrit par

Charlie, développeur freelance.

Je construis et mets en production des SaaS pour des fondateurs sans équipe technique.

À proposGitHubLinkedInTikTokInstagramEmail

Discussion

Reste courtois et concret. Je masque la pub, les attaques et le hors-sujet. Règles des discussions

    À lire ensuite

    Tu as un projet ?

    Une app à lancer, un produit à reprendre ou un doute sur la bonne approche : on en parle 30 minutes, c'est offert et sans engagement.

    Disponible pour 1 nouveau projet en décembre

    Tu veux apprendre à le faire ?

    Le parcours t'emmène de l'idée à une application en ligne, avec l'IA comme équipe technique. Les premières leçons sont gratuites.