Агентские инструменты JavaScript

Веб-функции: document.modelContext

Imperative API использует document.modelContext.registerTool() для программного определения инструментов JavaScript. Это идеально подходит для одностраничных приложений, в которых инструменты нужно добавлять или удалять в зависимости от текущего маршрута или статуса пользователя.

WebMCP работает на основе архитектуры "производитель – потребитель":

  • Веб-страницы создают инструменты. Вы определяете и регистрируете инструменты с помощью document.modelContext.registerTool(). Это все, что нужно сделать на веб-странице. Вам не нужно вызывать getTools() или executeTool() на странице, чтобы предоставить агентам доступ к инструментам.
  • Основные потребители – агенты браузера и расширений. Агенты, предоставляемые браузером или через него, включая встроенный агент браузера, расширения браузера, инструменты разработчика и системные помощники, обнаруживают и вызывают зарегистрированные инструменты вне диапазона через внутренние механизмы платформы.
  • Агенты на странице и тестовые программы. getTools() и executeTool() предназначены для JavaScript-помощников на странице (например, встроенного виджета чата или <iframe>) и автоматизированных наборов тестов, которым нужно находить и вызывать инструменты непосредственно в контексте страницы.

Регистрация и жизненный цикл

Инструменты регистрируются путем передачи объекта определения инструмента и необязательного объекта параметров (ModelContextRegisterToolOptions), который поддерживает два необязательных элемента:

  • signal – AbortSignal, используемый для отмены регистрации инструмента, когда он больше не нужен.
  • exposedTo – массив строк источника, определяющий, каким документам в дереве документов разрешено обнаруживать и выполнять инструмент за пределами фрейма.

Управление жизненным циклом с помощью AbortController

В WebMCP нет метода unregisterTool(). Чтобы отменить регистрацию инструмента, необходимо передать AbortSignal во время регистрации и прервать этот сигнал, когда инструмент больше не нужен.

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

Прерывание сигнала регистрации только отменяет регистрацию инструмента, но не отменяет уже запущенные выполнения. Инструменты, которые запускают длительные процессы, также должны учитывать сигнал об отмене, описанный ниже. В противном случае после отмены регистрации останутся необработанные запросы и устаревшие обновления интерфейса.

Как управлять доступом к инструментам из разных источников с помощью exposedTo

По умолчанию зарегистрированный инструмент доступен только для зарегистрированного документа и фреймов того же источника. Если ваше приложение встроено в <iframe> (или содержит сторонние виджеты) и вам нужно, чтобы родительские или дочерние фреймы могли обнаруживать и вызывать инструмент, укажите разрешенные источники с помощью 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"],
});
  • Взаимное согласие. Обнаружение инструментов из разных источников требует согласия обеих сторон. Фрейм поставщика инструмента авторизует источник потребителя через exposedTo, а фрейм потребителя должен явно запросить источник поставщика через getTools({ fromOrigins: [...] }). Без fromOrigins getTools() запрашивает только документы с того же источника.
  • Соответствие источника. Доступ всегда есть у источника регистрирующего документа. Источники, указанные в правиле exposedTo, получают разрешение на обнаружение инструмента через document.modelContext.getTools({ fromOrigins: [...] }) и его запуск через document.modelContext.executeTool().
  • Требование безопасности. Каждый источник в exposedTo ДОЛЖЕН быть потенциально надежным (HTTPS или localhost). Если передать недопустимый или небезопасный источник, registerTool() отклонит его с ошибкой SecurityError.
  • Область действия. exposedTo управляет обнаружением документов на странице в разных фреймах в дереве документов.

Определение параметров

Параметры определяются с помощью свойства inputSchema. Это должен быть объект схемы JSON, описывающий структурированные данные, которые ожидает инструмент.

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

Описание инструментов и параметров редактора

Агент уже получает полное inputSchema, поэтому в description нужно только указать, что делает инструмент и когда его использовать. НЕ повторяйте в нем названия параметров, типы, значения enum, значения по умолчанию или ограничения. Вместо этого объявите их в схеме (enum, default, minimum/maximum). Описания параметров следуют тому же правилу: указывайте, что означает значение, а не его тип или допустимые значения.

