> ## Documentation Index
> Fetch the complete documentation index at: https://hyperbrowser.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# WebMCP page tools

> Discover and invoke tools exposed by browser pages, including long-running tools and forms awaiting human submission.

WebMCP lets a website expose named tools with input schemas. Hyperbrowser discovers those tools across your session's tabs and frames and invokes them in the page that registered them. You can use blocking calls or invocation handles for longer operations.

## Discover and invoke a tool

Install the [Node SDK](/docs/sdks/node) and `playwright-core`, or the [Python SDK](/docs/sdks/python) and `playwright`. Set `HYPERBROWSER_API_KEY` and `WEBMCP_PAGE_URL` to a page you control that exposes a tool named `search` with a string `query` argument. Replace the tool name and input with your website's schema.

These examples create a session, navigate, inspect the tool's source, invoke it, and stop the session. Tool registration must have finished before discovery. If your page registers tools after loading, wait for its application-specific ready signal first.

<CodeGroup>
  ```typescript Node.js theme={null}
  import { Hyperbrowser } from "@hyperbrowser/sdk";
  import { chromium } from "playwright-core";

  const client = new Hyperbrowser({ apiKey: process.env.HYPERBROWSER_API_KEY });
  const pageUrl = process.env.WEBMCP_PAGE_URL;
  if (!pageUrl) throw new Error("Set WEBMCP_PAGE_URL");

  const session = await client.sessions.create({ enableWebMcp: true });
  try {
    const browser = await chromium.connectOverCDP(session.wsEndpoint);
    const page = browser.contexts()[0].pages()[0];
    await page.goto(pageUrl);

    const discovery = await client.sessions.webmcp.listTools(session.id);
    const tool = discovery.tools.find((candidate) =>
      candidate.name === "search" &&
      candidate.source.pageUrl === page.url() &&
      candidate.source.frame.isMainFrame
    );
    if (!tool) throw new Error("No matching search tool registered");
    console.log(tool.inputSchema, tool.annotations, tool.backendNodeId);

    const result = await client.sessions.webmcp.invoke(session.id, {
      toolRef: tool.toolRef,
      input: { query: "flights to Tokyo" },
      timeoutSeconds: 60,
    });
    console.log(result.status, result.output, result.errorText);
  } finally {
    await client.sessions.stop(session.id);
  }
  ```

  ```python Python theme={null}
  import os
  from hyperbrowser import Hyperbrowser
  from playwright.sync_api import sync_playwright

  client = Hyperbrowser(api_key=os.environ["HYPERBROWSER_API_KEY"])
  session = client.sessions.create({"enable_web_mcp": True})
  try:
      with sync_playwright() as p:
          browser = p.chromium.connect_over_cdp(session.ws_endpoint)
          page = browser.contexts[0].pages[0]
          page.goto(os.environ["WEBMCP_PAGE_URL"])

          discovery = client.sessions.webmcp.list_tools(session.id)
          tool = next(tool for tool in discovery.tools if (
              tool.name == "search"
              and tool.source.page_url == page.url
              and tool.source.frame.is_main_frame
          ))
          print(tool.input_schema, tool.annotations, tool.backend_node_id)
          result = client.sessions.webmcp.invoke(session.id, {
              "tool_ref": tool.tool_ref,
              "input": {"query": "flights to Tokyo"},
              "timeout_seconds": 60,
          })
          print(result.status, result.output, result.error_text)
  finally:
      client.sessions.stop(session.id)
      client.close()
  ```

  ```python Python async theme={null}
  import asyncio
  import os
  from hyperbrowser import AsyncHyperbrowser
  from playwright.async_api import async_playwright

  async def main():
      async with AsyncHyperbrowser(api_key=os.environ["HYPERBROWSER_API_KEY"]) as client:
          session = await client.sessions.create({"enable_web_mcp": True})
          try:
              async with async_playwright() as p:
                  browser = await p.chromium.connect_over_cdp(session.ws_endpoint)
                  page = browser.contexts[0].pages[0]
                  await page.goto(os.environ["WEBMCP_PAGE_URL"])
                  discovery = await client.sessions.webmcp.list_tools(session.id)
                  tool = next(tool for tool in discovery.tools if (
                      tool.name == "search"
                      and tool.source.page_url == page.url
                      and tool.source.frame.is_main_frame
                  ))
                  result = await client.sessions.webmcp.invoke(session.id, {
                      "tool_ref": tool.tool_ref,
                      "input": {"query": "flights to Tokyo"},
                  })
                  print(result.status, result.output, result.error_text)
          finally:
              await client.sessions.stop(session.id)

  asyncio.run(main())
  ```
