Accueil / Articles

WebMCP en pratique : transformer un tableau de tâches en outils pour des agents IA

WebMCP en pratique : une board de tâches transformée en outils pour des agents IA grâce à des outils déclaratifs et scriptés, une liste d’outils dynamique, un pont MCP et un refus.

Éditeur : REACTAPP.TOP14 min de lecture3318 mots

Sources: WebMCP: When Websites Become AI Tools

Agents qui devinent votre site web

Aujourd’hui, un agent d’IA travaillant avec un site web se contente le plus souvent de deviner. Il prend une capture d’écran à la recherche de quelque chose ressemblant à un bouton, ou il analyse le DOM en espérant que les noms de classe ont bien le sens qu’ils semblent avoir. Après un clic, il vérifie si la page a changé de manière à indiquer un succès. C’est un tour impressionnant reposant sur des fondations fragiles, car le site n’a jamais rien dit à l’agent et toutes les conclusions sont inférées.

WebMCP est la proposition de Chrome pour mettre fin à ces devinements. Une page annonce ses capacités, l’agent lit cette annonce et invoque une fonction avec des arguments structurés, au lieu de viser un rectangle à l’écran. Le changement semble mineur, mais il déplace l’intelligence : au lieu de reconstruire l’objectif d’une application à partir de pixels et de balises, l’agent reçoit cet objectif directement de l’application elle-même.

Afin de vérifier si cela fonctionne en pratique, une petite plateforme de tâches a été créée et un agent réel y a été assigné. Le résultat le plus instructif est apparu lorsque l’agent a refusé catégoriquement une demande.

Trois rôles

