Agentic JavaScript-tools

Webfuncties: document.modelContext

De Imperative API gebruikt document.modelContext.registerTool() om JavaScript-tools programmatisch te definiëren. Dit is ideaal voor apps met één pagina (SPA's) waar tools moeten worden toegevoegd of verwijderd op basis van de huidige route of gebruikersstatus.

WebMCP werkt op een producent/consument-architectuur:

  • Webpagina's zijn toolproducenten: Je definieert en registreert tools met document.modelContext.registerTool(). Dat is alles wat een webpagina moet implementeren. Je hoeft getTools() of executeTool() niet aan te roepen op de pagina om tools beschikbaar te maken voor agents.
  • Browser- en extensie-agents zijn de primaire consumenten: Agents die worden geleverd door of via de browser, waaronder de ingebouwde agent van de browser, browserextensies, DevTools en assistenten op systeemniveau, ontdekken en roepen geregistreerde tools out-of-band aan via interne platformmechanismen.
  • In-page agents en test harnesses: getTools() en executeTool() bestaan specifiek voor in-page JavaScript-assistenten (bijv. een ingesloten chatwidget of <iframe>) en geautomatiseerde testpakketten die tools rechtstreeks in de paginacontext moeten ontdekken en aanroepen.

Registratie en levenscyclus

Tools worden geregistreerd door een tooldefinitieobject en een optioneel optieobject (ModelContextRegisterToolOptions) door te geven. Dit ondersteunt 2 optionele leden:

  • signal: Een AbortSignal die wordt gebruikt om de registratie van de tool ongedaan te maken als deze niet meer nodig is.
  • exposedTo: Een matrix van oorsprongstekenreeksen die bepalen welke documenten in de documentstructuur de tool mogen ontdekken en uitvoeren over framegrenzen heen.

Levenscyclusbeheer met AbortController

WebMCP biedt geen unregisterTool()-methode. Als je de registratie van een tool ongedaan wilt maken, moet je tijdens de registratie een AbortSignal doorgeven en dat signaal afbreken als de tool niet meer nodig is.

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();

Als je het registratiesignaal afbreekt, wordt de registratie van de tool ongedaan gemaakt. Uitvoeringen die al lopen, worden niet geannuleerd. Tools die langdurig werk starten, moeten ook het signaal per uitvoering respecteren dat hieronder wordt beschreven. Anders blijven er bij het ongedaan maken van de registratie verweesde verzoeken en verouderde UI-updates achter.

Blootstelling van cross-origin tools beheren met exposedTo

Een geregistreerde tool is standaard privé voor het registrerende document en frames van dezelfde oorsprong. Als je app is ingesloten in een <iframe> (of widgets van derden insluit) en je bovenliggende of onderliggende frames nodig hebt om de tool te vinden en aan te roepen, geef je de toegestane herkomsten op met 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"],
});
  • Wederzijdse aanmelding: Cross-origin toolontdekking is een tweerichtingsaanmelding. Het frame van de toolprovider autoriseert de oorsprong van de consument via exposedTo en het consumentenframe moet die oorsprong van de provider expliciet aanvragen via getTools({ fromOrigins: [...] }). Zonder fromOrigins voert getTools() alleen query's uit op documenten met dezelfde oorsprong.
  • Oorsprongovereenkomst: De eigen oorsprong van het registratiedocument heeft altijd toegang. Oorsprongen die zijn opgegeven in exposedTo krijgen toestemming om de tool te ontdekken via document.modelContext.getTools({ fromOrigins: [...] }) en uit te voeren via document.modelContext.executeTool().
  • Beveiligingsvereiste: Elke oorsprong in exposedTo MOET potentieel betrouwbaar zijn (HTTPS of localhost). Als je een ongeldige of niet-beveiligde oorsprong doorgeeft, wordt registerTool() afgewezen met een SecurityError.
  • Bereik: exposedTo beheert de detectie van meerdere documenten op de pagina in frames in de documentstructuur.

Parameters definiëren

Parameters (params) worden gedefinieerd met de eigenschap inputSchema. Dit moet een JSON-schema-object zijn dat de gestructureerde gegevens beschrijft die de tool verwacht.

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 }
});

Beschrijvingen van schrijftools en parameters