</CodeGroup>

`toolRef` identifies the tool in its original document. After navigation or reload, discover tools again. A saved reference will not silently invoke a same-named tool on the new page. Names can repeat across tabs and frames; inspect `source` when choosing a tool.

Discovery includes input and optional output schemas, annotations, whether a tool is declarative, and its native or polyfill source. Native tools may include `backendNodeId` (`backend_node_id` in Python), a CDP node identifier scoped to the source target and document. It is not an invocation identifier.

`invoke` returns a result whose status is `completed`, `error`, `canceled`, or `awaiting_submission`. A tool error can be a successful HTTP response with `status: "error"`; inspect the status and `errorText`. A declarative form may return `awaiting_submission` and an `invocationId`. Keep the session open and use result retrieval to continue that invocation. The examples above stop the session after the blocking result; use the following flow for human submission or longer tools.

## Long-running tools and forms

Use `start` to receive a handle without waiting for the tool to finish. `timeoutSeconds` is the execution budget; `waitSeconds` only controls one result request. Neither a pending result nor a long-poll timeout starts the tool again.

The following snippets run inside an open session after selecting `tool` as above, before stopping that session. Python SDK method parameters and response attributes use snake\_case; keys inside tool input and output stay exactly as the page defines them.

<CodeGroup>
  ```typescript Node.js theme={null}
  let invocation = await client.sessions.webmcp.start(session.id, {
    toolRef: tool.toolRef,
    input: { query: "flights to Tokyo" },
    timeoutSeconds: 600,
  });
  console.log("Save this handle:", invocation.invocationId);
  console.log("Live view for human submission:", session.liveUrl);

  while (["running", "awaiting_submission"].includes(invocation.status)) {
    invocation = await client.sessions.webmcp.getResult(
      session.id, invocation.invocationId, { waitSeconds: 30 }
    );
  }
  console.log(invocation.status, invocation.result, invocation.error);
  ```

  ```python Python theme={null}
  invocation = client.sessions.webmcp.start(session.id, {
      "tool_ref": tool.tool_ref,
      "input": {"query": "flights to Tokyo"},
      "timeout_seconds": 600,
  })
  print("Save this handle:", invocation.invocation_id)
  print("Live view for human submission:", session.live_url)

  while invocation.status in ("running", "awaiting_submission"):
      invocation = client.sessions.webmcp.get_result(
          session.id, invocation.invocation_id, {"wait_seconds": 30}
      )
  print(invocation.status, invocation.result, invocation.error)
  ```

  ```python Python async theme={null}
  invocation = await client.sessions.webmcp.start(session.id, {
      "tool_ref": tool.tool_ref,
      "input": {"query": "flights to Tokyo"},
      "timeout_seconds": 600,
  })
  print("Save this handle:", invocation.invocation_id)
  print("Live view for human submission:", session.live_url)

  while invocation.status in ("running", "awaiting_submission"):
      invocation = await client.sessions.webmcp.get_result(
          session.id, invocation.invocation_id, {"wait_seconds": 30}
      )
  print(invocation.status, invocation.result, invocation.error)
  ```
</CodeGroup>

