L'API impérative utilise document.modelContext.registerTool() pour définir des outils JavaScript de manière programmatique. Cette approche est idéale pour les applications monopages (SPA) où des outils doivent être ajoutés ou supprimés en fonction de la route actuelle ou de l'état de l'utilisateur.
WebMCP fonctionne sur une architecture producteur/consommateur :
- Les pages Web sont des producteurs d'outils : vous définissez et enregistrez des outils à l'aide de
document.modelContext.registerTool(). C'est tout ce qu'une page Web doit implémenter. Vous n'avez pas besoin d'appelergetTools()ouexecuteTool()sur la page pour exposer des outils aux agents. - Les agents de navigateur et d'extension sont les principaux consommateurs : les agents fournis par ou via le navigateur (y compris l'agent intégré du navigateur, les extensions de navigateur, les outils de développement et les assistants au niveau du système) découvrent et appellent les outils enregistrés hors bande via des mécanismes de plate-forme internes.
- Agents intégrés à la page et harnais de test :
getTools()etexecuteTool()existent spécifiquement pour les assistants JavaScript intégrés à la page (par exemple, un widget de chat intégré ou<iframe>) et les suites de tests automatisés qui doivent découvrir et appeler des outils directement dans le contexte de la page.
Enregistrement et cycle de vie
Les outils sont enregistrés en transmettant un objet de définition d'outil et un objet d'options facultatif (ModelContextRegisterToolOptions), qui accepte deux membres facultatifs :
signal:AbortSignalutilisé pour désenregistrer l'outil lorsqu'il n'est plus nécessaire.exposedTo: tableau de chaînes d'origine contrôlant quels documents de l'arborescence de documents sont autorisés à découvrir et à exécuter l'outil au-delà des limites du frame.
Gestion du cycle de vie avec AbortController
WebMCP ne fournit pas de méthode unregisterTool(). Pour annuler l'enregistrement d'un outil, vous devez transmettre un AbortSignal lors de l'enregistrement et annuler ce signal lorsque l'outil n'est plus nécessaire.
const controller = new AbortController();
await document.modelContext.registerTool({
name: "get_user_preferences",
description: "Retrieves the user's saved preferences.",
inputSchema: { type: "object", properties: {} },
execute() {
const prefs = localStorage.getItem("user_prefs");
return prefs ? JSON.parse(prefs) : { theme: "light" };
},
annotations: { readOnlyHint: true }
}, { signal: controller.signal });
// To unregister the tool (e.g., on component unmount):
controller.abort();
L'abandon du signal d'enregistrement ne fait que désenregistrer l'outil. Il n'annule pas les exécutions déjà en cours. Les outils qui lancent des tâches de longue durée doivent également respecter le signal par exécution décrit ci-dessous. Sinon, la désinscription laissera des requêtes orphelines et des mises à jour d'UI obsolètes.
Contrôler l'exposition des outils interorigines avec exposedTo
Par défaut, un outil enregistré est privé pour le document d'enregistrement et les frames de même origine. Lorsque votre application est intégrée dans un <iframe> (ou intègre des widgets tiers) et que vous avez besoin de frames ancêtres ou frères pour découvrir et appeler l'outil, spécifiez les origines autorisées à l'aide de exposedTo.
const controller = new AbortController();
await document.modelContext.registerTool({
name: "get_cart_items",
description: "Retrieves items currently in the user's shopping cart.",
inputSchema: { type: "object", properties: {} },
async execute(input, { signal }) {
return await cartService.getItems({ signal });
},
annotations: { readOnlyHint: true },
}, {
signal: controller.signal,
exposedTo: ["https://checkout.example.com"],
});
- Handshake d'activation mutuelle : la découverte d'outils interorigines est une activation bidirectionnelle. Le frame du fournisseur d'outils autorise l'origine du consommateur via
exposedTo, et le frame du consommateur doit explicitement demander l'origine du fournisseur viagetTools({ fromOrigins: [...] }). SansfromOrigins,getTools()n'interroge que les documents de même origine. - Correspondance d'origine : le justificatif d'immatriculation a toujours accès à sa propre origine. Les origines spécifiées dans
exposedTosont autorisées à découvrir l'outil viadocument.modelContext.getTools({ fromOrigins: [...] })et à l'exécuter viadocument.modelContext.executeTool(). - Exigence de sécurité : chaque origine dans
exposedToDOIT être potentiellement fiable (HTTPS ou localhost). Si vous transmettez une origine non valide ou non sécurisée,registerTool()est rejeté avec une erreurSecurityError. - Champ d'application :
exposedTocontrôle la découverte de documents croisés sur la page dans les frames de l'arborescence du document.
Définir des paramètres
Les paramètres (params) sont définis à l'aide de la propriété inputSchema. Il doit s'agir d'un objet de schéma JSON décrivant les données structurées attendues par l'outil.
await document.modelContext.registerTool({
name: "calculate_area",
description: "Calculates the area of a rectangle.",
inputSchema: {
type: "object",
properties: {
width: { type: "number", description: "The width of the rectangle." },
height: { type: "number", description: "The height of the rectangle." }
},
required: ["width", "height"]
},
execute(input) {
// input is { width: 10, height: 20 }
return input.width * input.height;
},
annotations: { readOnlyHint: true }
});
Rédiger des descriptions d'outils et de paramètres
L'agent reçoit déjà l'intégralité de inputSchema. Par conséquent, description n'a qu'à indiquer ce que fait l'outil et quand l'utiliser. NE RÉPÉTEZ PAS les noms, types, valeurs enum, valeurs par défaut ou limites des paramètres dans la description. Déclarez-les plutôt dans le schéma (enum, default, minimum/maximum). Les descriptions des paramètres suivent la même règle : indiquez ce que signifie la valeur, et non son type ou les valeurs autorisées.
Le navigateur ne valide pas les arguments par rapport à inputSchema et n'applique pas les valeurs default. Vous devez donc effectuer ces deux opérations dans execute.
const SORT_ORDERS = ["newest", "price_low_to_high"];
await document.modelContext.registerTool({
name: "search_products",
// Don't list the sort orders here, even via SORT_ORDERS: `enum` already does.
description: "Searches the product catalog by keyword. Use when the user wants to find or compare products.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Words to search for." },
sort: { type: "string", enum: SORT_ORDERS, default: "newest", description: "Order of the results." },
},
required: ["query"],
},
async execute({ query, sort = "newest" }, { signal }) {
if (typeof query !== "string" || !SORT_ORDERS.includes(sort)) {
return { error: "Pass a search query and a supported sort order.", sortOrders: SORT_ORDERS };
}
return await productService.search({ query, sort }, { signal });
},
annotations: { readOnlyHint: true },
});
Modèles d'exécution
Le rappel execute reçoit toujours deux arguments : l'objet d'entrée et un objet d'options contenant un AbortSignal : execute(inputObject, { signal }).
Dans quel contexte utiliser async execute ?
Utilisez async lorsque l'outil implique des opérations qui renvoient une promesse ou prennent du temps à s'exécuter :
- Appels réseau : récupération de données à partir d'une API.
- Stockage asynchrone : accès à IndexedDB.
- Événements externes : attente de la fin d'un changement d'état ou d'une animation spécifique.
async execute(input, { signal }) {
const response = await fetch(`/api/data/${input.id}`, { signal });
return await response.json();
}
Quand utiliser execute (synchrone)
Utilisez une fonction synchrone standard pour les opérations immédiates. Un outil synchrone s'exécute jusqu'à la fin avant que l'annulation ne puisse être observée. Il peut donc ignorer le signal :
- Logique pure : calculs mathématiques, filtrage ou tri des données déjà en mémoire.
- État synchrone : lecture à partir de
localStorageou d'un gestionnaire d'état synchrone.
execute(input) {
return input.items.filter(item => item.active);
}
Gérer les erreurs dans execute
Lorsque execute génère une exception (ou renvoie une promesse refusée), l'agent reçoit uniquement un échec d'exécution générique. Le message d'erreur n'est pas transmis à l'agent. Si votre outil rencontre une erreur exploitable ou récupérable (par exemple, une entrée non valide, des éléments manquants ou un état HTTP d'échec), NE LANCEZ PAS throw new Error(...). Renvoyez plutôt une charge utile d'erreur structurée via la valeur de retour normale de l'outil afin que l'agent IA reçoive les détails du diagnostic et puisse s'autocorriger ou réessayer :
execute({ itemName }) {
const item = menuItems.find((entry) => entry.name.toLowerCase() === itemName.toLowerCase());
if (!item) {
// Return the error as a value instead of throwing, so the agent receives the message:
return {
error: `Menu item "${itemName}" not found.`,
availableItems: menuItems.map((entry) => entry.name),
};
}
return item;
}
Ne relancez l'exception que lorsque l'exécution a été annulée (signal.aborted est true).
Annuler les exécutions en cours
L'annulation est initiée par l'utilisateur ou l'agent. OBLIGATOIRE : Tout outil qui effectue des requêtes réseau ou d'autres tâches asynchrones de longue durée DOIT respecter signal. Sinon, les exécutions annulées continuent de consommer des ressources et peuvent écrire des données obsolètes dans l'interface utilisateur une fois que l'agent est passé à autre chose.
- API qui acceptent un signal (lecteurs
fetch(),addEventListener(),ReadableStream) : transmettez directementsignal. - API qui ne le font pas (
setTimeout,setInterval, boucles d'interrogation, pipelines lourdsawait) : appelezsignal.throwIfAborted()entre les étapes et/ou rejetez sursignal.addEventListener('abort', ...). Les minuteurs n'acceptent aucun signal. Vous devez donc les effacer vous-même dans l'écouteur d'abandon.
await document.modelContext.registerTool({
name: "fetch_tool",
description: "Fetch the text content of a URL and show it on the page.",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "The URL to fetch" },
priority: {
type: "string",
enum: ["high", "low", "auto"],
description: "Relative network priority for the request",
},
},
required: ["url"],
},
execute: async ({ url, priority }, { signal }) => {
const output = document.querySelector("pre");
output.textContent = "Loading…";
try {
// Passing `signal` aborts the network request the moment execution is cancelled.
const response = await fetch(url, { priority, signal });
if (!response.ok) {
output.textContent = "";
return { error: `Request failed with status ${response.status}` };
}
const text = await response.text();
output.textContent = text;
return text;
} catch (err) {
// Roll back the UI state this execution set, so a cancelled run leaves no trace.
output.textContent = "";
if (signal.aborted) {
// The execution was cancelled: re-throw instead of returning an error value.
throw err;
}
// Return other errors as a value instead of throwing, so the agent receives the message.
return { error: err instanceof Error ? err.message : String(err) };
}
},
annotations: { readOnlyHint: true }
});
Signaler des outils importants
Depuis Chrome 154, annotations est compatible avec consequentialHint. Définissez-le sur true pour les outils qui effectuent des actions à fort enjeu, irréversibles ou réelles (réserver un voyage, transférer de l'argent, envoyer des messages ou supprimer des données). Il indique aux agents de demander une confirmation explicite de l'utilisateur avant l'exécution.
await document.modelContext.registerTool({
name: 'book_flight',
description: 'Book a flight for the user with confirmed flight details.',
inputSchema: {
type: 'object',
properties: {
flightId: { type: 'string', description: 'ID of the flight to book' },
passengers: { type: 'number', description: 'Number of tickets to purchase' },
},
required: ['flightId', 'passengers'],
},
execute: async ({ flightId, passengers }, { signal }) => {
// Forward `signal` to the transaction API to honor cancellation:
return await bookFlightReservation({ flightId, passengers }, { signal });
},
annotations: {
readOnlyHint: false, // This tool mutates state (it makes a booking).
consequentialHint: true, // Spends money and is not trivially reversible.
untrustedContentHint: false, // Output is first-party data, not third-party content.
}
});
OBLIGATOIRE : Traitez consequentialHint comme une indication, et non comme un mécanisme d'application. Il ne bloque pas l'exécution de lui-même, et les navigateurs antérieurs à l'indice n'afficheront aucune invite. Conservez la confirmation intégrée à l'application que l'action justifie déjà.
Outils de débogage des indicateurs
Depuis Chrome 156, annotations est compatible avec debugging. Définissez annotations.debugging sur true pour les outils conçus pour le débogage et les outils pour les développeurs plutôt que pour les tâches de l'utilisateur final. Exemples : vider l'état interne de l'application, activer/désactiver des flags de fonctionnalité ou amorcer des données de test. Cela permet aux agents de distinguer les outils réservés aux développeurs de ceux destinés à agir pour l'utilisateur. Sa valeur par défaut est false. Les agents intégrés à la page et les environnements de test peuvent le lire à partir de RegisteredTool.annotations.debugging via getTools().
await document.modelContext.registerTool({
name: "dump_app_state",
description: "Returns a snapshot of the internal application store for debugging.",
execute() {
return appStore.getState();
},
annotations: {
readOnlyHint: true, // Only reads state.
debugging: true, // Developer tooling, not an end-user action.
}
});
Agents et tests sur la page (getTools et executeTool)
Les pages Web n'ont pas besoin de getTools() ni de executeTool() pour exposer des outils aux agents IA. Il suffit d'appeler document.modelContext.registerTool() pour que les agents de navigateur, les extensions de navigateur, les outils de développement et les assistants au niveau du système puissent découvrir et exécuter vos outils hors bande.
Les méthodes getTools() et executeTool() existent spécifiquement pour les consommateurs exécutés dans l'environnement JavaScript :
- Agents JavaScript intégrés à la page : un assistant IA, un widget Copilot ou une interface de chat intégrée directement à la page (ou s'exécutant dans un
<iframe>) qui doit découvrir les outils proposés par la page environnante ou les frames accessibles dans l'arborescence du document, et les appeler au nom de l'utilisateur. - Tests et débogage automatisés : suites de tests (comme Playwright, Vitest ou des tests unitaires dans le navigateur) qui inspectent les métadonnées des outils enregistrés et simulent les appels d'outils de l'agent sans nécessiter de harnais LLM externe actif.
Découvrir les outils disponibles avec getTools()
Les agents intégrés à la page appellent document.modelContext.getTools() pour récupérer un tableau d'objets RegisteredTool à partir du document actuel et de tous les cadres accessibles dans l'arborescence du document.
Depuis Chrome 155, RegisteredTool.inputSchema est renvoyé directement en tant qu'objet JavaScript (une copie complète du schéma fourni lors de l'enregistrement). La modification de cet objet ne modifie pas le schéma réel de l'outil enregistré.
// Example: An in-page AI assistant discovering tools available on the page:
const tools = await document.modelContext.getTools();
// In-page agents can listen for tools being registered or unregistered dynamically:
document.modelContext.addEventListener("toolchange", async () => {
const updatedTools = await document.modelContext.getTools();
console.log(`Active tools updated: ${updatedTools.map((t) => t.name).join(", ")}`);
});
Pour découvrir les outils enregistrés dans des iFrames descendantes interorigines (qui doivent explicitement accorder l'accès via exposedTo lors de l'enregistrement), transmettez fromOrigins dans le dictionnaire d'options :
// Query tools registered in the embedded cart frame that exposed them to this checkout origin:
const cartTools = await document.modelContext.getTools({
fromOrigins: ["https://cart.example.com"],
});
Appeler des outils avec executeTool()
Les agents intégrés à la page et les scripts de test appellent document.modelContext.executeTool(tool, inputObject, options) pour exécuter un outil enregistré. L'outil s'exécute sur le document où il a été enregistré, et la promesse se résout en un résultat sous forme de chaîne renvoyé par le rappel execute de l'outil.
Depuis Chrome 155, executeTool() accepte directement un objet JavaScript brut pour inputObject, ce qui correspond au format d'entrée reçu par le rappel execute de votre outil.
// 1. Discover the tool (typically done during agent initialization or test setup):
const tools = await document.modelContext.getTools();
const areaTool = tools.find((t) => t.name === "calculate_area");
if (areaTool) {
const controller = new AbortController();
// Example input payload matching the tool's inputSchema:
const inputObject = { width: 10, height: 20 };
const options = { signal: controller.signal };
// 2. Pass input arguments as a JavaScript object, with a JSON string fallback for Chrome <155:
let resultString;
try {
resultString = await document.modelContext.executeTool(areaTool, inputObject, options);
} catch (err) {
if (err?.message?.startsWith("Failed to parse input")) {
resultString = await document.modelContext.executeTool(
areaTool,
JSON.stringify(inputObject),
options
);
} else {
// Re-throw real tool execution or cancellation errors:
throw err;
}
}
console.log("Execution output string:", resultString);
}
La promesse renvoyée est rejetée si :
inputObjectn'est pas un objet ou ne peut pas être sérialisé en chaîne JSON (par exemple, références circulaires ou valeursBigInt). Notez que les valeurs omises par la sérialisation JSON (comme les fonctions etundefined) seront omises de l'objet reçu par l'outil, plutôt que de provoquer un rejet.- L'exécution est annulée via
options.signal. - L'outil a été désenregistré avant l'exécution ou a rencontré une erreur d'exécution non gérée.
Modèle Tool Factory
Pour transmettre du contexte (comme des magasins ou des instances d'application) à vos outils, utilisez des fonctions de fabrique.
export function createInventoryTool(inventoryManager) {
return {
name: "get_inventory",
description: "Lists items in the inventory.",
inputSchema: { type: "object", properties: {} },
execute() {
return inventoryManager.getItems();
},
annotations: { readOnlyHint: true }
};
}
Types TypeScript
Dans les projets TypeScript, NE rédigez PAS de déclarations pour document.modelContext. Installez le package webmcp-types officiel (npm install --save-dev webmcp-types) et ajoutez-le à tsconfig.json, car il n'est pas récupéré automatiquement :
{
"compilerOptions": {
"types": ["webmcp-types"]
}
}
document.modelContextest défini comme facultatif. Protégez-le donc avecif (document.modelContext)avant de l'utiliser.- Avec un
inputSchemaintégré, le type d'entréeexecuteest déduit du schéma. - Les types nommés tels que
WebMCP.ModelContextToolsont globaux (aucune importation n'est nécessaire) et sont utiles pour saisir des fabriques d'outils.
Compatibilité avec les frameworks
OBLIGATOIRE : Si l'application utilise React, Angular ou Vue, enregistrez les outils via l'intégration WebMCP officielle de ce framework, listée ci-dessous. N'appelez PAS document.modelContext.registerTool() à partir de composants et n'écrivez pas votre propre wrapper de cycle de vie. Chaque intégration gère déjà la détection des fonctionnalités, le nettoyage de AbortSignal et la définition du champ d'application des outils au cycle de vie du composant ou de l'injecteur. Ces API sont expérimentales. Avant d'écrire du code, recherchez la signature actuelle dans les types ou la documentation du package.
| Framework | Package | API | Docs |
|---|---|---|---|
| React 18+ | use-webmcp-tool |
useWebMCP(options) : un appel par outil |
npmjs.com/package/use-webmcp-tool |
| Angular | @angular/core, @angular/forms/signals |
provideExperimentalWebMcpTools([...]) (fournisseurs d'applications ou d'itinéraires), declareExperimentalWebMcpTool(...) (services internes), provideExperimentalWebMcpForms() + l'option experimentalWebMcpTool sur les formulaires Signal |
angular.dev/ai/webmcp |
| Vue | @vueuse/core |
Composable useWebMCP(options) |
vueuse.org/core/usewebmcp |
Toutes les règles de ce guide s'appliquent toujours lorsque vous utilisez ces intégrations :
- Conservez la même forme de définition d'outil :
name,description,inputSchema,execute(input, { signal })etannotations. - Suivez les règles concernant les descriptions et les annotations.
- Validez l'entrée et appliquez les valeurs par défaut dans
execute. - Transmettez
signalà une tâche de longue durée. - Pour ne proposer un outil que de temps en temps, transmettez
enabled(React, Vue). NE PAS encapsuler l'appel de hook ou de composable dans une condition. - Les intégrations peuvent encapsuler les résultats et les erreurs (par exemple, en tant que
{ content, isError }). Consultez la documentation de l'intégration avant de vous fier à la manière dont les erreurs parviennent à l'agent.
Remarques relatives à l'API
- annotations (facultatif) : dictionnaire pour les métadonnées de l'outil.
- readOnlyHint : (facultatif) définissez cette option sur
truesi l'outil ne modifie aucun état et ne lit que les données. Cela aide les agents à décider quand appeler l'outil en toute sécurité. - consequentialHint : (facultatif, Chrome 154 ou version ultérieure) Définissez cette option sur
truepour les actions à fort enjeu, irréversibles ou réelles, afin que les agents demandent d'abord la confirmation de l'utilisateur. - untrustedContentHint : (facultatif) définissez cette option sur
truelorsque l'outil renvoie du contenu que votre site ne contrôle pas, comme du texte généré par l'utilisateur ou des réponses de l'API tierces. - debugging (facultatif, Chrome 156 ou version ultérieure) : définissez cette valeur sur
truelorsque l'outil est destiné au débogage et aux outils pour les développeurs, et non aux tâches de l'utilisateur final. La valeur par défaut estfalse.
- readOnlyHint : (facultatif) définissez cette option sur
- Options d'inscription :
- signal : (facultatif)
AbortSignalutilisé pour annuler l'enregistrement de l'outil lorsqu'il n'est plus nécessaire. - exposedTo : (facultatif) tableau de chaînes d'origine sécurisées contrôlant quels documents de l'arborescence de documents sont autorisés à découvrir et à exécuter l'outil au-delà des limites des frames.
- signal : (facultatif)
- Options de découverte d'outils (
ModelContextGetToolOptions) :- fromOrigins (facultatif) : tableau de chaînes d'origine sécurisées à interroger dans les frames accessibles. L'appel de
getTools()sansfromOriginsn'interroge que les documents de même origine.
- fromOrigins (facultatif) : tableau de chaînes d'origine sécurisées à interroger dans les frames accessibles. L'appel de
- Format de retour : la fonction
executepeut renvoyer n'importe quelle valeur sérialisable au format JSON (objet, tableau, chaîne, nombre, booléen). Le message d'une exception générée à partir deexecuten'est pas transmis à l'agent. Par conséquent, renvoyez les erreurs exploitables en tant que charges utiles d'erreur structurées (par exemple,{ error: "..." }) via la valeur de retour de l'outil afin que l'agent puisse les lire et agir en conséquence. - Contexte sécurisé : WebMCP nécessite HTTPS. Toutes les origines de
exposedToetfromOriginsdoivent également être potentiellement fiables. - Obsolètes/Supprimés :
navigator.modelContext(obsolète dans Chromium 150),unregisterTool(),provideContext()etclearContext()ne sont plus acceptés.
Stratégies de remplacement
L'API impérative WebMCP doit être utilisée avec la détection de fonctionnalités pour assurer la compatibilité avec les navigateurs qui ne sont pas encore compatibles avec WebMCP.
if ('modelContext' in document) {
// Register tools
}