De agent krijgt de volledige inputSchema al, dus de description hoeft alleen te zeggen wat de tool doet en wanneer deze moet worden gebruikt. Herhaal GEEN parameternamen, typen, enum-waarden, standaardwaarden of limieten. Declareer ze in plaats daarvan in het schema (enum, default, minimum/maximum). Voor parameterbeschrijvingen geldt dezelfde regel: zeg wat de waarde betekent, niet het type of de toegestane waarden.

De browser valideert geen argumenten tegen inputSchema en past geen default-waarden toe. Doe beide in 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 },
});

Uitvoeringspatronen

De callback execute krijgt altijd 2 argumenten: het invoerobject en een optieobject met een AbortSignal: execute(inputObject, { signal }).

Wanneer te gebruiken async execute

Gebruik async als de tool bewerkingen omvat die een Promise retourneren of tijd kosten om te voltooien:

  • Netwerkaanroepen: Gegevens ophalen uit een API.
  • Asynchrone opslag: Toegang tot IndexedDB.
  • Externe gebeurtenissen: Wachten tot een specifieke statuswijziging of animatie is afgerond.
async execute(input, { signal }) {
  const response = await fetch(`/api/data/${input.id}`, { signal });
  return await response.json();
}

Wanneer u execute (synchroon) gebruikt

Gebruik een standaard synchrone functie voor onmiddellijke bewerkingen. Een synchrone tool wordt uitgevoerd tot deze is voltooid voordat de annulering kan worden waargenomen. De tool kan het signaal dus negeren:

  • Pure logica: Wiskunde, filteren of sorteren van gegevens die al in het geheugen staan.
  • Synchrone status: Lezen uit localStorage of een synchrone statusbeheerder.
execute(input) {
  return input.items.filter(item => item.active);
}

Fouten in execute afhandelen

Als execute een fout genereert (of een afgewezen promise retourneert), krijgt de agent alleen een algemene uitvoeringsfout. De foutmelding wordt niet doorgestuurd naar de medewerker. Als uw tool een actiegerichte of herstelbare fout tegenkomt (zoals ongeldige invoer, ontbrekende items of een mislukte HTTP-status), MOET U NIET throw new Error(...). Retourneer in plaats daarvan een gestructureerde foutpayload via de normale retourwaarde van de tool, zodat de AI-agent de diagnostische gegevens krijgt en zichzelf kan corrigeren of het opnieuw kan proberen:

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;
}

Alleen opnieuw genereren als de uitvoering is geannuleerd (signal.aborted is true).

Uitvoeringen die bezig zijn annuleren

De annulering wordt gestart door de gebruiker of de medewerker. VERPLICHT: Elke tool die netwerkverzoeken of ander langdurig asynchroon werk uitvoert, MOET signal respecteren. Anders blijven geannuleerde uitvoeringen resources verbruiken en kunnen ze verouderde gegevens in de UI schrijven nadat de agent is verdergegaan.

  • API's die een signaal accepteren (fetch(), addEventListener(), ReadableStream lezers): geef signal rechtstreeks door.
  • API's die dat niet doen (setTimeout, setInterval, polling-loops, await-zware pipelines): roep signal.throwIfAborted() aan tussen stappen en/of weiger op signal.addEventListener('abort', ...). Timers accepteren geen signaal, dus je moet ze zelf wissen in de abort-listener.
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 }
});

Tools met gevolgen markeren

Sinds Chrome 154 ondersteunt annotations consequentialHint. Stel deze in op true voor tools die acties met een hoog risico, onomkeerbare acties of acties in de echte wereld uitvoeren, zoals reizen boeken, geld overmaken, berichten sturen of gegevens verwijderen. Hiermee wordt aangegeven dat agents expliciete gebruikersbevestiging moeten vragen voordat ze worden uitgevoerd.

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.
  }
});

VERPLICHT: Behandel consequentialHint als een hint, niet als een handhavingsmechanisme. De hint blokkeert de uitvoering niet zelf en browsers die ouder zijn dan de hint, geven helemaal geen prompt. Behoud de in-app bevestiging die de actie al vereist.

Tools voor foutopsporing bij markeren

Sinds Chrome 156 ondersteunt annotations debugging. Stel annotations.debugging in op true voor tools die zijn gemaakt voor foutopsporing en tools voor ontwikkelaars in plaats van taken voor eindgebruikers. Voorbeelden: de interne app-status dumpen, functieflags aan- of uitzetten of testgegevens seeden. Zo kunnen agents tools die alleen voor ontwikkelaars zijn, onderscheiden van tools die bedoeld zijn om voor de gebruiker te handelen. De standaardwaarde is false. In-page agents en test harnesses kunnen dit lezen vanuit 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.
  }
});

