Les agents IA apprennent à utiliser le web, et la plupart le font de la manière la plus fragile qui soit : en analysant votre DOM, en devinant quel bouton cliquer, et en cassant à chaque changement de classe CSS. WebMCP inverse ce modèle. Au lieu de forcer les agents à scraper vos pages, votre application web déclare des outils structurés — chercher dans ce catalogue, récupérer cette fiche détaillée, soumettre ce formulaire — et tout agent compatible MCP fonctionnant dans le navigateur peut les appeler directement, avec des entrées et des sorties typées.
WebMCP est une proposition incubée au sein du W3C Web Machine Learning Community Group, portée par des ingénieurs de Microsoft et Google. Elle définit une API navigateur (navigator.modelContext) par laquelle une page enregistre ses outils. Pendant que les navigateurs déploient le support natif derrière des drapeaux expérimentaux, le projet @mcp-b fournit un polyfill prêt pour la production et des bindings React utilisables dès aujourd'hui.
Dans ce tutoriel, vous ajouterez WebMCP à un projet Next.js App Router : enregistrement d'outils de recherche en lecture seule, publication de ressources structurées, et construction d'un outil de capture de leads avec une étape de confirmation humaine. C'est exactement l'architecture en production sur noqta.tn, où 8 outils WebMCP permettent aux agents IA de rechercher nos services, parcourir le contenu et demander des devis.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20 ou supérieur installé
- Un projet Next.js 14+ utilisant l'App Router (ou lancez
npx create-next-app@latest) - Une familiarité de base avec les hooks React et TypeScript
- Zod installé (nous l'utiliserons pour les schémas d'outils)
- Chrome ou un navigateur basé sur Chromium pour tester avec l'extension MCP-B
Ce que vous allez construire
Vous allez construire un site de démonstration de produits qui expose quatre capacités aux agents IA :
search_products— un outil en lecture seule qui recherche dans un catalogue de produits par mot-cléget_product_details— un outil en lecture seule qui renvoie les détails complets d'un produit- Une ressource produits — un document JSON structuré et adressable que les agents peuvent lire sans appeler d'outil
request_quote— un outil d'écriture qui capture un lead, protégé par une confirmation utilisateur explicite via l'élicitation WebMCP
À la fin, un agent IA connecté à votre page (via l'extension navigateur MCP-B ou un navigateur agentique) pourra répondre à des questions comme « trouve-moi un ordinateur portable dans mon budget et demande un devis » en appelant vos outils au lieu de scraper votre HTML.
Étape 1 : configuration du projet
Créez un nouveau projet Next.js si vous n'en avez pas :
npx create-next-app@latest webmcp-demo --typescript --app --tailwind
cd webmcp-demoInstallez les paquets WebMCP et Zod :
npm install @mcp-b/global @mcp-b/react-webmcp zodSi votre projet utilise déjà Zod 4, vous pouvez rencontrer un conflit de dépendances car @mcp-b épingle actuellement Zod 3. Installez avec le drapeau legacy : npm install @mcp-b/global @mcp-b/react-webmcp --legacy-peer-deps. Les outils fonctionnent correctement à l'exécution.
Les deux paquets jouent des rôles distincts :
@mcp-b/globalest le polyfill. L'importer une seule fois ajoutenavigator.modelContextau navigateur, de sorte que l'enregistrement d'outils fonctionne avant même l'arrivée du support natif.@mcp-b/react-webmcpfournit les hooks React —useWebMCPpour les outils,useWebMCPResourcepour les ressources etuseElicitationpour les confirmations utilisateur — qui gèrent l'enregistrement et le nettoyage au montage et au démontage.
Étape 2 : créer la couche de données produits
Les outils ont besoin de données sur lesquelles opérer. Créez un petit catalogue en mémoire dans lib/products.ts :
// lib/products.ts
export type Product = {
slug: string;
name: string;
category: string;
price: number;
description: string;
inStock: boolean;
};
export const products: Product[] = [
{
slug: "aero-14-laptop",
name: "Aero 14 Ultrabook",
category: "laptops",
price: 1299,
description: "A 14-inch ultrabook with 32GB RAM and 18-hour battery life.",
inStock: true,
},
{
slug: "titan-desk-pro",
name: "Titan Desk Pro",
category: "desks",
price: 549,
description: "Electric standing desk with dual motors and memory presets.",
inStock: true,
},
{
slug: "quiet-buds-x",
name: "QuietBuds X",
category: "audio",
price: 199,
description: "Noise-cancelling earbuds with adaptive transparency mode.",
inStock: false,
},
];
export function searchProducts(query: string): Product[] {
const q = query.toLowerCase();
return products.filter(
(p) =>
p.name.toLowerCase().includes(q) ||
p.category.toLowerCase().includes(q) ||
p.description.toLowerCase().includes(q)
);
}
export function getProduct(slug: string): Product | undefined {
return products.find((p) => p.slug === slug);
}Dans une application réelle, ces fonctions interrogeraient votre base de données ou appelleraient vos routes API — la couche d'outils que nous construisons ensuite ne se soucie pas de la provenance des données.
Étape 3 : enregistrer votre premier outil en lecture seule
Créez un composant client dans components/WebMCPProvider.tsx. Les outils WebMCP vivent dans le navigateur, ce fichier doit donc commencer par la directive use client :
// components/WebMCPProvider.tsx
"use client";
import "@mcp-b/global";
import { useWebMCP } from "@mcp-b/react-webmcp";
import { z } from "zod";
import { searchProducts } from "@/lib/products";
// Définissez les schémas au niveau du module pour qu'ils restent stables entre les rendus.
const searchInput = {
query: z.string().describe('Search keyword, e.g. "laptop" or "desk"'),
};
const productSchema = z.object({
slug: z.string(),
name: z.string(),
category: z.string(),
price: z.number(),
inStock: z.boolean(),
});
const searchOutput = {
results: z.array(productSchema),
};
export default function WebMCPProvider() {
useWebMCP({
name: "search_products",
description:
"Search the product catalog by keyword. Returns matching products with name, price, and stock status.",
inputSchema: searchInput,
outputSchema: searchOutput,
annotations: { readOnlyHint: true, openWorldHint: false },
handler: async ({ query }) => {
const results = searchProducts(query).map((p) => ({
slug: p.slug,
name: p.name,
category: p.category,
price: p.price,
inStock: p.inStock,
}));
return { results };
},
formatOutput: (o) => JSON.stringify(o.results, null, 2),
});
return null;
}Plusieurs détails comptent plus qu'il n'y paraît :
- Les schémas vivent en dehors du composant. Si vous définissez les schémas Zod à l'intérieur, ils sont recréés à chaque rendu et le hook peut réenregistrer l'outil en boucle. Les schémas au niveau du module gardent l'enregistrement stable.
- La
descriptionest votre prompt. L'agent décide d'appeler votre outil sur la seule base de ce texte. Rédigez-la comme vous documenteriez une API pour un développeur junior : ce qu'elle fait, ce qu'elle renvoie, et des exemples concrets d'entrées. - Les
annotationsguident le comportement de l'agent.readOnlyHint: trueindique aux clients que l'outil n'a aucun effet de bord, les agents peuvent donc l'appeler librement.openWorldHint: falsesignifie que l'outil opère sur un jeu de données fermé — votre catalogue — et non sur le web ouvert. formatOutputcontrôle la représentation textuelle lisible envoyée à côté de la sortie structurée.
Étape 4 : ajouter l'outil de détails
Dans le même composant, enregistrez un deuxième outil sous le premier. Les outils qui échouent doivent renvoyer des erreurs structurées plutôt que lever des exceptions, afin que l'agent puisse se rattraper :
const detailsInput = {
slug: z.string().describe('Product slug, e.g. "aero-14-laptop"'),
};
const detailsOutput = {
error: z.string().optional(),
name: z.string().optional(),
category: z.string().optional(),
price: z.number().optional(),
description: z.string().optional(),
inStock: z.boolean().optional(),
};useWebMCP({
name: "get_product_details",
description:
"Get full details of a product by its slug. Use search_products first to find slugs.",
inputSchema: detailsInput,
outputSchema: detailsOutput,
annotations: { readOnlyHint: true, openWorldHint: false },
handler: async ({ slug }) => {
const product = getProduct(slug);
if (!product) {
return { error: `Product "${slug}" not found. Try search_products first.` };
}
return {
name: product.name,
category: product.category,
price: product.price,
description: product.description,
inStock: product.inStock,
};
},
formatOutput: (o) => JSON.stringify(o, null, 2),
});Remarquez que le message d'erreur dit à l'agent quoi faire ensuite — « Essayez d'abord search_products » — transformant une impasse en chemin de récupération. Les bons messages d'erreur d'outils sont des prompts d'agents déguisés.
Étape 5 : exposer une ressource
Les outils répondent aux questions ; les ressources publient des documents. Une ressource est une charge utile adressable, en lecture seule, identifiée par un URI. Les agents peuvent la lire directement sans composer de requête — parfait pour les index, les catalogues et les informations d'entreprise.
Ajoutez le hook useWebMCPResource à votre provider :
import { useWebMCP, useWebMCPResource } from "@mcp-b/react-webmcp";
import { products } from "@/lib/products";
// Dans le corps du composant :
useWebMCPResource({
uri: "shop://products/index",
name: "Product Catalog Index",
description: "Complete list of products with slugs, prices, and stock status.",
mimeType: "application/json",
read: async (uri) => ({
contents: [
{
uri: uri.href,
text: JSON.stringify(products, null, 2),
},
],
}),
});Vous pouvez aussi déclarer des URI paramétrés avec des segments de gabarit, et lire les paramètres dans le callback :
useWebMCPResource({
uri: "shop://products/by-category/{category}",
name: "Products by Category",
description: "Products filtered by category name.",
mimeType: "application/json",
read: async (uri, params) => {
const category = (params?.category as string) || "";
const filtered = products.filter((p) => p.category === category);
return {
contents: [{ uri: uri.href, text: JSON.stringify(filtered, null, 2) }],
};
},
});Une bonne règle empirique : si l'agent appellerait le même outil avec les mêmes arguments à chaque session juste pour s'orienter, publiez ces données comme ressource à la place.
Étape 6 : construire un outil d'écriture avec confirmation humaine
Les outils en lecture seule sont sûrs par construction. Les outils d'écriture — soumettre des formulaires, créer des leads, envoyer des messages — nécessitent une barrière de consentement. La réponse de WebMCP est l'élicitation : l'outil se met en pause en pleine exécution et demande au client connecté de recueillir une entrée de l'utilisateur humain.
Ajoutez le hook useElicitation et une fonction utilitaire de confirmation :
import { useElicitation, useWebMCP } from "@mcp-b/react-webmcp";
// Dans le corps du composant :
const { elicitInput } = useElicitation();
const confirmWithUser = async (title: string, preview: object) => {
const details = JSON.stringify(preview, null, 2);
// Chemin préféré : l'élicitation WebMCP affiche une UI de confirmation
// native dans le client MCP connecté.
if (typeof window !== "undefined" && window.navigator?.modelContext) {
try {
const result = await elicitInput({
message: `${title}\n\nReview and confirm:\n\n${details}`,
requestedSchema: {
type: "object",
properties: {
confirm: {
type: "boolean",
title: "Confirm",
description: "Approve and send this request.",
},
},
required: ["confirm"],
},
});
return result.action === "accept" && result.content?.confirm === true;
} catch {
// Repli sur window.confirm ci-dessous.
}
}
// Repli : boîte de dialogue de confirmation du navigateur.
if (typeof window !== "undefined" && typeof window.confirm === "function") {
return window.confirm(`${title}\n\n${details}\n\nSend?`);
}
// Aucune UX de confirmation disponible : refuser l'effet de bord.
return false;
};Puis l'outil d'écriture lui-même :
const quoteInput = {
productSlug: z.string().describe("Slug of the product to quote"),
email: z.string().email().describe("Contact email for the quote"),
quantity: z.number().min(1).describe("Number of units"),
};
const quoteOutput = {
success: z.boolean(),
message: z.string().optional(),
error: z.string().optional(),
};
useWebMCP({
name: "request_quote",
description:
"Request a price quote for a product. Requires user confirmation before sending. Use this only after the user has expressed intent to get a quote.",
inputSchema: quoteInput,
outputSchema: quoteOutput,
annotations: { readOnlyHint: false, openWorldHint: false },
handler: async ({ productSlug, email, quantity }) => {
const product = getProduct(productSlug);
if (!product) {
return { success: false, error: `Unknown product "${productSlug}"` };
}
const approved = await confirmWithUser("Send quote request?", {
product: product.name,
email,
quantity,
estimatedTotal: product.price * quantity,
});
if (!approved) {
return { success: false, error: "User declined the quote request." };
}
const res = await fetch("/api/quotes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productSlug, email, quantity }),
});
if (!res.ok) {
return { success: false, error: `Server error: ${res.status}` };
}
return { success: true, message: "Quote request sent. Check your email." };
},
});La posture de sécurité ici est délibérée et mérite d'être intériorisée :
- L'agent propose, l'humain dispose. L'outil n'envoie jamais rien sans une confirmation booléenne explicite de l'utilisateur.
- L'aperçu montre exactement ce qui sera envoyé — produit, e-mail, quantité et total estimé — l'utilisateur confirme donc des faits, pas des impressions.
- Quand aucune UI de confirmation n'existe, l'outil refuse. Échouer de façon sûre vaut mieux que soumettre silencieusement.
- Le point de terminaison serveur valide malgré tout chaque champ. Les outils côté navigateur s'exécutent dans la session de l'utilisateur avec ses cookies ; votre API doit appliquer la même authentification, la même validation et la même limitation de débit que pour n'importe quel appel côté client.
Étape 7 : brancher le provider dans votre layout
Montez le provider une seule fois, à la racine, pour que les outils s'enregistrent sur chaque page :
// app/layout.tsx
import WebMCPProvider from "@/components/WebMCPProvider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="fr">
<body>
<WebMCPProvider />
{children}
</body>
</html>
);
}Comme useWebMCP se nettoie au démontage, vous pouvez aussi enregistrer des outils contextuels par page dans des routes individuelles — par exemple un outil get_current_article qui n'existe que sur les pages d'articles et renvoie le contenu que l'utilisateur lit actuellement. Les outils de niveau racine décrivent ce que votre site sait faire ; les outils de niveau page décrivent où se trouve l'utilisateur. La combinaison donne aux agents à la fois une carte et un repère « vous êtes ici ».
Tester votre implémentation
Lancez le serveur de développement :
npm run devVérifiez ensuite que les outils sont enregistrés. Ouvrez la console du navigateur sur localhost:3000 et testez le polyfill :
// Dans la console du navigateur :
navigator.modelContext !== undefined; // doit renvoyer truePour un test de bout en bout, installez l'extension MCP-B depuis le Chrome Web Store. Elle agit comme un client MCP qui découvre les outils sur la page ouverte. Ouvrez son panneau latéral sur votre site de dev et vous devriez voir search_products, get_product_details et request_quote listés avec leurs descriptions.
L'extension fait aussi le pont entre les outils de la page et les hôtes MCP natifs. Cela signifie que des agents de bureau comme Claude Code ou Claude Desktop peuvent appeler les outils de la page ouverte dans Chrome — votre site web devient effectivement un serveur MCP sans en héberger un.
Essayez un flux réaliste dans le chat de l'extension : demandez « trouve-moi un ultrabook et demande un devis pour 2 unités à test@example.com ». Un agent compétent enchaînera search_products, puis get_product_details, puis request_quote — et vous verrez apparaître l'invite d'élicitation vous demandant d'approuver avant tout envoi.
Dépannage
Les outils n'apparaissent pas dans le client. Vérifiez que import "@mcp-b/global" s'exécute avant tout enregistrement, et que votre composant provider se monte réellement (il doit être dans body et être un composant client). Inspectez la console pour des erreurs d'hydratation qui pourraient le démonter.
Les outils s'enregistrent deux fois en développement. Le Strict Mode de React monte les composants deux fois en dev. Les hooks @mcp-b gèrent correctement le nettoyage, mais si vous avez enregistré des outils manuellement via navigator.modelContext.registerTool, vous devez les désenregistrer vous-même au démontage — une raison de plus de préférer les hooks.
Conflit de dépendances à l'installation. Les paquets @mcp-b épinglent Zod 3 alors que beaucoup de projets en 2026 utilisent Zod 4. Utilisez --legacy-peer-deps avec npm ou acceptez l'avertissement de résolution avec pnpm. Gardez vos schémas d'outils sur la version de Zod attendue par @mcp-b.
Les changements du handler ne prennent pas effet. Le hook capture votre handler à l'enregistrement. En développement avec Fast Refresh, cela se résout généralement tout seul ; en production, assurez-vous que les valeurs dynamiques nécessaires à votre handler proviennent de refs ou d'un état au niveau du module, pas de closures périmées.
L'appel d'élicitation lève une exception. Tous les clients MCP n'implémentent pas encore l'élicitation. Enveloppez toujours elicitInput dans un try/catch et repliez-vous sur window.confirm, comme montré à l'étape 6.
Considérations de sécurité
WebMCP hérite du modèle de sécurité du navigateur, mais les agents ajoutent de nouveaux modes de défaillance qu'il faut anticiper :
- Traitez les entrées d'outils comme non fiables. Validez avec Zod (les hooks le font pour vous) et n'interpolez jamais les entrées dans du HTML ou des requêtes.
- Cadrez étroitement les outils d'écriture. Un outil par action, des paramètres minimaux, une confirmation explicite. Évitez les outils génériques du type « exécute ceci ».
- Gardez le contexte de session en tête. Les outils s'exécutent avec les cookies et permissions de l'utilisateur connecté. Un agent appelant
request_quoteest indiscernable de l'utilisateur cliquant sur le bouton — c'est exactement pourquoi la barrière de confirmation compte. - Limitez le débit côté serveur. Un agent en boucle peut appeler des outils bien plus vite qu'un humain ne clique. Vos routes API ont besoin des mêmes protections que pour tout client automatisé.
Prochaines étapes
- Ajoutez des outils contextuels par page qui exposent le contenu actuellement consulté, pour que les agents répondent à « résume cette page » à partir de données structurées plutôt que du DOM
- Publiez une ressource catalogue d'outils (un document JSON listant tous vos outils et ressources) pour que les agents s'orientent en une seule lecture
- Explorez le tutoriel Construire un serveur MCP en TypeScript pour comparer l'approche côté serveur avec WebMCP côté navigateur
- Lisez notre guide Construire un client MCP en TypeScript pour comprendre l'autre moitié du protocole
- Suivez le dépôt du W3C Web Machine Learning Community Group pour l'évolution de la spécification native de
navigator.modelContext
Conclusion
WebMCP inverse la relation entre les sites web et les agents IA : au lieu que les agents fassent de la rétro-ingénierie de votre interface, votre site publie une surface d'API typée, documentée et consciente des permissions, qui vit directement dans la page. Avec @mcp-b/react-webmcp, le coût d'implémentation est remarquablement bas — un composant client, une poignée de schémas Zod et des descriptions rédigées avec soin.
Vous avez construit un outil de recherche de catalogue, un outil de détails, des ressources paramétrées et un outil d'écriture protégé par une validation humaine. Le même patron s'applique à des sites de production réels : sur noqta.tn, cette architecture exacte alimente la recherche de services, la découverte de contenu et les demandes de devis pour tout agent IA qui visite le site. Le web agentique arrive vite, et les sites qui exposent des outils structurés seront ceux que les agents pourront réellement utiliser — avec précision, en sécurité, et avec l'utilisateur aux commandes.