Strumenti JavaScript agentici

Funzionalità web: document.modelContext

L'API Imperative utilizza document.modelContext.registerTool() per definire in modo programmatico gli strumenti JavaScript. Questa opzione è ideale per le applicazioni a pagina singola (SPA) in cui è necessario aggiungere o rimuovere strumenti in base al percorso o allo stato dell'utente corrente.

WebMCP opera su un'architettura producer/consumer:

  • Le pagine web sono produttori di strumenti: definisci e registri gli strumenti utilizzando document.modelContext.registerTool(). Questo è tutto ciò che una pagina web deve implementare. Non è necessario chiamare getTools() o executeTool() nella pagina per esporre gli strumenti agli agenti.
  • Gli agenti del browser e delle estensioni sono i consumatori principali: gli agenti forniti dal browser o tramite il browser, tra cui l'agente integrato del browser, le estensioni del browser, DevTools e gli assistenti a livello di sistema, rilevano e richiamano gli strumenti registrati out-of-band tramite meccanismi della piattaforma interna.
  • Agenti in-page e test harness: getTools() e executeTool() esistono specificamente per gli assistenti JavaScript in-page (ad es. un widget di chat incorporato o <iframe>) e le suite di test automatizzate che devono rilevare e richiamare gli strumenti direttamente nel contesto della pagina.

Registrazione e ciclo di vita

Gli strumenti vengono registrati passando un oggetto di definizione dello strumento e un oggetto di opzioni facoltativo (ModelContextRegisterToolOptions), che supporta due membri facoltativi:

  • signal: un AbortSignal utilizzato per annullare la registrazione dello strumento quando non è più necessario.
  • exposedTo: un array di stringhe di origine che controllano quali documenti nell'albero dei documenti possono rilevare ed eseguire lo strumento oltre i confini dei frame.

Gestione del ciclo di vita con AbortController

WebMCP non fornisce un metodo unregisterTool(). Per annullare la registrazione di uno strumento, devi passare un AbortSignal durante la registrazione e interrompere il segnale quando lo strumento non è più necessario.

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'interruzione del segnale di registrazione annulla solo la registrazione dello strumento, non le esecuzioni già in corso. Gli strumenti che avviano un lavoro a lunga esecuzione devono anche rispettare l'indicatore per esecuzione descritto di seguito, altrimenti l'annullamento della registrazione lascerà richieste orfane e aggiornamenti della UI obsoleti.

Controllo dell'esposizione degli strumenti multiorigine con exposedTo

Per impostazione predefinita, uno strumento registrato è privato per il documento di registrazione e i frame della stessa origine. Quando la tua applicazione è incorporata in un <iframe> (o incorpora widget di terze parti) e hai bisogno di frame di livello superiore o di pari livello per rilevare e richiamare lo strumento, specifica le origini consentite utilizzando 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 di attivazione reciproca: il rilevamento degli strumenti multiorigine è un'attivazione bidirezionale. Il frame del fornitore dello strumento autorizza l'origine del consumatore tramite exposedTo e il frame del consumatore deve richiedere esplicitamente l'origine del fornitore tramite getTools({ fromOrigins: [...] }). Senza fromOrigins, getTools() esegue query solo sui documenti della stessa origine.
  • Corrispondenza dell'origine: l'origine del documento di registrazione ha sempre accesso. Le origini specificate in exposedTo sono autorizzate a rilevare lo strumento tramite document.modelContext.getTools({ fromOrigins: [...] }) ed eseguirlo tramite document.modelContext.executeTool().
  • Requisito di sicurezza: ogni origine in exposedTo DEVE essere potenzialmente attendibile (HTTPS o localhost). Se viene passata un'origine non valida o non sicura, registerTool() rifiuta con un SecurityError.
  • Ambito: exposedTo controlla il rilevamento di più documenti in-page tra i frame nell'albero del documento.

Definizione dei parametri

I parametri (param) vengono definiti utilizzando la proprietà inputSchema. Deve essere un oggetto schema JSON che descrive i dati strutturati previsti dallo strumento.

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

Scrittura di descrizioni di strumenti e parametri