In-page agents en testen (getTools en executeTool)

Webpagina's hebben geen getTools() of executeTool() nodig om tools beschikbaar te maken voor AI-agents. Je hoeft alleen document.modelContext.registerTool() aan te roepen om browseragents, browserextensies, DevTools en assistenten op systeemniveau je tools out-of-band te laten ontdekken en uitvoeren.

De methoden getTools() en executeTool() zijn specifiek bedoeld voor consumenten die de JavaScript-omgeving gebruiken:

  1. In-page JavaScript-agents: Een AI-assistent, copilot-widget of chatinterface die rechtstreeks in de pagina is ingesloten (of wordt uitgevoerd in een <iframe>) en die moet ontdekken welke tools de omringende pagina of toegankelijke frames in de documentstructuur bieden en deze namens de gebruiker moet aanroepen.
  2. Geautomatiseerd testen en fouten opsporen: Testpakketten (zoals Playwright, Vitest of unittests in de browser) die geregistreerde toolmetadata inspecteren en aanroepen van agenttools simuleren zonder dat er een live externe LLM-harness nodig is.

Beschikbare tools ontdekken met getTools()

In-page agents roepen document.modelContext.getTools() aan om een array van RegisteredTool-objecten op te halen uit het huidige document en alle toegankelijke frames in de documentstructuur.

Sinds Chrome 155 wordt RegisteredTool.inputSchema rechtstreeks geretourneerd als een JavaScript-object (een diepe kopie van het schema dat bij de registratie is geleverd). Als je dit object muteert, verandert het werkelijke schema van de geregistreerde tool niet.

// 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(", ")}`);
});

Als je tools wilt ontdekken die zijn geregistreerd in cross-origin onderliggende iframes (die expliciet toegang moeten verlenen via exposedTo tijdens de registratie), geef je fromOrigins door in de optiewoordenlijst:

// 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"],
});

Tools aanroepen met executeTool()

In-page agents en testscripts roepen document.modelContext.executeTool(tool, inputObject, options) aan om een geregistreerde tool uit te voeren. De tool wordt uitgevoerd op het document waarin deze is geregistreerd en de promise wordt omgezet in het resultaat in tekenreeksvorm dat wordt geretourneerd door de callback execute van de tool.

Sinds Chrome 155 accepteert executeTool() een gewoon JavaScript-object rechtstreeks voor inputObject, wat overeenkomt met de invoerindeling die de callback execute van je tool krijgt.

// 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);
}

De geretourneerde promise wordt afgewezen als:

  • inputObject is geen object of kan niet worden geserialiseerd naar een json-tekenreeks (bijv. circulaire verwijzingen of BigInt-waarden). Waarden die worden weggelaten door json-serialisatie (zoals functies en undefined) worden weggelaten uit het object dat door de tool wordt ontvangen in plaats van dat ze een afwijzing veroorzaken.
  • De uitvoering is afgebroken via options.signal.
  • De registratie van de tool is ongedaan gemaakt vóór de uitvoering of er is een onverwerkte runtimefout opgetreden.

Tool Factory Pattern

Gebruik factory-functies om context (zoals winkels of app-instanties) door te geven aan je tools.

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 }
  };
}

TypeScript-typen

Schrijf in TypeScript-projecten GEEN handgeschreven declaraties voor document.modelContext. Installeer het officiële webmcp-types-pakket (npm install --save-dev webmcp-types) en voeg het toe aan tsconfig.json, omdat het niet automatisch wordt opgehaald:

{
  "compilerOptions": {
    "types": ["webmcp-types"]
  }
}
  • document.modelContext is getypt als optioneel, dus beveilig het met if (document.modelContext) vóór gebruik.
  • Bij een inline inputSchema wordt het invoertype execute afgeleid van het schema.
  • Benoemde typen zoals WebMCP.ModelContextTool zijn algemeen (geen import nodig) en zijn handig voor het typen van toolfabrieken.

Ondersteuning voor frameworks

