代理 JavaScript 工具

網頁功能: 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();

取消註冊信號只會取消註冊工具,不會取消已在執行的作業。啟動長時間執行工作的工具也必須遵循下述的每次執行信號,否則取消註冊會留下孤立的要求和過時的 UI 更新。

使用 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,否則已取消的執行作業會繼續消耗資源,並在代理程式繼續執行後,將過時資料寫入 UI。

  • 接受信號的 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。網頁內代理程式和測試控管工具可透過 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:(選用) 安全來源字串陣列,用於在可存取的頁框中查詢。呼叫 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
}