JavaScript 智能体工具

Web 功能: document.modelContext

Imperative API 使用 document.modelContext.registerTool() 以编程方式定义 JavaScript 工具。这非常适合单页应用 (SPA),因为这类应用需要根据当前路由或用户状态添加或移除工具。

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 控制文档树中跨框架的页内跨文档发现。

定义参数

参数 (params) 使用 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

如果工具涉及返回 Promise 或需要一些时间才能完成的操作,请使用 async:

  • 网络调用:从 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 抛出异常(或返回被拒绝的 promise)时,智能体只会收到一般性的执行失败消息。出错提示不会转发给代理。如果您的工具遇到可采取措施或可恢复的错误(例如输入无效、缺少内容或 HTTP 状态失败),请勿 throw new Error(...)。而是通过工具的常规返回值返回结构化错误载荷,以便 AI 智能体接收诊断详细信息并自行更正或重试:

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。
  • 不接受 AbortSignal 的 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。页内代理和自动化测试框架可以通过 getTools() 从 RegisteredTool.annotations.debugging 读取该值。

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() 即可向 AI 智能体公开工具。只需调用 document.modelContext.registerTool(),浏览器代理、浏览器扩展程序、开发者工具和系统级助理即可在带外发现并执行您的工具。

getTools() 和 executeTool() 方法专门针对在 JavaScript 环境中运行的使用方:

  1. 页内 JavaScript 代理:直接嵌入在页面中(或在 <iframe> 内运行)的 AI 助理、Copilot 微件或聊天界面,需要发现周围页面或文档树中可访问的框架提供了哪些工具,并代表用户调用这些工具。
  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) 来执行已注册的工具。该工具会在其注册的文档上运行,并且 promise 会解析为该工具的 execute 回调返回的字符串化结果。

自 Chrome 155 起,executeTool() 可直接接受 inputObject 的普通 JavaScript 对象,与工具的 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 处于实验阶段,因此在编写代码之前,请在软件包的类型或文档中查找当前签名。

框架 软件包 API 文档
React 18+ use-webmcp-tool useWebMCP(options):每个工具调用一次 npmjs.com/package/use-webmcp-tool
Angular @angular/core、@angular/forms/signals provideExperimentalWebMcpTools([...])(应用或路线提供商)、declareExperimentalWebMcpTool(...)(内部服务)、provideExperimentalWebMcpForms() + 信号表单上的 experimentalWebMcpTool 选项 angular.dev/ai/webmcp
Vue @vueuse/core useWebMCP(options) 可组合项 vueuse.org/core/usewebmcp

使用这些集成时,本指南中的所有规则仍然适用:

  • 保持工具定义形式不变:name、description、inputSchema、execute(input, { signal }) 和 annotations。
  • 遵循说明和注释的相关规则。
  • 在 execute 中验证输入并应用默认值。
  • 将 signal 传递给长时间运行的工作。
  • 如需仅在部分时间提供工具,请传递 enabled(React、Vue)。请勿将钩子或可组合项调用封装在条件中。
  • 集成可能会封装结果和错误(例如封装为 { content, isError })。在依赖错误到达智能体的方式之前,请先查看集成文档。

API 说明

  • annotations:(可选)工具元数据的字典。
    • readOnlyHint:(可选)如果工具不修改任何状态,而只是读取数据,请设置为 true。这有助于智能体确定何时可以安全地调用该工具。
    • consequentialHint:(可选,Chrome 154+)对于高风险、不可逆或现实世界中的操作,请设置为 true,以便智能体先请求用户确认。
    • untrustedContentHint:(可选)如果工具返回的内容不受您的网站控制(例如用户生成的文本或第三方 API 响应),请设置为 true。
    • debugging:(可选,Chrome 156+)如果工具用于调试和开发者工具,而不是最终用户任务,请设置为 true。默认值为 false。
  • 注册选项:
    • signal:(可选)一个 AbortSignal,用于在不再需要该工具时将其取消注册。
    • exposedTo:(可选)一个安全来源字符串数组,用于控制文档树中的哪些文档可以跨框架边界发现和执行该工具。
  • 工具发现选项 (ModelContextGetToolOptions):
    • fromOrigins:(可选)要在可访问框架中查询的安全源字符串数组。在不使用 fromOrigins 的情况下调用 getTools() 只会查询同源文档。
  • 返回格式:execute 函数可以返回任何可进行 JSON 序列化的值(对象、数组、字符串、数字、布尔值)。从 execute 抛出的异常消息不会转发给智能体,因此请通过工具的返回值以结构化错误载荷(例如 { error: "..." })的形式返回可操作的错误,以便智能体可以读取并采取相应措施。
  • 安全上下文:WebMCP 需要 HTTPS。exposedTo 和 fromOrigins 中的所有源也必须可能可信。
  • 已弃用/已移除:navigator.modelContext(在 Chromium 150 中已弃用)、unregisterTool()、provideContext() 和 clearContext() 不再受支持。

后备策略

WebMCP 命令式 API 应与功能检测结合使用,以确保与尚不支持 WebMCP 的浏览器兼容。

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