
# WebMCP page tools

WebMCP lets a page publish client-side tools on `document.modelContext`. Agentak turns
those tools into visible Pi tool calls inside its complete chat UI. The page keeps its own
UI, session, and state; the agent calls only the actions that the page deliberately
exposes.

This makes the chat part of the application instead of a separate assistant that only sees
text. A shop can publish product search and cart actions. A dashboard can publish filtered
queries. An editor can publish read and update actions while keeping its existing state and
validation.

## Quick start

Enable discovery for the current document with one option:

```ts
import { createPiSession } from "agentak/pi";

const session = createPiSession({ page: true });
```

This feature is off by default. A browser without WebMCP, or a page that publishes no
tools, simply adds nothing to the agent. Host tools passed through `tools` continue to work
alongside page tools.

Use Agentak in either place:

- Embed the chat in the application and use `page: true` to read that document directly.
- Use the Agentak Chrome side panel to discover tools from the active tab as the user
  navigates.

## Browser support

WebMCP is experimental. The version Agentak targets is available in Chrome 149 and Edge
150 behind an origin trial, and in Brave's Leo integration. Firefox and Safari do not have
a public build. Always rely on feature detection rather than a browser name:

```ts
import { webmcpSupported } from "agentak/pi";

if (webmcpSupported()) {
  // document.modelContext is available now.
}
```

`documentTools()` also checks for an API that appears shortly after the chat mounts. It
waits for up to five seconds, then stops polling.

## What Agentak maps

A registered WebMCP tool becomes a Pi `AgentTool`:

| WebMCP value           | Pi and chat value                     |
| ---------------------- | ------------------------------------- |
| `name`                 | provider-safe tool name               |
| `title`                | human-readable label                  |
| `description`          | model description                     |
| `inputSchema`          | tool parameter JSON Schema            |
| `executeTool()` result | text or MCP-shaped text/image content |
| `readOnlyHint`         | approval policy                       |
| `untrustedContentHint` | warning for the model and the user    |

WebMCP arguments and results cross the API as JSON strings. Agentak accepts both the newer
object schema and the JSON-string schema returned by earlier browser builds.

## Names and changing pages

WebMCP names can contain characters and lengths that model providers reject. Agentak:

- replaces unsafe characters with `_`
- limits names to 64 characters
- adds `_2`, `_3`, and so on when a host tool or another frame already uses the name

For example, `cart.add` reaches the model as `cart_add`.

A single-page application may register different tools on each screen. Agentak listens for
`toolchange`, reads the list again, and updates the live Pi tool set without creating a new
session.

## Approval and untrusted content

The page's annotations have security effects:

- `readOnlyHint: true` runs without a confirmation because the page says the tool changes
  nothing.
- A missing or false read-only hint asks before every call.
- `untrustedContentHint: true` adds a warning before the result for the model and labels
  the output for the person reading it. The model is told to treat the content as data,
  never as instructions.

A global `approvals: "never"` disables all confirmation, including page-tool confirmation.
Do not use it when a page can expose tools that change user data.

## Provide a custom page source

Pass a `PageTools` implementation instead of `true` to control where tools come from. This
minimal example publishes one tool manually; an iframe or side panel can put its own bridge
behind the same interface:

```ts
import { createPiSession, type PageTools } from "agentak/pi";

const page: PageTools = {
  async list() {
    return [
      {
        name: "read_page",
        title: "Read the page",
        description: "Read the rendered text of the active page.",
        inputSchema: { type: "object", properties: {} },
        origin: location.origin,
        readOnly: true,
        untrusted: true,
      },
    ];
  },
  async call(_tool, _args, _signal) {
    return JSON.stringify({
      title: document.title,
      url: location.href,
      text: document.body.innerText,
    });
  },
};

const session = createPiSession({ page });
```

A source can also provide `subscribe(listener)` when its list changes and `dispose()` for
cleanup. `list()` returns only serializable metadata. `call()` receives the selected tool,
JSON arguments, and an optional abort signal, then returns JSON text.

A real WebMCP `RegisteredTool` contains a live `Window` and cannot cross a worker, port, or
extension boundary. A bridge must run `getTools()` and `executeTool()` in the source
document and send only tool metadata, arguments, and result text across.

## Extension behavior

The Agentak Chrome side panel combines two sources:

1. `read_active_tab`, supplied by the extension for every reachable tab
2. the tools the active tab publishes on `document.modelContext`

The extension tool steps aside where the page publishes a reader of its own, such as a tool
named `read_page`. It runs WebMCP calls in the tab's main world, follows tab changes and
navigation, and puts the count of page-published tools on the toolbar badge.
`read_active_tab` is not included in the badge because it exists on nearly every page.

Read [Chrome extension](/extension) for permissions, active-tab history, and link
handling.