Браузер не проверяет аргументы на соответствие inputSchema и не применяет значения default, поэтому и то, и другое нужно делать в 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 },
});

Шаблоны выполнения

Обратный вызов execute всегда получает два аргумента: входной объект и объект параметров, содержащий AbortSignal – execute(inputObject, { signal }).

Когда использовать async execute

Используйте async, если инструмент выполняет операции, которые возвращают Promise или требуют времени для завершения:

  • Сетевые вызовы. Получение данных из API.
  • Асинхронное хранилище – доступ к IndexedDB.
  • Внешние события. Ожидание изменения состояния или завершения анимации.
async execute(input, { signal }) {
  const response = await fetch(`/api/data/${input.id}`, { signal });
  return await response.json();
}

Когда использовать execute (синхронный режим)

Для немедленных операций используйте стандартную синхронную функцию. Синхронный инструмент завершает работу до того, как будет обнаружена отмена, поэтому он может игнорировать сигнал:

  • Чистая логика. Математические операции, фильтрация или сортировка данных, которые уже находятся в памяти.
  • Синхронное состояние. Чтение из localStorage или синхронного менеджера состояний.
execute(input) {
  return input.items.filter(item => item.active);
}

Обработка ошибок в execute

Если execute выдает ошибку (или возвращает отклоненный промис), агент получает только общее сообщение о сбое выполнения. Сообщение об ошибке не пересылается агенту. Если в вашем инструменте возникает ошибка, которую можно исправить или устранить (например, недействительные входные данные, отсутствующие элементы или неудачный статус HTTP), НЕ throw new Error(...). Вместо этого возвращайте структурированные полезные данные об ошибке через обычное возвращаемое значение инструмента, чтобы ИИ-агент получал диагностические данные и мог самостоятельно исправить ошибку или повторить попытку.

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

Повторно генерировать исключение, только если выполнение было отменено (signal.aborted – это true).

Отмена выполняющихся запусков

Отмена инициируется пользователем или агентом. ОБЯЗАТЕЛЬНО. Любой инструмент, который выполняет сетевые запросы или другую длительную асинхронную работу, ДОЛЖЕН учитывать signal. В противном случае отмененные выполнения продолжат потреблять ресурсы и могут записывать устаревшие данные в интерфейс после того, как агент перейдет к другим задачам.

  • API, принимающие сигнал (fetch(), addEventListener(), ReadableStream): передают signal напрямую.
  • API, которые не поддерживают отмену (setTimeout, setInterval, циклы опроса, конвейеры с большим количеством await): вызывайте signal.throwIfAborted() между шагами и/или отклоняйте signal.addEventListener('abort', ...). Таймеры не принимают сигналы, поэтому их нужно очищать самостоятельно в слушателе прерывания.
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 }
});

Инструменты для сообщения о нарушении

Начиная с Chrome 154, annotations поддерживает consequentialHint. Установите значение true для инструментов, которые выполняют важные, необратимые или реальные действия, такие как бронирование поездок, перевод денег, отправка сообщений или удаление данных. Она указывает агентам, что перед выполнением действия нужно запросить явное подтверждение пользователя.

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

ОБЯЗАТЕЛЬНО. Рассматривайте consequentialHint как подсказку, а не как механизм принудительного применения. Она не блокирует выполнение сама по себе, а браузеры, которые не поддерживают эту подсказку, не будут показывать запрос. Оставьте подтверждение в приложении, если оно уже есть.

Инструменты отладки для жалоб

Начиная с версии Chrome 156, annotations поддерживает debugging. Установите для параметра annotations.debugging значение true для инструментов, созданных для отладки и инструментов разработчика, а не для задач конечных пользователей. Примеры: сброс внутреннего состояния приложения, переключение флагов функций или заполнение тестовых данных. Это позволяет агентам отличать инструменты, предназначенные только для разработчиков, от инструментов, предназначенных для действий от имени пользователя. Значение по умолчанию – false. Агенты на странице и тестовые программы могут считывать его из RegisteredTool.annotations.debugging через 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.
  }
});

