webmcp-tool

WebMCP

Registering WebMCP tools with JavaScript

The imperative API is the stable path into the standard. What a good tool looks like, how the abort signal keeps a single-page app honest, and the mistakes that make an agent call the wrong function.

Last reviewed 27 August 2026

One call registers one tool. Everything else — how an agent chooses it, whether it gets the arguments right, whether it dares call it without asking — comes down to how you fill in the fields.

await document.modelContext.registerTool({
  name: "search_products",
  description:
    "Search the product catalog by free text, with optional price and " +
    "dimension filters. Returns up to 20 matches with price, key specs " +
    "and stock status. Use this before get_product_details when the " +
    "user has not named a specific product.",
  inputSchema: {
    type: "object",
    properties: {
      query: { type: "string", description: "Free-text search term" },
      maxPrice: { type: "number", description: "Upper price bound in EUR" },
      maxWidthCm: { type: "number", description: "Maximum device width in centimetres" },
    },
    required: ["query"],
  },
  annotations: { readOnlyHint: true },
  execute: async ({ query, maxPrice, maxWidthCm }, { signal }) => {
    const params = new URLSearchParams({ q: query });
    if (maxPrice) params.set("max_price", String(maxPrice));
    if (maxWidthCm) params.set("max_width_cm", String(maxWidthCm));

    const res = await fetch(`/api/search?${params}`, {
      credentials: "same-origin",
      signal,
    });
    if (!res.ok) {
      return { content: [{ type: "text", text: `Search failed: ${res.status}` }] };
    }
    return { content: [{ type: "text", text: JSON.stringify(await res.json()) }] };
  },
});
A read-only search tool, complete

The description is the interface

An agent picks between your tools by reading their descriptions, and nothing else. This is the part engineers reliably under-invest in, because in every other API the docstring is commentary rather than dispatch logic. Here it is dispatch logic.

A description that works tends to answer three questions in order:

  1. What comes back. Not searches products but returns up to 20 matches with price, key specs and stock status. An agent that knows the shape of the return can plan two steps ahead.
  2. When to prefer this one. If you have search_products and get_product_details, say which comes first. Without it you get detail lookups on identifiers the agent invented.
  3. What it will not do. Does not place orders is a sentence that saves a support ticket.
A useful test

Hand the tool list — names and descriptions only, no code — to a colleague who has not seen the site, and read them a user request. If they cannot pick the right tool and guess the arguments, neither will an agent.

Schemas: typed, described, required

Without an inputSchema the agent sends whatever it invented. With one, the browser validates before your code runs. Three habits matter more than the rest:

  • Give every property a description. maxWidthCm with maximum device width in centimetres gets metric input; without it you will receive inches.
  • Use enum wherever the set is closed. It converts a free-text guess into a choice.
  • Keep required genuinely minimal. Every required field is another chance for the call to be abandoned rather than attempted.

Lifecycle: the abort signal is not optional

registerTool accepts an AbortSignal in its options, and aborting it unregisters the tool. In a single-page application this is what stops your tool list from describing a page the user left four navigations ago — an agent calling add_to_cart from a checkout flow that no longer exists is a genuinely bad outcome.

function useProductTools(product) {
  useEffect(() => {
    const controller = new AbortController();

    document.modelContext.registerTool(
      {
        name: "add_to_cart",
        description: `Add "${product.name}" to the cart. Confirm quantity with the user first.`,
        inputSchema: {
          type: "object",
          properties: { quantity: { type: "integer", minimum: 1, maximum: 10 } },
          required: ["quantity"],
        },
        execute: async ({ quantity }) => addToCart(product.id, quantity),
      },
      { signal: controller.signal },
    );

    return () => controller.abort();   // leaves the view, drops the tool
  }, [product.id]);
}
Tools scoped to the lifetime of a view

The second signal — the one handed to execute — is a different thing: it aborts the invocation, not the registration. Thread it into every fetch you make so a cancelled agent turn does not leave requests running.

Keep the handler away from your UI state

A tool handler cannot reach into React state or a Redux store the way a click handler can. In practice this is the single biggest source of retrofit effort on existing applications, and it is a design problem rather than a WebMCP problem: business logic that only exists inside a component was always going to be hard to call from anywhere else.

The move is to extract the operation into a plain function that takes arguments and returns data, then let both the click handler and the tool call it. If that refactor looks large, it is worth scoping honestly before promising a delivery date.

How many tools

The practical ceiling is well under fifty per page. Past that, tool choice degrades — the agent has more plausible options than it can distinguish, and starts picking by name similarity. Most sites need three to eight on any given page: find, inspect, compare, act.

Feature-detect, always

document.modelContext is undefined in every browser that has not enabled the trial, and the specification requires a secure context. Guard registration behind a check, or load the polyfill so the same code path works everywhere.

if (window.isSecureContext && "modelContext" in document) {
  await registerAllTools();
}

Sources

Primary documents, checked on 27 August 2026

  1. webmachinelearning.github.io/webmcpW3C Web Machine Learning Community Group draft — WebIDL, annotations, permissions policy
  2. github.com/webmachinelearning/webmcpExplainer repository, imperative and declarative API
  3. developer.chrome.com/docs/ai/webmcpOrigin trial, flags, permissions policy directive
  4. html.spec.whatwg.org — AbortSignalCancellation semantics used by registerTool and execute
  5. json-schema.orgThe schema dialect used for inputSchema

Keep reading

Check your own site against this

The Agent Readiness Score measures exactly what this article describes, and shows the evidence behind every finding.

Run the check →