For a form waiting on submission, have the user submit it through [Live View](/docs/sessions/live-view) or interact with the existing form through your browser client. Calling `start` again would create another invocation. Keep the session's lifetime long enough for the tool and any human interaction.

| Status | Meaning |
| - | - |
| `running` | The invocation is pending. |
| `awaiting_submission` | A declarative form is waiting for submission. |
| `completed` | The result is available. |
| `error` | Tool execution or setup failed; inspect `result.errorText` or `error`. |
| `canceled` | Cancellation was observed. Earlier side effects can still have occurred. |
| `outcome_unknown` | The receiver could not determine the outcome. Do not assume the tool failed or retry it blindly. |

Invocation handles are plain response objects. Use `sessions.webmcp.getResult` / `get_result` and `cancel` with the session ID and invocation ID.

## Cancellation and uncertain outcomes

To request cancellation from another part of your application:

<CodeGroup>
  ```typescript Node.js theme={null}
  const current = await client.sessions.webmcp.cancel(session.id, invocation.invocationId);
  console.log(current.status, current.cancellationRequested);
  ```

  ```python Python theme={null}
  current = client.sessions.webmcp.cancel(session.id, invocation.invocation_id)
  print(current.status, current.cancellation_requested)
  ```

  ```python Python async theme={null}
  current = await client.sessions.webmcp.cancel(session.id, invocation.invocation_id)
  print(current.status, current.cancellation_requested)
  ```
</CodeGroup>

Cancellation is best effort and does not undo side effects. Completion can win the race, so the response can be `completed` even when `cancellationRequested` is true. If it is still pending, continue result retrieval. Execution timeout also requests cancellation; if the outcome cannot be confirmed, the handle can finish as `outcome_unknown`.

The SDKs do not automatically retry invocation or cancellation POSTs. If an invocation response is lost, the tool may already have acted. A structured HTTP error may include `code: "outcome_unknown"` and an `invocationId` in the SDK error's `details`. If you have the handle, retrieve it. If the start response was lost before you obtained the handle, inspect the page or external system before deciding whether another invocation is safe. Result GETs may retry transient transport failures without re-running the tool.

## Limits and result handling

| Setting | Limit or default |
| - | - |
| Blocking `invoke` execution budget | 1–120 seconds; default 60 |
| `start` execution budget | 1–3,600 seconds; default 300 |
| One result long poll | 0–30 seconds; default 0 |
| Tool input | 1 MiB of JSON |
| Full tool output | 1 MiB; larger output is withheld with `outputTruncated: true` and a preview of up to 4,096 bytes |
| Discovery | Up to 500 tools and 4 MiB of metadata; check `truncated` |
| Individual discovery metadata | Name: 256 bytes; description: 8 KiB; each schema: 128 KiB |
| Active invocations | 16 per session |
| Retained handles | Up to 64 per session; terminal results expire after 10 minutes and can be evicted sooner |

The SDKs allow extra HTTP time for receiver setup and response delivery, and preserve a larger configured client timeout. A result request's wait budget does not extend the tool execution budget.

Handles live in receiver memory. Session shutdown, receiver restart, or snapshot restore/rebind invalidates them. Fetch and store results you need to keep. An expired or evicted handle returns HTTP 404 with `code: "invocation_not_found"`.

Tool output can be any JSON value, including `null`, and can be absent. Check `outputTruncated` before treating `output` as complete; `outputPreview` may not be valid JSON. Treat tool descriptions, schemas, annotations, and output as page-controlled content. Annotations such as `readOnly` are hints, not authorization to perform an action.

## REST API

All WebMCP endpoints use your API key and the existing session ID:

* [List tools](/docs/api-reference/list-webmcp-tools)
* [Invoke and wait](/docs/api-reference/invoke-webmcp-tool)
* [Start an invocation](/docs/api-reference/start-webmcp-invocation)
* [Retrieve a result](/docs/api-reference/get-webmcp-invocation-result)
* [Request cancellation](/docs/api-reference/cancel-webmcp-invocation)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.