L'agente riceve già l'intero inputSchema, quindi il description deve solo indicare cosa fa lo strumento e quando utilizzarlo. NON ripetere i nomi, i tipi, i valori enum, i valori predefiniti o i limiti dei parametri; dichiarali invece nello schema (enum, default, minimum/maximum). Le descrizioni dei parametri seguono la stessa regola: indica il significato del valore, non il tipo o i valori consentiti.

Il browser non convalida gli argomenti rispetto a inputSchema né applica i valori default, quindi esegui entrambe le operazioni 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 },
});

Pattern di esecuzione

Il callback execute riceve sempre due argomenti: l'oggetto di input e un oggetto di opzioni che contiene un AbortSignal - execute(inputObject, { signal }).

Quando utilizzare async execute

Utilizza async quando lo strumento prevede operazioni che restituiscono una Promise o richiedono tempo per essere completate:

  • Chiamate di rete: recupero di dati da un'API.
  • Archiviazione asincrona: accesso a IndexedDB.
  • Eventi esterni: in attesa del completamento di una modifica di stato o di un'animazione specifica.
async execute(input, { signal }) {
  const response = await fetch(`/api/data/${input.id}`, { signal });
  return await response.json();
}

Quando utilizzare execute (sincrono)

Utilizza una funzione sincrona standard per le operazioni immediate. Uno strumento sincrono viene eseguito fino al completamento prima che l'annullamento possa essere osservato, quindi può ignorare l'indicatore:

  • Logica pura: calcoli, filtri o ordinamento dei dati già in memoria.
  • Stato sincrono: lettura da localStorage o da un gestore dello stato sincrono.
execute(input) {
  return input.items.filter(item => item.active);
}

Gestione degli errori in execute

Quando execute genera un errore (o restituisce una promessa rifiutata), l'agente riceve solo un errore di esecuzione generico. Il messaggio di errore non viene inoltrato all'agente. Se lo strumento rileva un errore su cui è possibile intervenire o che è possibile correggere (ad esempio un input non valido, elementi mancanti o uno stato HTTP non riuscito), NON throw new Error(...). Restituisci invece un payload di errore strutturato tramite il normale valore restituito dello strumento, in modo che l'agente AI riceva i dettagli diagnostici e possa correggersi o riprovare:

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

Genera nuovamente l'eccezione solo quando l'esecuzione è stata annullata (signal.aborted è true).

Annullamento delle esecuzioni in corso

L'annullamento viene avviato dall'utente o dall'agente. OBBLIGATORIO: qualsiasi strumento che esegue richieste di rete o altre operazioni asincrone a lunga esecuzione DEVE rispettare signal, altrimenti le esecuzioni annullate continuano a consumare risorse e possono scrivere dati obsoleti nella UI dopo che l'agente è passato ad altro.

  • API che accettano un indicatore (lettori fetch(), addEventListener(), ReadableStream): trasmetti signal direttamente.
  • API che non lo fanno (setTimeout, setInterval, loop di polling, pipeline con un utilizzo intensivo di await): chiama signal.throwIfAborted() tra i passaggi e/o rifiuta su signal.addEventListener('abort', ...). I timer non accettano alcun segnale, quindi devi cancellarli manualmente nel listener di interruzione.
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 }
});

Segnalazione di strumenti con conseguenze significative

A partire da Chrome 154, annotations supporta consequentialHint. Impostalo su true per gli strumenti che eseguono azioni ad alto rischio, irreversibili o nel mondo reale, come prenotare viaggi, spostare denaro, inviare messaggi o eliminare dati. Indica agli agenti di richiedere la conferma esplicita dell'utente prima dell'esecuzione.

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

OBBLIGATORIO: considera consequentialHint come un suggerimento, non come un meccanismo di applicazione. Non blocca l'esecuzione in modo autonomo e i browser precedenti al suggerimento non mostreranno alcun prompt. Mantieni qualsiasi conferma in-app già prevista dall'azione.

Strumenti di debug per la segnalazione

A partire da Chrome 156, annotations supporta debugging. Imposta annotations.debugging su true per gli strumenti creati per il debug e gli strumenti per sviluppatori anziché per le attività degli utenti finali. Esempi: dumping dello stato interno dell'app, attivazione/disattivazione dei flag di funzionalità o seeding dei dati di test. In questo modo, gli agenti possono distinguere gli strumenti riservati agli sviluppatori da quelli destinati ad agire per conto dell'utente. Il valore predefinito è false. Gli agenti in-page e i test harness possono leggerlo da RegisteredTool.annotations.debugging tramite 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.
  }
});

