Herramientas de JavaScript para agentes

Funciones web: document.modelContext

La API imperativa usa document.modelContext.registerTool() para definir herramientas de JavaScript de manera programática. Esto es ideal para las aplicaciones de una sola página (SPA) en las que se deben agregar o quitar herramientas según la ruta actual o el estado del usuario.

WebMCP opera en una arquitectura de productor/consumidor:

  • Las páginas web son productoras de herramientas: Defines y registras herramientas con document.modelContext.registerTool(). Eso es todo lo que necesita implementar una página web. No es necesario que llames a getTools() ni a executeTool() en la página para exponer las herramientas a los agentes.
  • Los agentes de navegador y de extensión son los consumidores principales: Los agentes proporcionados por el navegador o a través de él, incluidos el agente integrado del navegador, las extensiones del navegador, las Herramientas para desarrolladores y los asistentes a nivel del sistema, descubren e invocan herramientas registradas fuera de banda a través de mecanismos internos de la plataforma.
  • Agentes in-page y entornos de pruebas: getTools() y executeTool() existen específicamente para asistentes de JavaScript in-page (p.ej., un widget de chat incorporado o <iframe>) y paquetes de pruebas automatizadas que necesitan descubrir e invocar herramientas directamente en el contexto de la página.

Registro y ciclo de vida

Las herramientas se registran pasando un objeto de definición de herramienta y un objeto de opciones opcional (ModelContextRegisterToolOptions), que admite dos miembros opcionales:

  • signal: Es un AbortSignal que se usa para cancelar el registro de la herramienta cuando ya no se necesita.
  • exposedTo: Es un array de cadenas de origen que controlan qué documentos del árbol de documentos pueden descubrir y ejecutar la herramienta más allá de los límites de los marcos.

Manejo del ciclo de vida con AbortController

WebMCP no proporciona un método unregisterTool(). Para cancelar el registro de una herramienta, debes pasar un AbortSignal durante el registro y anular esa señal cuando ya no se necesite la herramienta.

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

Anular el indicador de registro solo cancela el registro de la herramienta, no cancela las ejecuciones que ya se están ejecutando. Las herramientas que inician trabajos de larga duración también deben respetar la señal por ejecución que se describe a continuación. De lo contrario, la cancelación del registro dejará solicitudes huérfanas y actualizaciones de la IU inactivas.

Controla la exposición de herramientas de origen cruzado con exposedTo

De forma predeterminada, una herramienta registrada es privada para el documento de registro y los marcos del mismo origen. Cuando tu aplicación esté incorporada en un <iframe> (o incorpore widgets de terceros) y necesites marcos principales o secundarios para descubrir e invocar la herramienta, especifica los orígenes permitidos con 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"],
});
  • Confirmación mutua de aceptación: El descubrimiento de herramientas de origen cruzado es una aceptación bidireccional. El marco del proveedor de herramientas autoriza el origen del consumidor a través de exposedTo, y el marco del consumidor debe solicitar explícitamente ese origen del proveedor a través de getTools({ fromOrigins: [...] }). Sin fromOrigins, getTools() solo consulta documentos del mismo origen.
  • Coincidencia de origen: El origen del documento de registro siempre tiene acceso. Los orígenes especificados en exposedTo tienen permiso para descubrir la herramienta a través de document.modelContext.getTools({ fromOrigins: [...] }) y ejecutarla a través de document.modelContext.executeTool().
  • Requisito de seguridad: Cada origen en exposedTo DEBE ser potencialmente confiable (HTTPS o localhost). Si se pasa un origen no válido o no seguro, registerTool() se rechaza con un SecurityError.
  • Alcance: exposedTo controla el descubrimiento entre documentos en la página en todos los marcos del árbol de documentos.

Define parámetros

Los parámetros (params) se definen con la propiedad inputSchema. Debe ser un objeto de esquema JSON que describa los datos estructurados que espera la herramienta.

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

Escribe descripciones de herramientas y parámetros

