Un agent qui veut chercher un article sur un blog n'a aujourd'hui qu'une seule méthode : regarder la page comme un humain pressé, repérer le champ de recherche, deviner le bon sélecteur, taper, relire les résultats à l'œil. Lent, fragile, cassé au premier redesign.
WebMCP renverse ça. La page déclare ses actions comme des outils typés, un nom, une description, un JSON Schema, et l'agent les appelle directement. Fini la devinette : un contrat.
C'est encore expérimental. Un draft du W3C, un origin trial ouvert dans Chrome, un seul agent qui sait le lire pour l'instant. Mais on peut le tester aujourd'hui sur son site. Ce billet montre comment exposer une page Symfony à un agent, ce que la doc de Chrome oublie de dire sur cette intégration précise, et ce que ça pourrait changer si la mayonnaise prend.
Pour aller plus loin
Créer un serveur MCP avec Symfony
Ce que c'est, en clair
Une page expose des outils via document.modelContext. Deux façons de les déclarer.
L'impérative : en JavaScript, document.modelContext.registerTool({...}), avec un callback qui fait le travail. On contrôle tout, la validation, l'appel réseau, le retour.
La déclarative : deux attributs, toolname et tooldescription, posés sur un <form> existant, et le navigateur dérive le schéma tout seul depuis les champs. Zéro JavaScript, mais on subit ce que le navigateur infère.
Point commun : tout tourne côté client. Pas de serveur, pas de protocole réseau, pas de client distant à authentifier. Le handler s'exécute dans le contexte de la page, avec la session et les cookies du visiteur. Un agent qui appelle un tool a exactement les droits du visiteur, ni plus, ni moins. Si on a déjà monté un serveur MCP classique, c'est le miroir : là où le serveur MCP posait la question de l'authentification du client distant, WebMCP ne la pose pas, parce qu'il n'y a pas de client à part le navigateur lui-même. Les deux cohabitent sans se marcher dessus.
Le statut, sans enjoliver : la spec bouge, l'origin trial court jusqu'à Chrome 156, et le seul consommateur réel est Gemini dans Chrome. On construit sur du sable qui peut se tasser en socle, ou s'en aller. À garder en tête à chaque décision.
Le tester en local, en cinq minutes
Pas besoin de token ni de déploiement pour jouer avec. Deux choses.
- Activer le flag
chrome://flags/#enable-webmcp-testinget relancer Chrome. Ça exposedocument.modelContextsur toutes les pages, sans origin trial. - Installer l'extension Model Context Tool Inspector. Elle ouvre un panneau latéral qui liste les tools qu'une page enregistre, affiche leur schéma, et laisse les appeler à la main, ou laisse Gemini le faire.
À partir de là, tout ce qu'une page enregistre devient visible et testable. C'est le banc d'essai.
Un tool impératif dans Symfony et Stimulus
Le plus simple pour commencer : exposer une recherche. Avec un endpoint JSON déjà en place, on est à quelques lignes du but. Pas de nouvelle route, pas de logique métier à dupliquer : le tool est une façade qui rend le JSON existant à l'agent.
On le met dans un controller Stimulus, monté sur une <div hidden> du layout pour qu'il soit présent partout.
// assets/controllers/webmcp_search_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
connect() {
this.mc = document.modelContext ?? navigator.modelContext ?? null;
if (!this.mc?.registerTool) return; // pas d'API : on ne fait rien
this.abort = new AbortController();
this.mc.registerTool({
name: "search_articles",
description: "Recherche des articles du blog par mots-clés.",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
execute: async ({ query }) => {
const url = `/api/recherche/autocomplete?q=${encodeURIComponent(query)}`;
const res = await fetch(url, {
headers: { Accept: "application/json" },
credentials: "same-origin",
});
return JSON.stringify(await res.json());
},
}, { signal: this.abort.signal });
}
disconnect() {
this.abort?.abort();
this.mc?.unregisterTool?.("search_articles");
}
}Recharger la page avec le flag actif, ouvrir l'inspecteur : search_articles apparaît, avec son schéma. On l'appelle avec une query, on récupère le JSON. L'agent voit une fonction propre, pas une liste de résultats à scraper.
Ça a l'air trivial. Trois détails ne le sont pas, et aucun n'est dans la doc de Chrome.
navigator ou document ? La proposal du W3C parle de navigator.modelContext, la doc de Chrome de document.modelContext. Les deux existent aujourd'hui, mais Chrome affiche un avertissement de dépréciation dès qu'on touche navigator. Feature-detecter les deux, préférer document. C'est le genre de basique que le draft peut encore déplacer, donc autant ne pas parier sur un seul nom.
Turbo Drive tend un piège. La spec dit que les tools ne survivent pas à une navigation. Sauf que Turbo ne navigue pas vraiment : il remplace le <body> sans recharger le contexte JavaScript. Résultat, les tools ne se nettoient pas tout seuls, et chaque changement de page empile un doublon. La parade est le disconnect() de Stimulus, appelé pile au moment où Turbo retire l'ancien élément : on y désenregistre le tool, par le signal côté Chrome et par unregisterTool(name) côté proposal, les deux pour être tranquille.
Ne pas déclarer Permissions-Policy: tools. WebMCP est gouverné par une Permissions Policy nommée tools, au défaut self : la page peut enregistrer des tools, une iframe tierce non. Tentant de l'expliciter dans un bundle de sécurité comme NelmioSecurity. Mauvaise idée : la version actuelle du bundle maintient une liste blanche des directives connues, tools n'y figure pas, et la déclarer fait planter la config au démarrage. Le défaut self suffit déjà. On ne touche à rien, ça marche.
Le passer en prod : un token, et de l'invisible
En local, le flag suffit. Pour de vrais visiteurs, il faut un token d'origin trial. On l'obtient sur la console Chrome Origin Trials en enregistrant son origine, et on le rend dans une balise du <head> :
<meta http-equiv="origin-trial" content="LE_TOKEN">Dans Symfony, on injecte le token par une variable d'environnement et un petit composant Twig qui ne rend la balise que si le token est présent (convention pratique : valeur vide ou préfixée dummy-, on ne rend rien, WebMCP reste inactif proprement). Le token est public, visible dans le HTML, il n'y a rien à cacher, et il expire avec le trial.
Ce qui frappe une fois en prod : il n'y a rien à voir. Aucun bouton, aucun badge. La balise est dans le <head>, l'enregistrement se fait en JavaScript, la <div> est cachée. Un visiteur voit la même page qu'avant. Le tool n'existe que pour un agent qui interroge document.modelContext. Le seul signe visible côté grand public, c'est un audit Lighthouse qui coche une case « Agentic Browsing ». WebMCP, c'est une entrée de service pour des visiteurs qui n'ont pas encore de corps.
Et la déclarative, sur un formulaire ?
Sur le papier, c'est l'API rêvée pour un formulaire de contact : deux attributs, et l'agent sait écrire un message. En pratique, elle se cogne vite au réel. Un formulaire multi-étapes ne montre à l'agent que l'étape courante. Le nommage bracketé de Symfony (contact[coordonnees][email]) donne un schéma dérivé illisible. Et un honeypot anti-spam, ce champ masqué en CSS qu'un bot remplit bêtement, fuite dans le schéma dérivé : l'agent coopératif le remplit poliment et se fait recaler en silence.
La leçon vaut d'être retenue : la déclarative brille sur un formulaire simple, mono-page, sans anti-spam. Dès qu'il y a de la logique, un tool impératif qui pilote la chose, ou qui pré-remplit et laisse l'humain valider, reste plus sûr. Surtout pour une action sensible : soumettre un message ou cocher un consentement à la place de quelqu'un, ce n'est pas une commodité, c'est un risque.
Le potentiel, sans hype
Reste la vraie question : est-ce que ça vaut le coup de s'y mettre pour un truc que seul Gemini lit ?
De près, le pari est mince. Un consommateur, une spec mouvante, une fenêtre qui peut se refermer. De loin, il change quelque chose. Depuis vingt ans, les machines lisent nos pages en douce, par scraping, en devinant, souvent contre notre gré. WebMCP inverse le geste : le site décide, explicitement, ce qu'il offre aux agents et comment. Ce n'est plus une extraction subie, c'est une interface déclarée. Et une interface déclarée, ça se versionne, ça se teste, ça se sécurise, contrairement à un scraper qu'on encaisse.
Dans le même esprit
Une IA a trouvé les failles que ma QA ne voyait plus
Si les agents de navigateur deviennent un usage courant, les sites qui auront déjà déclaré leurs actions partiront avec une surface d'intégration propre plutôt qu'un DOM à deviner. Si ça fait pschitt, on aura appris à penser son site comme une API pour humains et pour machines, ce qui n'est pas une mauvaise gymnastique.
Le coût d'entrée est faible : un controller, un endpoint qu'on a souvent déjà, un token. Le flag et l'inspecteur suffisent pour se faire un avis en une soirée. À ce prix, la question n'est peut-être pas « est-ce que ça va marcher », mais « qu'est-ce que ça coûte de tenir la porte ouverte, au cas où quelqu'un finisse par entrer ».
Une coquille, une erreur dans ce billet ? Signale-la-moi.