VERPLICHT: Als de app React, Angular of Vue gebruikt, registreer je tools via de officiële WebMCP-integratie van dat framework, die hieronder wordt vermeld. Roep document.modelContext.registerTool() NIET aan vanuit componenten en schrijf geen eigen levenscycluswrapper. Elke integratie verwerkt al functiedetectie, AbortSignal-opschoning en scopingtools voor de levenscyclus van de component of injector. Deze API's zijn experimenteel. Zoek daarom de huidige handtekening op in de typen of documenten van het pakket voordat je code schrijft.

Framework Pakket API Documenten
React 18+ use-webmcp-tool useWebMCP(options): één aanroep per tool npmjs.com/package/use-webmcp-tool
Angular @angular/core, @angular/forms/signals provideExperimentalWebMcpTools([...]) (app- of routeproviders), declareExperimentalWebMcpTool(...) (interne services), provideExperimentalWebMcpForms() + de optie experimentalWebMcpTool op signaalformulieren angular.dev/ai/webmcp
Vue @vueuse/core useWebMCP(options) composable vueuse.org/core/usewebmcp

Alle regels in deze gids zijn nog steeds van toepassing als je deze integraties gebruikt:

  • Houd de vorm van de tooldefinitie hetzelfde: name, description, inputSchema, execute(input, { signal }) en annotations.
  • Volg de regels voor beschrijvingen en annotaties.
  • Valideer de invoer en pas de standaardwaarden toe in execute.
  • Geef signal door aan langlopend werk.
  • Als je een tool maar een deel van de tijd wilt aanbieden, geef je enabled door (React, Vue). Verpak de hook of composable aanroep NIET in een voorwaarde.
  • Integraties kunnen resultaten en fouten verpakken (bijvoorbeeld als { content, isError }). Controleer de documentatie van de integratie voordat je erop vertrouwt hoe fouten de agent bereiken.

API-notities

  • annotations: (Optioneel) Een woordenboek voor toolmetadata.
    • readOnlyHint: (Optioneel) Stel deze in op true als de tool geen status wijzigt en alleen gegevens leest. Zo kunnen agents bepalen wanneer het veilig is om de tool aan te roepen.
    • consequentialHint: (Optioneel, Chrome 154+) Stel in op true voor acties met grote gevolgen, onomkeerbare acties of acties in de echte wereld, zodat agents eerst om bevestiging van de gebruiker vragen.
    • untrustedContentHint: (Optioneel) Stel deze in op true als de tool content retourneert waarover je site geen controle heeft, zoals door gebruikers gegenereerde tekst of API-reacties van derden.
    • debugging: (Optioneel, Chrome 156+) Stel in op true als de tool is bedoeld voor foutopsporing en tools voor ontwikkelaars, niet voor taken van eindgebruikers. De standaardwaarde is false.
  • Registratieopties:
    • signal: (Optioneel) Een AbortSignal die wordt gebruikt om de registratie van de tool ongedaan te maken als deze niet meer nodig is.
    • exposedTo: (Optioneel) Een matrix van beveiligde oorsprongstekenreeksen die bepalen welke documenten in de documentstructuur de tool mogen ontdekken en uitvoeren over framegrenzen heen.
  • Opties voor toolontdekking (ModelContextGetToolOptions):
    • fromOrigins: (Optioneel) Een matrix van beveiligde oorsprongstekenreeksen om query's uit te voeren in toegankelijke frames. Als je getTools() aanroept zonder fromOrigins, worden alleen documenten met dezelfde oorsprong opgevraagd.
  • Retourindeling: De functie execute kan elke JSON-serialiseerbare waarde retourneren (object, matrix, tekenreeks, getal, booleaans). Het bericht van een uitzondering die wordt gegenereerd door execute, wordt niet doorgestuurd naar de agent. Retourneer daarom actiegerichte fouten als gestructureerde foutpayloads (bijvoorbeeld { error: "..." }) via de retourwaarde van de tool, zodat de agent deze kan lezen en erop kan reageren.
  • Beveiligde context: WebMCP vereist HTTPS. Alle oorsprongen in exposedTo en fromOrigins moeten ook potentieel betrouwbaar zijn.
  • Beëindigd/verwijderd: navigator.modelContext (beëindigd in Chromium 150), unregisterTool(), provideContext() en clearContext() worden niet meer ondersteund.

Reservestrategieën

De WebMCP Imperative API moet worden gebruikt met functiedetectie om compatibiliteit te garanderen met browsers die WebMCP nog niet ondersteunen.

if ('modelContext' in document) {
  // Register tools
}