El agente ya recibe el inputSchema completo, por lo que la description solo necesita decir qué hace la herramienta y cuándo usarla. NO repitas los nombres de los parámetros, los tipos, los valores de enum, los valores predeterminados ni los límites en él. En su lugar, decláralos en el esquema (enum, default, minimum/maximum). Las descripciones de los parámetros siguen la misma regla: indica lo que significa el valor, no su tipo ni los valores permitidos.

El navegador no valida los argumentos con inputSchema ni aplica valores default, por lo que debes hacer ambas cosas en 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 },
});

Patrones de ejecución

La devolución de llamada execute siempre recibe dos argumentos: el objeto de entrada y un objeto de opciones que contiene un AbortSignal: execute(inputObject, { signal }).

Cuándo usar async execute

Usa async cuando la herramienta incluya operaciones que devuelvan una promesa o tarden en completarse:

  • Llamadas de red: Recuperar datos de una API.
  • Almacenamiento asíncrono: Acceso a IndexedDB.
  • Eventos externos: Esperar a que finalice un cambio de estado o una animación específicos.
async execute(input, { signal }) {
  const response = await fetch(`/api/data/${input.id}`, { signal });
  return await response.json();
}

Cuándo usar execute (síncrono)

Usa una función síncrona estándar para las operaciones inmediatas. Una herramienta síncrona se ejecuta hasta completarse antes de que se pueda observar la cancelación, por lo que puede ignorar el indicador:

  • Lógica pura: Operaciones matemáticas, filtrado o clasificación de datos que ya están en la memoria.
  • Estado síncrono: Lectura desde localStorage o un administrador de estado síncrono.
execute(input) {
  return input.items.filter(item => item.active);
}

Cómo controlar errores en execute

Cuando execute arroja (o devuelve una promesa rechazada), el agente solo recibe una falla de ejecución genérica. El mensaje de error no se reenvía al agente. Si tu herramienta encuentra un error que se puede corregir o recuperar (como una entrada no válida, elementos faltantes o un estado HTTP con errores), NO throw new Error(...). En su lugar, devuelve una carga útil de error estructurada a través del valor de retorno normal de la herramienta para que el agente de IA reciba los detalles de diagnóstico y pueda autocorregirse o volver a intentarlo:

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

Solo vuelve a arrojar la excepción cuando se canceló la ejecución (signal.aborted es true).

Cancela las ejecuciones en curso

El usuario o el agente inician la cancelación. OBLIGATORIO: Cualquier herramienta que realice solicitudes de red o cualquier otro trabajo asíncrono de larga duración DEBE respetar signal. De lo contrario, las ejecuciones canceladas seguirán consumiendo recursos y podrán escribir datos obsoletos en la IU después de que el agente haya avanzado.

  • APIs que aceptan un indicador (lectores de fetch(), addEventListener() y ReadableStream): Pasa signal directamente.
  • APIs que no lo hacen (setTimeout, setInterval, bucles de sondeo, canalizaciones con uso intensivo de await): llama a signal.throwIfAborted() entre pasos o rechaza en signal.addEventListener('abort', ...). Los temporizadores no aceptan indicadores, por lo que debes borrarlos tú mismo en el objeto de escucha de anulación.
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 }
});

Herramientas para marcar contenido que genera consecuencias

A partir de Chrome 154, annotations admite consequentialHint. Establécelo en true para las herramientas que realizan acciones de alto riesgo, irreversibles o del mundo real, como reservar viajes, mover dinero, enviar mensajes o borrar datos. Indica a los agentes que soliciten la confirmación explícita del usuario antes de la ejecución.

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

OBLIGATORIO: Trata consequentialHint como una sugerencia, no como un mecanismo de aplicación de políticas. No bloquea la ejecución por sí sola, y los navegadores anteriores a la sugerencia no mostrarán ningún mensaje. Mantén cualquier confirmación en la app que la acción ya requiera.

Herramientas de depuración de marcas

Desde Chrome 156, annotations admite debugging. Establece annotations.debugging en true para las herramientas creadas para la depuración y las herramientas para desarrolladores, en lugar de las tareas del usuario final. Por ejemplo, volcar el estado interno de la app, activar o desactivar marcas de funciones, o propagar datos de prueba. Esto permite que los agentes distingan las herramientas exclusivas para desarrolladores de las herramientas destinadas a actuar en nombre del usuario. El valor predeterminado es false. Los agentes in-page y los entornos de pruebas pueden leerlo desde RegisteredTool.annotations.debugging a través de 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.
  }
});