Chaque instant de l’expérience implique l’un des trois acteurs.

  • La page est le fournisseur. Elle enregistre les outils. Chaque outil associe un nom à une description destinée à un modèle, un JSON Schema décrivant les arguments, ainsi qu’une fonction qui effectue le travail.
  • Le navigateur est le registre. Pour chaque onglet, il conserve la liste des outils, déduit des schémas à partir de l’HTML chaque fois que c’est possible, et décide quand un outil apparaît ou disparaît.
  • L’agent est le consommateur. Il interroge le navigateur pour connaître les outils disponibles via getTools() et exécute l’outil choisi avec executeTool(). Le DOM reste hors de sa portée, et il n’a aucun moyen de compenser par des fonctionnalités que la page n’a jamais mises en place.
  • Ce dernier point constitue l’essence de l’idée. La documentation de Chrome décrit l’API de manière suffisamment claire, mais lire une explication n’est pas identique à comprendre réellement le fonctionnement, c’est pourquoi la solution la plus rapide a été de créer la version la plus concise possible.

    La démonstration : un tableau de tâches en trois fichiers

    Le projet se compose de index.html, app.js et style.css. Rien n’a besoin d’être compilé ou installé. Les utilisateurs peuvent ajouter des tâches, les cocher et filtrer la liste ; il s’agit du type de projet qui peut être réalisé en une après-midi. Ce qui est nouveau, c’est que la page s’affiche également à un agent IA sous forme d’un ensemble d’outils appelables, via un objet fourni par le navigateur.

    const mc = document.modelContext;   // undefined if WebMCP is off
    

    Cette seule ligne constitue l’unique point d’entrée. Lorsque l’objet existe, la page peut enregistrer des outils. S’il n’existe pas, la page n’est qu’un tableau de tâches, ce qui correspond à son comportement de dégradation prévu.

    Afin de rendre les deux éléments visibles en même temps, la page de démonstration les place côte à côte. À gauche se trouve le tableau de tâches, c’est-à-dire le fournisseur. À droite, un panneau « Pretend Agent » joue le rôle du consommateur et effectue des appels getTools() et executeTool() exactement comme le ferait un agent réel, ce qui évite tout câblage supplémentaire. Vous verrez ce panneau dans chaque capture d’écran ci-dessous.

    WebMCP figure toujours sous le statut « Proposed » sur Chrome Platform Status et n’est pas activé par défaut nulle part, il faut donc d’abord définir une flag.

    Ouvrez chrome://flags/#enable-webmcp-testing, mettez-le sur « Enabled » et redémarrez. Sans cela, document.modelContext est défini comme undefined. Chrome propose également une version d’essai de WebMCP origin qui débute avec Chrome 149. Ce paramètre convient aux expériences locales, tandis que la version d’essai origin est la solution préconisée par Chrome pour des tests plus larges.

    D’abord le boucle terminée

    Il est plus facile de comprendre l’API après avoir vu ce qu’elle produit. Claude Code a été connecté au tableau de contrôle et on lui a donné une phrase lui demandant d’ajouter quatre tâches : trouver des informations et mener des recherches, rédiger des plans de blog, créer un blog complet avec des exemples, et le publier. Quatre tâches sont apparues. Une deuxième phrase lui a demandé de marquer les tâches 3, 4 et 5 comme terminées, ce qui a également fonctionné.

    Claude n’a pas pris de capture d’écran du tableau ni lu son HTML. Il a demandé au navigateur quels outils étaient disponibles, a repéré add-task et l’a invoqué quatre fois avec des arguments structurés. Voici la même scène vue depuis l’intérieur du navigateur.

    Les quatre nouvelles tâches se trouvent maintenant aux positions #3 à #6 sur le tableau, et le panneau DevTools (Application, puis WebMCP) liste les six outils que la page publie actuellement. L’un d’eux, clear-completed, n’était pas présent une minute plus tôt, et la raison en apparaîtra bientôt. Les trois terminations de tâches sont enregistrées une par une.

    Chaque appel s’affiche avec ses arguments ({"id":3}, {"id":4} et {"id":5}), et la ligne de résumé indique trois appels au total, sans échecs, annulations ni appels en cours. Le débogage de WebMCP est ainsi beaucoup plus rapide, car le panneau affiche précisément ce que l’agent a envoyé.

    Ainsi, une phrase tapée dans une console se transforme en quatre appels de fonction structurés à l’intérieur d’une page navigateur. La question suivante est de savoir ce que la page devait faire pour y parvenir.

    Deux façons de publier un outil

    La première ne nécessite absolument pas de JavaScript. Un formulaire existant reçoit quatre attributs supplémentaires.

    <form id="add-form"
          toolname="add-task"
          tooldescription="Add a new task to the user's task board."
          toolautosubmit>
      <input name="title" required maxlength="80"
             toolparamdescription="Short description of the task to add.">
      <select name="priority"
              toolparamdescription="How urgent the task is.">
        <option value="low">low</option>
        <option value="normal" selected>normal</option>
        <option value="high">high</option>
      </select>
      <button type="submit">Add</button>
    </form>
    

    Ce formulaire correspond à l’outil add-task que Claude a invoqué quatre fois. Les noms de champs, les attributs required ainsi que les options <select> sont tous lus par le navigateur, qui assemble ensuite le JSON Schema de manière autonome. L’agent ne voit qu’une chaîne de caractères obligatoire et un enum à trois valeurs, sans que personne n’ait tapé manuellement le schéma. Dans une application existante, il s’agit probablement de l’expérience WebMCP la plus simple, car un formulaire HTML ordinaire offre un avantage initial. Tout ce qui est ajouté, c’est une description de ce que le formulaire signifie pour un agent.

    Un détail mérite une attention particulière : le même gestionnaire submit sert à la fois une personne qui clique sur « Ajouter » et un agent qui appelle l’outil, et l’événement indique lequel des deux c’est.

    $('#add-form').addEventListener('submit', (e) => {
      e.preventDefault();
      const task = addTask(new FormData(e.target).get('title'), /* ... */);
    
      if (!e.agentInvoked) {
        e.target.reset();          // human — clear the box
      } else {
        e.respondWith?.(Promise.resolve(
          text(`Added task #${task.id}: "${task.title}".`)
        ));
      }
    });
    

    Le drapeau agentInvoked indique qui a appuyé sur le bouton, et respondWith() renvoie un résultat à l’agent sans quitter l’application. Il existe un seul parcours de code avec deux appels, ce qui constitue un bon choix de conception, car personne ne souhaite une « version IA » pour chaque action dans une application. La même logique doit servir les utilisateurs et les agents chaque fois que c’est pertinent.

    La méthode déclarative est sans doute la moitié moins intéressante, bien qu’elle soit celle qui attire le plus l’attention, car elle se présente bien et ne nécessite pas de réflexion supplémentaire. C’est ce qu’un formulaire ne peut pas exprimer qui rend les choses intéressantes. C’est le deuxième parcours, registerTool, qui ressemble beaucoup à ce que l’on écrirait pour un serveur MCP backend.

    await mc.registerTool({
      name: 'list-tasks',
      description: 'List the tasks on the board. Use this before acting so you know the task IDs.',
      inputSchema: {
        type: 'object',
        properties: {
          status: { type: 'string', enum: ['all', 'open', 'done'] },
        },
      },
      annotations: { readOnlyHint: true },
      async execute({ status = 'all' }) {
        const rows = visible(tasks, status);
        return text(rows.map((t) => `#${t.id} [${t.done ? 'x' : ' '}] ${t.title}`).join('\n'));
      },
    });
    

    Examinez attentivement cette description. Elle est rédigée pour un modèle et non pour un développeur, et une phrase telle que « utilisez ceci avant d’agir afin de connaître les IDs des tâches » indique quand l’appeler. Les descriptions font désormais partie de l’interface API, et celles qui sont vagues entraînent des agents qui devinent.

    Le drapeau readOnlyHint indique qu’il est possible d’appeler une fonction sans demander l’avis de l’utilisateur au préalable. list-tasks et estimate-task le possèdent. complete-task et clear-completed ne le possèdent pas, car ils modifient des données.

    Les outils peuvent également être lents, et un agent peut changer d’avis en cours de route. L’outil estimate-task attend délibérément trois secondes, et il reçoit un AbortSignal en tant que deuxième argument.

    async execute({ id }, { signal }) {
      const t = findTask(tasks, id);
    
      if (!t) {
        return text(`No task #${id}.`);
      }
    
      await sleep(3000, signal); // throws if the agent aborts
    
      return text(
        `Task #${t.id} is about ${
          t.priority === 'high' ? '2 hours' : '30 minutes'
        }.`
      );
    }
    

    En respectant ce signal, l’annulation fonctionne. En l’ignorant, l’outil continue de fonctionner après le départ de l’agent.

    Une liste d’outils qui change pendant que la page est ouverte

    La raison principale d’enregistrer les outils dans un script, plutôt que d’envoyer un manifeste statique, est que la liste peut être modifiée en temps de exécution. Le tableau dispose d’un outil clear-completed qui supprime toutes les tâches terminées. Cela n’a de sens que lorsque quelque chose est terminé, ce qui explique pourquoi il apparaissait sur l’écran capture précédent juste après que trois tâches aient été marquées comme terminées. Il n’existe donc que à ces moments-là.

    let clearCtl = null;
    
    async function syncClearTool() {
      const has = tasks.some((t) => t.done);
    
      if (has && !clearCtl) {
        const ctl = new AbortController();
        clearCtl = ctl;
    
        await mc.registerTool(
          {
            name: 'clear-completed',
            /* ... */
          },
          {
            signal: ctl.signal
          }
        );
      } else if (!has && clearCtl) {
        const ctl = clearCtl;
        clearCtl = null;
    
        setTimeout(() => ctl.abort(), 0); // abort() IS unregister
      }
    }
    

    Il n’existe pas de unregisterTool(). Lors de l’enregistrement, on passe un AbortSignal, et l’annulation de ce signal supprime l’outil. C’est là que le concept fonctionne : le signal qui annule une appel en cours revoque également la capacité correspondante, un mécanisme qui remplit deux fonctions.

    Lorsqu’on coche une tâche comme terminée, clear-completed apparaît dans la liste de l’agent ; en décochant cette tâche, l’outil disparaît. L’agent est informé de cela via un événement toolchange et n’a pas besoin de faire de vérifications périodiques.

    mc.addEventListener('toolchange', refreshTools);
    

    Ainsi, la liste des outils n’est pas un manifest publié une seule fois. Elle décrit ce que l’application peut faire actuellement, dans son état actuel.

    En quoi cela diffère-t-il d’un serveur MCP backend

    Ensuite, on a demandé à Claude Code de limiter le tableau aux tâches terminées.

    L’agent a appelé la fonction filter-tasks avec {"status":"done"} ; le bouton « Terminé » est devenu actif et le tableau n’a affiché que les tâches achevées. L’agent n’a pas récupéré de copie filtrée des données. Il a simplement modifié la vue que l’utilisateur consultait.

    Un serveur MCP backend ne peut pas y parvenir. Se trouvant de l’autre côté d’une frontière réseau, il travaille avec des données stockées et ne sait rien de ce qui s’affiche actuellement sur n’importe quel écran. Les outils WebMCP se trouvent à l’intérieur de la fenêtre, partageant son DOM, sa session et ses cookies, de sorte que l’agent et l’utilisateur voient le même écran et l’un ou l’autre peut le modifier. C’est à la fois un véritable atout et un véritable risque, ce dernier étant abordé dans une section distincte ci-dessous. C’est aussi pourquoi qualifier WebMCP de « MCP à l’intérieur du navigateur » le minimise. L’endroit où se trouve l’outil est important, car il est lié à l’état même de la page que l’utilisateur consulte.

    Connexion d’un agent externe

    Le panneau Pretend Agent est utile pendant le développement, mais il s’avère peu fiable, car il s’agit de la page qui s’appelle elle-même. Un véritable test nécessite un outil en dehors du navigateur capable de voir uniquement ce que getTools() rapporte. C’est là que intervient agent.mjs, une petite utilité en ligne de commande utilisant quelques fonctionnalités du Chrome DevTools Protocol sans aucune dépendance, étant donné que Node 22 dispose d’un WebSocket global. Par conception, elle ne peut voir que ce que getTools() rapporte et agir uniquement via executeTool(), sans possibilité de scraping du DOM.

    node agent.mjs list
    node agent.mjs call add-task '{"title":"Ship the demo","priority":"high"}'
    

    Ces deux commandes énumèrent les outils puis appellent add-task en indiquant un titre et une priorité. La configuration utilisée pour générer les captures d’écran va encore plus loin. Claude Desktop, Claude Code et ChatGPT utilisent MCP et non WebMCP, donc mcp-bridge.mjs agit comme adaptateur, et un adaptateur de petite taille, car les protocoles correspondent presque exactement : tools/list est associé à getTools() et tools/call à executeTool(). Claude Code le détecte grâce à un fichier .mcp.json.

    {
      "mcpServers": {
        "webmcp-board": {
          "command": "node",
          "args": ["C:\\projects\\web-mcp-demo\\mcp-bridge.mjs"],
          "env": { "PAGE_URL": "https://tusharkanjariya.github.io/web-mcp-demo/" }
        }
      }
    }
    

    Toute la chaîne consiste donc en Claude Code qui communique avec le pont via MCP, le pont qui communique avec Chrome via CDP, et Chrome qui appelle les outils enregistrés par la page. Il y a donc quatre étapes, et la page finale ne peut pas savoir quel client se trouve de l’autre côté.

    Claude Code
        ↓
    MCP
        ↓
    mcp-bridge.mjs
        ↓
    Chrome DevTools Protocol
        ↓
    WebMCP
        ↓
    my task board
    

    Un autre avantage : le pont lance son propre Chrome si personne ne répond sur le port de débogage, et ouvre la page lorsque l’onglet n’est pas présent. Un client MCP peut démarrer tout cela à partir de zéro.

    Le refus

    Puis on a demandé à l’agent de supprimer une seule tâche.

    La moitié supérieure de la capture d’écran montre la demande de filtrage précédente. La moitié inférieure est la partie intéressante : Claude a indiqué qu’aucun outil delete-task n’existait sur cette plateforme, que la seule méthode de suppression disponible était clear-completed, ce qui effacerait également les tâches n°2, n°3, n°4 et n°5, et qu’il ne comptait pas le faire.

    Rien d’un garde-fou, d’une instruction système ou d’une couche de sécurité n’a provoqué ce refus. La capacité de supprimer une seule tâche n’existait tout simplement pas, et l’agent l’a remarqué. Il serait erroné de tirer de trop de conclusions d’un seul échange dans une application de démonstration. Néanmoins, cela change la façon dont on perçoit le problème. Un agent qui dirige une page en utilisant des pixels et des coordonnées aurait trouvé quelque chose permettant de supprimer des éléments et l’aurait essayé, car les agents de capture d’écran ne disposent que de l’improvisation comme solution de repli. Celui-ci a compté ses six outils, a constaté qu’aucun ne permettait de supprimer une tâche, a reconnu les dommages collatéraux liés à l’option la plus proche et a fait une pause pour demander de l’aide.

    La surface des outils constituait la limite. Personne n’a écrit de règle interdisant la suppression de contenus. La page ne proposait tout simplement jamais la suppression comme une action possible, et cela suffisait, même si WebMCP ne rend pas automatiquement les agents sûrs.

    Où Chrome et les spécifications diffèrent

    Le test a révélé des différences entre la documentation et le navigateur, et il a fallu toute une après-midi pour les résoudre. Trois d’entre elles concernent la structure des données.

    • Les arguments de executeTool() : la spécification décrit un objet, mais Chrome 151 attend une chaîne JSON, et un objet provoque l’erreur Failed to parse input arguments.
    • RegisteredTool.inputSchema : défini comme un objet dans la spécification, mais Chrome renvoie une chaîne JSON, il faut donc la parser avant de lire les properties.
    • Ce à quoi se réfère executeTool() : la spécification indique un DOMString, ce qui est vrai, mais il s’agit d’une enveloppe {content:[…]} serialisée qui doit être déballée deux fois pour accéder au texte.

    Rien de tout cela n’est difficile une fois que l’on connaît les règles. Les deux problèmes réels étaient des bugs liés au cycle de vie, et ils ne se manifestent que lorsque un véritable agent interagit avec la page.

    Une appel à form.reset() alors qu’une soumission déclenchée par un agent est en cours annule l’appel à l’outil avec le message Exécution de l’outil annulée en raison d’un réinitialisation du formulaire. Un gestionnaire tout à fait ordinaire qui vide les champs après soumission, comme le font la plupart, a provoqué l’échec de chaque appel add-task initié par un agent. La tâche était bien créée, mais la réponse n’est jamais arrivée ; c’est pourquoi le gestionnaire présenté précédemment ne procède au réinitialisation que pour les soumissions effectuées par un utilisateur humain.

    Dans les versions antérieures à Chrome 153, la suppression d’une étape annulait également toutes ses appels encore en cours. Cela n’était pas problématique tant que l’étape ne se supprimait pas elle-même en raison de son succès. clear-completed supprime toutes les tâches terminées, de sorte qu’il ne reste rien à faire, et l’étape se retire alors. Dans la version 151, cet annulation interrompait sa propre réponse avant même qu’elle ne soit renvoyée. La solution consiste en setTimeout(() => ctl.abort(), 0) : en différant l’annulation d’un cycle temporel, on permet à l’appel de se résoudre d’abord. Cela est expliquable et fonctionne, mais différer le nettoyage d’un cycle temporel est le type de correction qui mérite d’être réexaminée après quelques mises à jour de Chrome.

    Déploiement de la démo

    La démo est entièrement statique, ce qui permet à GitHub Pages de la servir telle quelle, et HTTPS fournit le contexte sécurisé exigé par WebMCP. Chacun dispose ainsi d’un tableau de tâches fonctionnel, mais pas de WebMCP fonctionnel, car les visiteurs qui n’ont pas activé le paramètre voient toujours document.modelContext comme étant défini sur « undefined ». Pour y remédier, il faut un jeton d’essai d’origine dans le <head>, enregistré comme premier parti, car Chrome rejette les jetons de tiers placés dans une balise meta. Sur un profil propre sans paramètres activés, modelContext renvoyait la valeur « object » ainsi que les six outils. L’essai est valable jusqu’à Chrome 156.

    La question de sécurité sans bonne réponse

    Les outils WebMCP s’exécutent à l’intérieur d’une fenêtre de connexion active. C’est là l’essentiel, mais c’est aussi tout le problème.

    La spécification inclut un untrustedContentHint, qui n’est qu’un indice. La manière de gérer l’injection de commandes reste non résolue, et aucune partie de cette API ne constitue une limite d’autorisation. Si du texte contrôlé par un attaquant peut être affiché sur une page et lu par un agent, readOnlyHint ne peut pas aider.

    Le pont peut également être mis à disposition via HTTP par l’intermédiaire d’un tunnel afin que les connecteurs de ChatGPT puissent y accéder. Cela fonctionne, mais réfléchissez bien avant de le faire, car le tunnel expose une extrémité sans authentification qui permet à quiconque connaît l’adresse d’exécuter des outils dans la fenêtre de votre navigateur connecté. Vingt minutes d’expérimentations, c’est acceptable, mais le laisser actif toute la nuit, non.

    Essayer sur votre propre application

    Les administrateurs d’une application web peuvent évaluer l’API en une après-midi, car elle est de petite taille. Choisissez n’importe quel formulaire existant, ajoutez-lui les attributs toolname et tooldescription, puis ouvrez le panneau WebMCP dans les outils de développement pour voir l’outil apparaître. Environ dix minutes de ce travail enseignent plus que toute une heure de lecture.

    La leçon dépasse la syntaxe. Dire honnêtement ce que votre application peut faire fait désormais partie intégrante de ce que vous livrez, plutôt que de constituer une documentation écrite ultérieurement. Le panneau ne proposait pas de fonction de suppression pour une tâche unique, donc l’agent n’a jamais effectué de suppression. Ce que getTools() renvoie détermine la façon dont un agent perçoit les capacités de votre application, et cette perception devient progressivement l’interface elle-même. La démonstration comprenait six outils répartis en trois fichiers sans dépendances ; réfléchissez donc à ce que votre propre page dirait d’elle-même. Pour la reproduire, activez l’option chrome://flags/#enable-webmcp-testing, puis exécutez node agent.mjs list. Obtenir six outils en réponse signifie que vous communiquez avec une page web de la même manière qu’un agent le ferait.

    Réponses rapides

    Qu’est-ce que WebMCP ? C’est une API de navigateur proposée qui permet à une page de publier ses propres fonctionnalités à un agent d’IA sous forme d’outils pouvant être appelés, en utilisant document.modelContext. Les agents appellent directement ces outils, évitant ainsi le scraping du DOM et la coordination des clics.

    En quoi diffère-t-il d’un serveur MCP classique ? Les serveurs backend fonctionnent en dehors des limites du réseau et traitent des données stockées. Les outils WebMCP s’exécutent dans l’onglet, ce qui leur permet de modifier ce que voit l’utilisateur ; ils réutilisent également la session et les permissions de la page. L’appel à filter-tasks a modifié visiblement le tableau.

    Comment une page enregistre-t-elle un outil ? Soit on ajoute les attributs toolname et tooldescription à un formulaire existant pour permettre au navigateur de déduire le schéma à partir des champs, soit on appelle document.modelContext.registerTool() en fournissant un nom, une description, un schéma JSON ainsi qu’une fonction execute. Les formulaires conviennent aux actions simples, tandis que registerTool gère tout ce que les formulaires ne peuvent pas exprimer.

    Une étiquette est-elle requise ? Pour l’instant, oui. Activez cette étiquette et redémarrez Chrome, ou lancez-le avec --enable-features=WebMCP. Pour les visiteurs qui n’ont pas activé cette étiquette, un jeton d’essai d’origine first-party dans le <head> de la page fonctionne.

    Claude Desktop ou ChatGPT peuvent-ils appeler ces outils ? Pas seuls, car ils utilisent MCP plutôt que WebMCP. Un petit adaptateur mappe tools/list vers getTools() et tools/call vers executeTool(). Celui-ci compte environ 130 lignes, ne nécessite aucune dépendance et fonctionne via stdio pour les clients locaux.

    Est-ce sécurisé ? La question reste ouverte. untrustedContentHint et readOnlyHint ne sont que des indications ; aucun ne définit une limite d’autorisation, et la spécification n’a pas encore résolu le problème de l’injection de commandes. Construisez l’interface des outils en supposant qu’un agent pourrait être amené à appeler n’importe quoi que vous exposez.

    Lectures complémentaires

  • Serena MCP Server : édition de code sensible aux symboles pour les assistants IA — Configurez Serena avec Docker et SSE afin que Claude Code, VS Code et OpenCode disposent d’une recherche de symboles assistée par LSP, de possibilités de renomage sécurisées et d’une mémoire persistante avec moins de tokens.