Agenti e test in-page (getTools e executeTool)

Le pagine web non hanno bisogno di getTools() o executeTool() per esporre gli strumenti agli agenti AI. Per consentire ad agenti del browser, estensioni del browser, DevTools e assistenti a livello di sistema di rilevare ed eseguire i tuoi strumenti fuori banda, è sufficiente chiamare document.modelContext.registerTool().

I metodi getTools() e executeTool() esistono specificamente per i consumer in esecuzione nell'ambiente JavaScript:

  1. Agenti JavaScript in-page: un assistente AI, un widget copilot o un'interfaccia di chat incorporata direttamente nella pagina (o in esecuzione all'interno di un <iframe>) che deve scoprire quali strumenti offrono la pagina circostante o i frame accessibili nell'albero del documento e richiamarli per conto dell'utente.
  2. Test e debug automatizzati: suite di test (come Playwright, Vitest o test delle unità nel browser) che ispezionano i metadati degli strumenti registrati e simulano le chiamate degli strumenti dell'agente senza richiedere un harness LLM esterno attivo.

Scoperta degli strumenti disponibili con getTools()

Gli agenti in-page chiamano document.modelContext.getTools() per recuperare un array di oggetti RegisteredTool dal documento corrente e da eventuali frame accessibili nell'albero del documento.

A partire da Chrome 155, RegisteredTool.inputSchema viene restituito direttamente come oggetto JavaScript (una copia completa dello schema fornito al momento della registrazione). La modifica di questo oggetto non altera lo schema effettivo dello strumento registrato.

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

Per scoprire gli strumenti registrati all'interno di iframe discendenti multiorigine (che devono concedere esplicitamente l'accesso tramite exposedTo durante la registrazione), passa fromOrigins nel dizionario delle opzioni:

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

Chiamata degli strumenti con executeTool()

Gli agenti in-page e gli script di test chiamano document.modelContext.executeTool(tool, inputObject, options) per eseguire uno strumento registrato. Lo strumento viene eseguito sul documento in cui è stato registrato e la promise viene risolta nel risultato convertito in stringa restituito dal callback execute dello strumento.

A partire da Chrome 155, executeTool() accetta un oggetto JavaScript semplice direttamente per inputObject, in modo che corrisponda al formato di input ricevuto dal callback execute dello strumento.

// 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 promessa restituita viene rifiutata se:

  • inputObject non è un oggetto o non può essere serializzato in una stringa JSON (ad es. riferimenti circolari o valori BigInt). Tieni presente che i valori omessi dalla serializzazione JSON (come le funzioni e undefined) verranno omessi dall'oggetto ricevuto dallo strumento anziché causare un rifiuto.
  • L'esecuzione viene interrotta tramite options.signal.
  • La registrazione dello strumento è stata annullata prima dell'esecuzione o si è verificato un errore di runtime non gestito.

Pattern Tool Factory

Per passare il contesto (come archivi o istanze di applicazioni) ai tuoi strumenti, utilizza le funzioni factory.

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

Tipi di TypeScript

Nei progetti TypeScript, NON scrivere a mano le dichiarazioni per document.modelContext. Installa il pacchetto webmcp-types ufficiale (npm install --save-dev webmcp-types) e aggiungilo a tsconfig.json, poiché non viene rilevato automaticamente:

{
  "compilerOptions": {
    "types": ["webmcp-types"]
  }
}
  • document.modelContext è digitato come facoltativo, quindi proteggilo con if (document.modelContext) prima dell'uso.
  • Con un inputSchema incorporato, il tipo di input execute viene dedotto dallo schema.
  • I tipi con nome come WebMCP.ModelContextTool sono globali (non è necessaria l'importazione) e sono utili per digitare le factory degli strumenti.

Supporto dei framework

OBBLIGATORIO: se l'app utilizza React, Angular o Vue, registra gli strumenti tramite l'integrazione WebMCP ufficiale del framework, elencata di seguito. NON chiamare document.modelContext.registerTool() dai componenti e non scrivere il tuo wrapper del ciclo di vita. Ogni integrazione gestisce già il rilevamento delle funzionalità, la pulizia di AbortSignal e l'ambito degli strumenti per il ciclo di vita del componente o dell'iniettore. Queste API sono sperimentali, quindi prima di scrivere il codice, cerca la firma corrente nei tipi o nella documentazione del pacchetto.

Framework Pacchetto API Documenti
React 18+ use-webmcp-tool useWebMCP(options): una chiamata per strumento npmjs.com/package/use-webmcp-tool
Angular @angular/core, @angular/forms/signals provideExperimentalWebMcpTools([...]) (fornitori di app o percorsi), declareExperimentalWebMcpTool(...) (servizi interni), provideExperimentalWebMcpForms() + l'opzione experimentalWebMcpTool su Signal Forms angular.dev/ai/webmcp
Vue @vueuse/core useWebMCP(options) componibile vueuse.org/core/usewebmcp

Tutte le regole di questa guida si applicano anche quando utilizzi queste integrazioni:

  • Mantieni invariata la forma della definizione dello strumento: name, description, inputSchema, execute(input, { signal }) e annotations.
  • Segui le regole per le descrizioni e le annotazioni.
  • Convalida l'input e applica i valori predefiniti all'interno di execute.
  • Passa signal alle attività di lunga durata.
  • Per offrire uno strumento solo in alcuni casi, passa enabled (React, Vue). NON inserire la chiamata hook o componibile in una condizione.
  • Le integrazioni possono aggregare risultati ed errori (ad esempio come { content, isError }). Consulta la documentazione dell'integrazione prima di fare affidamento sul modo in cui gli errori raggiungono l'agente.

Note sull'API

  • annotations: (facoltativo) un dizionario per i metadati dello strumento.
    • readOnlyHint: (facoltativo) imposta su true se lo strumento non modifica alcuno stato e legge solo i dati. In questo modo, gli agenti possono decidere quando è sicuro chiamare lo strumento.
    • consequentialHint: (facoltativo, Chrome 154 e versioni successive) Imposta su true per azioni ad alto rischio, irreversibili o reali, in modo che gli agenti richiedano prima la conferma dell'utente.
    • untrustedContentHint: (facoltativo) imposta su true quando lo strumento restituisce contenuti non controllati dal tuo sito, ad esempio testo generato dagli utenti o risposte dell'API di terze parti.
    • debugging: (facoltativo, Chrome 156+) imposta su true se lo strumento è destinato al debug e agli strumenti per sviluppatori, non alle attività degli utenti finali. Il valore predefinito è false.
  • Opzioni di registrazione:
    • signal: (facoltativo) un AbortSignal utilizzato per annullare la registrazione dello strumento quando non è più necessario.
    • exposedTo: (facoltativo) un array di stringhe di origine sicure che controllano quali documenti nell'albero dei documenti possono rilevare ed eseguire lo strumento oltre i confini dei frame.
  • Opzioni di rilevamento degli strumenti (ModelContextGetToolOptions):
    • fromOrigins: (facoltativo) un array di stringhe di origini sicure su cui eseguire query nei frame accessibili. La chiamata di getTools() senza fromOrigins esegue query solo sui documenti della stessa origine.
  • Formato restituito: la funzione execute può restituire qualsiasi valore serializzabile in JSON (oggetto, array, stringa, numero, booleano). Il messaggio di un'eccezione generata da execute non viene inoltrato all'agente, quindi restituisci gli errori di cui è possibile eseguire l'azione come payload di errore strutturati (ad es. { error: "..." }) tramite il valore restituito dello strumento in modo che l'agente possa leggerli e agire di conseguenza.
  • Contesto sicuro: WebMCP richiede HTTPS. Anche tutte le origini in exposedTo e fromOrigins devono essere potenzialmente attendibili.
  • Deprecati/rimossi: navigator.modelContext (deprecato in Chromium 150), unregisterTool(), provideContext() e clearContext() non sono più supportati.

Strategie di riserva

L'API WebMCP Imperative deve essere utilizzata con il rilevamento delle funzionalità per garantire la compatibilità con i browser che non supportano ancora WebMCP.

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