Agentes in-page y pruebas (getTools y executeTool)

Las páginas web no necesitan getTools() ni executeTool() para exponer herramientas a los agentes de IA. Solo tienes que llamar a document.modelContext.registerTool() para que los agentes del navegador, las extensiones del navegador, las Herramientas para desarrolladores y los asistentes a nivel del sistema descubran y ejecuten tus herramientas fuera de banda.

Los métodos getTools() y executeTool() existen específicamente para los consumidores que se ejecutan dentro del entorno de JavaScript:

  1. Agentes de JavaScript in-page: Un asistente de IA, un widget de copiloto o una interfaz de chat incorporados directamente en la página (o que se ejecutan dentro de un <iframe>) que necesitan descubrir qué herramientas ofrecen la página circundante o los marcos accesibles en el árbol de documentos, y que los invocan en nombre del usuario.
  2. Pruebas y depuración automatizadas: Paquetes de pruebas (como Playwright, Vitest o pruebas de unidades en el navegador) que inspeccionan los metadatos de las herramientas registradas y simulan invocaciones de herramientas de agentes sin necesidad de un arnés de LLM externo en vivo.

Descubre las herramientas disponibles con getTools()

Los agentes in-page llaman a document.modelContext.getTools() para recuperar un array de objetos RegisteredTool del documento actual y de cualquier marco accesible en el árbol de documentos.

Desde Chrome 155, RegisteredTool.inputSchema se devuelve directamente como un objeto de JavaScript (una copia profunda del esquema proporcionado en el registro). La mutación de este objeto no altera el esquema real de la herramienta registrada.

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

Para descubrir herramientas registradas dentro de iframes descendientes de origen cruzado (que deben otorgar acceso de forma explícita a través de exposedTo durante el registro), pasa fromOrigins en el diccionario de opciones:

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

Invoca herramientas con executeTool()

Los agentes in-page y las secuencias de comandos de prueba llaman a document.modelContext.executeTool(tool, inputObject, options) para ejecutar una herramienta registrada. La herramienta se ejecuta en el documento en el que se registró, y la promesa se resuelve en el resultado convertido en cadena que devuelve la devolución de llamada execute de la herramienta.

Desde Chrome 155, executeTool() acepta un objeto de JavaScript simple directamente para inputObject, que coincide con el formato de entrada que recibe la devolución de llamada execute de tu herramienta.

// 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 promesa que se muestra se rechaza en los siguientes casos:

  • inputObject no es un objeto o no se puede serializar en una cadena JSON (p.ej., referencias circulares o valores BigInt). Ten en cuenta que los valores omitidos por la serialización JSON (como las funciones y undefined) se omitirán del objeto que reciba la herramienta, en lugar de provocar un rechazo.
  • La ejecución se anula a través de options.signal.
  • La herramienta se canceló antes de la ejecución o se produjo un error de tiempo de ejecución no controlado.

Patrón de fábrica de herramientas

Para pasar el contexto (como almacenes o instancias de aplicación) a tus herramientas, usa funciones de fábrica.

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

Tipos de TypeScript

En los proyectos de TypeScript, NO escribas declaraciones para document.modelContext a mano. Instala el paquete oficial de webmcp-types (npm install --save-dev webmcp-types) y agrégalo a tsconfig.json, ya que no se selecciona automáticamente:

{
  "compilerOptions": {
    "types": ["webmcp-types"]
  }
}
  • document.modelContext se escribe como opcional, por lo que debes protegerlo con if (document.modelContext) antes de usarlo.
  • Con un inputSchema intercalado, el tipo de entrada execute se infiere del esquema.
  • Los tipos con nombre, como WebMCP.ModelContextTool, son globales (no se necesita importación) y útiles para escribir fábricas de herramientas.

Compatibilidad con frameworks