Агенты на странице и тестирование (getTools и executeTool)

Чтобы ИИ-агенты могли использовать инструменты, на веб-страницах не нужны getTools() или executeTool(). Чтобы агенты браузера, расширения браузера, инструменты разработчика и системные помощники могли обнаруживать и выполнять ваши инструменты вне диапазона, достаточно вызвать document.modelContext.registerTool().

Методы getTools() и executeTool() предназначены специально для потребителей, работающих в среде JavaScript:

  1. Агенты JavaScript на странице. ИИ-помощник, виджет Copilot или интерфейс чата, встроенный непосредственно в страницу (или работающий внутри <iframe>), которому необходимо определить, какие инструменты предлагает окружающая страница или доступные фреймы в дереве документов, и вызвать их от имени пользователя.
  2. Автоматизированное тестирование и отладка. Наборы тестов (например, Playwright, Vitest или модульные тесты в браузере), которые проверяют метаданные зарегистрированных инструментов и имитируют вызовы инструментов агента без необходимости использования внешней LLM.

Поиск доступных инструментов с помощью getTools()

Агенты на странице вызывают document.modelContext.getTools(), чтобы получить массив объектов RegisteredTool из текущего документа и всех доступных фреймов в дереве документа.

Начиная с Chrome 155 RegisteredTool.inputSchema возвращается непосредственно как объект JavaScript (глубокая копия схемы, предоставленной при регистрации). Изменение этого объекта не влияет на фактическую схему зарегистрированного инструмента.

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

Чтобы найти инструменты, зарегистрированные в дочерних окнах iframe из разных источников (которые должны явно предоставлять доступ через exposedTo во время регистрации), передайте fromOrigins в словаре параметров:

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

Вызов инструментов с помощью executeTool()

Агенты на странице и тестовые скрипты вызывают document.modelContext.executeTool(tool, inputObject, options), чтобы запустить зарегистрированный инструмент. Инструмент запускается в документе, в котором он был зарегистрирован, а обещание разрешается в строковый результат, возвращенный обратным вызовом execute инструмента.

Начиная с Chrome 155, executeTool() принимает простой объект JavaScript непосредственно для inputObject, соответствующий формату ввода, который получает обратный вызов execute вашего инструмента.

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

Возвращаемый объект Promise отклоняется, если:

  • inputObject не является объектом или не может быть сериализован в строку JSON (например, из-за циклических ссылок или значений BigInt). Обратите внимание, что значения, пропущенные при сериализации JSON (например, функции и undefined), будут пропущены в объекте, полученном инструментом, а не приведут к отклонению.
  • Выполнение прервано через options.signal.
  • Регистрация инструмента была отменена до его выполнения, или произошла необработанная ошибка выполнения.

Шаблон фабрики инструментов

Чтобы передать контекст (например, хранилища или экземпляры приложений) в инструменты, используйте фабричные функции.

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

В проектах TypeScript НЕ пишите объявления для document.modelContext вручную. Установите официальный пакет webmcp-types (npm install --save-dev webmcp-types) и добавьте его в tsconfig.json, поскольку он не выбирается автоматически:

{
  "compilerOptions": {
    "types": ["webmcp-types"]
  }
}
  • document.modelContext – необязательный параметр, поэтому перед использованием его нужно защитить с помощью if (document.modelContext).
  • При использовании встроенного inputSchema тип входных данных execute определяется на основе схемы.
  • Именованные типы, такие как WebMCP.ModelContextTool, являются глобальными (импорт не требуется) и полезны для фабрик инструментов ввода.

Поддержка фреймворков