OBLIGATORIO: Si la app usa React, Angular o Vue, registra las herramientas a través de la integración oficial de WebMCP de ese framework, que se indica a continuación. NO llames a document.modelContext.registerTool() desde componentes ni escribas tu propio wrapper de ciclo de vida. Cada integración ya controla la detección de funciones, la limpieza de AbortSignal y las herramientas de alcance para el ciclo de vida del componente o del inyector. Estas APIs son experimentales, por lo que, antes de escribir código, busca la firma actual en los tipos o documentos del paquete.

Framework Paquete API Documentos
React 18+ use-webmcp-tool useWebMCP(options): una llamada por herramienta npmjs.com/package/use-webmcp-tool
Angular @angular/core, @angular/forms/signals provideExperimentalWebMcpTools([...]) (proveedores de apps o rutas), declareExperimentalWebMcpTool(...) (servicios internos), provideExperimentalWebMcpForms() + la opción experimentalWebMcpTool en los formularios de Signal angular.dev/ai/webmcp
Vue @vueuse/core Función de componibilidad useWebMCP(options) vueuse.org/core/usewebmcp

Todas las reglas de esta guía se siguen aplicando cuando usas estas integraciones:

  • Mantén la misma forma de definición de la herramienta: name, description, inputSchema, execute(input, { signal }) y annotations.
  • Sigue las reglas para las descripciones y las anotaciones.
  • Valida la entrada y aplica los valores predeterminados dentro de execute.
  • Pasa signal al trabajo de larga duración.
  • Para ofrecer una herramienta solo algunas veces, pasa enabled (React, Vue). NO envuelvas el hook ni la llamada componible en una condición.
  • Las integraciones pueden unir resultados y errores (por ejemplo, como { content, isError }). Consulta la documentación de la integración antes de confiar en cómo llegan los errores al agente.

Notas sobre la API

  • annotations: (Opcional) Es un diccionario para los metadatos de la herramienta.
    • readOnlyHint: (Opcional) Se establece en true si la herramienta no modifica ningún estado y solo lee datos. Esto ayuda a los agentes a decidir cuándo es seguro llamar a la herramienta.
    • consequentialHint: (Opcional, Chrome 154 y versiones posteriores) Se establece en true para acciones de alto riesgo, irreversibles o del mundo real, de modo que los agentes soliciten primero la confirmación del usuario.
    • untrustedContentHint: (Opcional) Se establece en true cuando la herramienta devuelve contenido que tu sitio no controla, como texto generado por el usuario o respuestas de la API de terceros.
    • debugging: (Opcional, Chrome 156 y versiones posteriores) Se establece en true cuando la herramienta está diseñada para la depuración y las herramientas para desarrolladores, no para las tareas del usuario final. La configuración predeterminada es false.
  • Opciones de registro:
    • signal: (Opcional) Es un AbortSignal que se usa para cancelar el registro de la herramienta cuando ya no se necesita.
    • exposedTo: (Opcional) Es un array de cadenas de origen seguras que controlan qué documentos del árbol de documentos pueden descubrir y ejecutar la herramienta más allá de los límites de los marcos.
  • Opciones de descubrimiento de herramientas (ModelContextGetToolOptions):
    • fromOrigins: (Opcional) Es un array de cadenas de origen seguras para consultar en marcos accesibles. Llamar a getTools() sin fromOrigins solo consulta documentos del mismo origen.
  • Formato de devolución: La función execute puede devolver cualquier valor serializable en JSON (objeto, array, cadena, número, booleano). El mensaje de una excepción que se genera desde execute no se reenvía al agente, por lo que se deben devolver errores prácticos como cargas útiles de error estructuradas (p.ej., { error: "..." }) a través del valor que devuelve la herramienta para que el agente pueda leerlos y actuar en consecuencia.
  • Contexto seguro: WebMCP requiere HTTPS. Todos los orígenes en exposedTo y fromOrigins también deben ser potencialmente confiables.
  • Obsoletas o quitadas: Ya no se admiten navigator.modelContext (obsoleta en Chromium 150), unregisterTool(), provideContext() ni clearContext().

Estrategias de resguardo

La API imperativa de WebMCP se debe usar con la detección de funciones para garantizar la compatibilidad con los navegadores que aún no admiten WebMCP.

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