ОБЯЗАТЕЛЬНО. Если в приложении используется React, Angular или Vue, зарегистрируйте инструменты через официальную интеграцию WebMCP этого фреймворка, указанную ниже. НЕ вызывайте document.modelContext.registerTool() из компонентов и не пишите собственную оболочку жизненного цикла. Каждая интеграция уже обрабатывает обнаружение функций, очистку AbortSignal и инструменты определения области действия для жизненного цикла компонента или инжектора. Эти API являются экспериментальными, поэтому перед написанием кода найдите текущую сигнатуру в типах или документах пакета.

Framework Пакет API Документы
React 18+ use-webmcp-tool useWebMCP(options): один вызов на инструмент npmjs.com/package/use-webmcp-tool
Angular @angular/core, @angular/forms/signals provideExperimentalWebMcpTools([...]) (поставщики приложений или маршрутов), declareExperimentalWebMcpTool(...) (внутренние сервисы), provideExperimentalWebMcpForms() + параметр experimentalWebMcpTool в формах Signal Forms angular.dev/ai/webmcp
Vue @vueuse/core useWebMCP(options) composable-функция vueuse.org/core/usewebmcp

При использовании этих интеграций по-прежнему действуют все правила, описанные в этом руководстве:

  • Сохраняйте форму определения инструмента: name, description, inputSchema, execute(input, { signal }) и annotations.
  • Соблюдайте правила в отношении описаний и аннотаций.
  • Проверьте входные данные и примените значения по умолчанию в execute.
  • Передайте signal в длительную задачу.
  • Чтобы инструмент был доступен только в некоторых случаях, передайте enabled (React, Vue). НЕ помещайте хук или вызов composable-функции в условие.
  • Интеграции могут упаковывать результаты и ошибки (например, как { content, isError }). Прежде чем полагаться на то, как ошибки доходят до агента, ознакомьтесь с документацией по интеграции.

Примечания к API

  • annotations (необязательно) – словарь для метаданных инструмента.
    • readOnlyHint (необязательно). Установите значение true, если инструмент не изменяет состояние, а только считывает данные. Это помогает агентам решать, когда безопасно вызывать инструмент.
    • consequentialHint (необязательно, Chrome 154 и более поздних версий). Установите значение true для важных, необратимых или реальных действий, чтобы агенты сначала запрашивали подтверждение пользователя.
    • untrustedContentHint (необязательно). Укажите значение true, если инструмент возвращает контент, который не контролируется вашим сайтом, например текст, созданный пользователями, или ответы сторонних API.
    • debugging (необязательно, Chrome 156 и более поздние версии). Установите значение true, если инструмент предназначен для отладки и инструмента разработчика, а не для конечных пользователей. Значение по умолчанию – false.
  • Варианты регистрации:
    • signal (необязательно) – объект AbortSignal, который используется для отмены регистрации инструмента, когда он больше не нужен.
    • exposedTo (необязательно) – массив строк защищенного источника, определяющий, каким документам в дереве документов разрешено обнаруживать и выполнять инструмент за пределами фрейма.
  • Варианты поиска инструментов (ModelContextGetToolOptions):
    • fromOrigins (необязательно) – массив строк безопасных источников для запроса в доступных фреймах. Вызов getTools() без fromOrigins запрашивает только документы из того же источника.
  • Формат возвращаемого значения. Функция execute может возвращать любое сериализуемое в JSON значение (объект, массив, строку, число, логическое значение). Сообщение об исключении, вызванном execute, не пересылается агенту, поэтому возвращайте ошибки, требующие действий, в виде структурированных полезных нагрузок (например, { error: "..." }) через возвращаемое значение инструмента, чтобы агент мог прочитать их и принять меры.
  • Безопасный контекст. Для WebMCP требуется HTTPS. Все источники в exposedTo и fromOrigins также должны быть потенциально надежными.
  • Устаревшие и удаленные правила. navigator.modelContext (устарело в Chromium 150), unregisterTool(), provideContext() и clearContext() больше не поддерживаются.

Резервные стратегии

WebMCP Imperative API следует использовать с функцией обнаружения, чтобы обеспечить совместимость с браузерами, которые пока не поддерживают WebMCP.

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