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

# WebMCP

> How the chat embed publishes your MCP server's tools to AI browsing agents through document.modelContext: the request flow, widget steps, attribution, verification and troubleshooting.

> [!NOTE]
>
> For what WebMCP does and how to switch it on from the dashboard, read [WebMCP](/platform/webmcp) in the Platform docs. This page is the developer reference behind it.

[WebMCP](https://github.com/webmachinelearning/webmcp) is a proposed browser API that lets a web page hand tools to an AI agent browsing it. The page registers each tool on `document.modelContext` with a name, a description and a JSON Schema for its input. An agent standing on the page can then call the tool directly, where it would otherwise have to work out which fields to fill and which button to press.

Waniwani publishes these tools for you. When a browsing agent loads a page that carries the Waniwani embed script, the embed registers the tools of your agent's MCP server with the browser. They are the same tools and flows ChatGPT and Claude reach through your connector, and the same ones the chat widget calls, so a flow you build once runs on all three surfaces.

## How it works

```mermaid theme={null}
sequenceDiagram
    participant Agent as Browsing agent
    participant Page as Your page (embed)
    participant WW as Waniwani API
    participant MCP as Your MCP server
    Page->>WW: GET /webmcp/tools?token=wwp_…
    WW->>MCP: tools/list
    WW-->>Page: tools
    Page->>Agent: registerTool() for each tool
    Agent->>Page: execute(args)
    Page->>WW: POST /webmcp (call)
    WW->>MCP: tools/call
    WW-->>Page: result, plus a widget when the step has one
    Page-->>Agent: text content
```

The embed first checks whether the browser exposes `document.modelContext`. It also looks at `navigator.modelContext`, where earlier drafts of the proposal put it. An ordinary visitor's browser has neither, so for almost every visit the check costs one property read and makes no network request.

On a browser that does expose it, the embed fetches your tool list from the Waniwani API using the public token on the script tag. Waniwani resolves the MCP server behind that token's environment on each request, which means your page never contains the server's address. Each tool is then registered with the browser under its MCP name, together with its description, input schema and annotations. When the agent calls one, the embed posts the call to Waniwani, Waniwani calls the tool on your MCP server, and the result goes back to the agent as MCP content blocks.

Tools are unregistered when the visitor leaves the page. On a single-page app that changes routes without reloading, the tools stay registered and each call reports the URL the visitor is on at the time of the call.

### Widget steps

A flow step that shows a widget, such as a calendar or a quote card, has nowhere to render inside the agent's window. So Waniwani resolves the step on the server: it calls the display tool, finds the view bound to it, and the embed opens that view in a dialog over your page. The agent gets a text version of the step telling it the interface is on screen. For a step that waits on the visitor, the agent is told to say one sentence about what is showing, wait for the visitor to report what they did, then continue the flow. If the view can't be resolved, the agent receives the step's data and carries it in words instead.

A tool with a view bound to it behaves the same way when the agent calls it directly. The agent gets the result and the view opens on the page.

The dialog is 672px wide, narrower on small screens, and it stops the page behind it from scrolling. While a flow is waiting on the visitor there is no close button, since closing it would strand the agent mid-flow. The next widget step replaces it. The view is the same one ChatGPT, Claude and the chat widget render, so there is nothing to build twice.

### Sessions and attribution

Flow state is keyed by a session id that the embed keeps in `sessionStorage` under `waniwani:webmcp:session`. It lasts as long as the tab. A visitor who opens a new tab starts the flow over, and one who comes back next week doesn't land in the middle of a question they abandoned.

Next to it the embed sends the visitor's persistent id, the one the chat widget uses (or yours, if you set `data-visitor-id`). A flow an agent starts on the visitor's behalf and a conversation they continue in the chat therefore belong to the same person in your analytics.

Waniwani stamps every call on the server before it reaches your MCP server. The `_meta` your tools receive carries `waniwani/source: "webmcp"`, the session and visitor ids, the channel id, and the page URL and title under `waniwani/extra.page`. The page can't relabel its own traffic, so events these calls produce are attributed to the `webmcp` source and to your web channel.

## Prerequisites

* An agent on Waniwani whose environment points at an MCP server, managed or external. Without one, every call fails with `MCP_SERVER_URL_REQUIRED`.
* A web channel on that agent. Its embed snippet carries the public token (`wwp_…`) and channel id the embed needs.
* The tools you want agents to use, defined on that MCP server. See [Choose what agents see](#choose-what-agents-see).

## Install

WebMCP is on by default in the chat embed. If the Waniwani script tag is already on your site, browsing agents can use your tools today and there is nothing to add.

**1. Copy the snippet**

In the dashboard, open your agent's **Settings → Channels**, pick the web channel and open its **Integrate** tab. Copy the HTML snippet under **Embed code**.

**2. Paste it on your pages**

Paste it into your site's HTML just before `</body>`, on every page where agents should find your tools.

```html theme={null}
<script
  src="https://app.waniwani.ai/embed.js"
  defer
  data-api="https://app.waniwani.ai/api/mcp/chat"
  data-token="wwp_..."
  data-channel-id="..."
  data-theme="light"
></script>
```

The dashboard fills in the host for your [region](/platform/regions). Keep `data-channel-id` on the tag. Without it the embed learns the channel from a cached copy of the channel config, so the first page a visitor opens would publish tools with no channel to attribute calls to.

**3. Check it**

Follow [Verify the install](#verify-the-install).

`https://app.waniwani.ai/embed.js` is a small loader that always serves the latest SDK release, and on a WebMCP browser it starts fetching the tool list while the main bundle downloads. If you load a pinned bundle from a CDN yourself, use `@waniwani/sdk` 0.21.4 or later.

### Publish tools without showing the chat

To give agents your tools on a page that shouldn't show a chat widget, pick the **Off** display style on the Integrate tab. The snippet then carries `data-mode="off"`: nothing renders for your visitors, and the embed still publishes your tools. Widget steps still open their dialog, because it mounts separately from the chat.

```html theme={null}
<script
  src="https://app.waniwani.ai/embed.js"
  defer
  data-api="https://app.waniwani.ai/api/mcp/chat"
  data-token="wwp_..."
  data-channel-id="..."
  data-mode="off"
></script>
```

### Turn it off on one page

Add `data-webmcp="false"` (or `"off"`) to the script tag. The chat keeps working on that page and no tools are published there. Other pages sharing the channel are unaffected.

```html theme={null}
<script src="https://app.waniwani.ai/embed.js" defer data-token="wwp_..." data-channel-id="..." data-webmcp="false"></script>
```

### Lock it to your site

Set **Allowed origin** on the channel's **Settings** tab, for example `https://www.example.com`. Waniwani then refuses tool listings and calls from any other origin, and from any request that sends no `Origin` header at all, with a `403 ORIGIN_NOT_ALLOWED`. The comparison is exact, so `https://example.com` and `https://www.example.com` are two different origins.

## Choose what agents see

Every tool your MCP server lists is published, in the order the server lists it. The agent reads the tool's `name`, its `description` and its `inputSchema`, and decides from those whether and how to call it, so describe what each tool does in a sentence or two. Waniwani refuses a call to any tool name that isn't in the current listing, so a page can't reach a tool your server doesn't advertise.

To change what agents see, change your MCP server and deploy. The tool list is cached for about a minute, so a new or removed tool can take a minute or two to show up on your pages.

### Turn a website form into a tool

There is no attribute that exposes an existing HTML form as a tool. You rebuild the form as a flow on your MCP server, and the flow becomes the tool. Form fields become state fields that interrupts ask for, and the submit handler turns into an action node calling the backend your form already posts to.

A callback request form with three fields, for example:

```ts theme={null}
import { createFlow, START, END } from "@waniwani/sdk/mcp";
import { z } from "zod";
import { callbacks } from "./services";

export const requestCallback = createFlow({
  id: "request_callback",
  title: "Request a callback",
  description:
    "Books a callback from an advisor. Collects the visitor's name, phone number and preferred time of day.",
  state: {
    name: z.string().describe("Visitor's full name"),
    phone: z.string().describe("Phone number to call, with country code"),
    slot: z.enum(["morning", "afternoon", "evening"]).describe("Preferred time of day"),
    callbackId: z.string().optional().describe("Set once the callback is booked"),
  },
})
  .addNode({
    id: "ask_contact",
    label: "Ask for contact details",
    run: ({ interrupt }) =>
      interrupt({
        name: { question: "What's your name?" },
        phone: { question: "What number should we call?" },
        slot: {
          question: "When suits you best?",
          suggestions: ["morning", "afternoon", "evening"],
        },
      }),
  })
  .addNode({
    id: "book_callback",
    label: "Book the callback",
    run: async ({ state }) => {
      const booked = await callbacks.create({
        name: state.name,
        phone: state.phone,
        slot: state.slot,
      });
      return { callbackId: booked.id };
    },
  })
  .addEdge(START, "ask_contact")
  .addEdge("ask_contact", "book_callback")
  .addEdge("book_callback", END)
  .compile();
```

Register it on your server with `await requestCallback.register(server)`. The browsing agent now sees a `request_callback` tool on every page carrying the embed, and ChatGPT, Claude and the chat widget get the same flow. Validation and re-asking work the way they do on any other surface. See [Interrupts](/sdk/flows/interrupts) for the question options and [Nodes](/sdk/flows/nodes) for widget steps.

## Verify the install

Few browsers ship WebMCP yet, so the quickest check is to stand in for the agent yourself. On a staging copy of a page that carries the embed, add this script anywhere in the HTML. It gives the page a minimal `document.modelContext` that records what the embed registers.

```html theme={null}
<script>
  window.__webmcpTools = [];
  document.modelContext = {
    registerTool(tool) {
      window.__webmcpTools.push(tool);
    },
  };
</script>
```

Load the page and open the browser console. The embed logs `[webmcp] registered N site tool(s)` once it's done, and the network tab shows a request to `/api/mcp/chat/webmcp/tools`. From the console you can list the tools and call one the way an agent would:

```js theme={null}
__webmcpTools.map((tool) => tool.name);

const flow = __webmcpTools.find((tool) => tool.name === "request_callback");
await flow.execute({ action: "start", intent: "Visitor wants a callback" });
```

The call reaches your MCP server as a real `webmcp` call, and a widget step opens its dialog on the page. Check the flow tool's `inputSchema` for the exact arguments it takes.

You can also fetch the listing from a terminal. Send the `Origin` header when the channel has an allowed origin, since a request without one is refused.

```bash theme={null}
curl "https://app.waniwani.ai/api/mcp/chat/webmcp/tools?token=wwp_...&channel=..." \
  -H "Origin: https://www.example.com"
```

The response is `{ "tools": [...] }`, the list an agent will see.

## Troubleshooting

| What you see | Why | What to do |
| - | - | - |
| No request to `/webmcp/tools` and nothing in the console | The browser has no `document.modelContext`, the tag has `data-webmcp="false"`, or the tag has no `data-token` | Test with the stand-in script above and check the tag's attributes. |
| `[webmcp] could not list site tools` in the console | The listing failed, usually on one of the errors below | Look at the failed request's status in the network tab. |
| `401` | The public token is wrong, revoked or rotated | Copy a fresh snippet from the Integrate tab. |
| `403 ORIGIN_NOT_ALLOWED` | The page's origin doesn't match the channel's **Allowed origin** exactly | Fix the allowed origin. Watch for `www`, the scheme, a port or a trailing slash. |
| `400 MCP_SERVER_URL_REQUIRED` | The environment has no MCP server URL | Deploy the agent or set its external MCP URL. |
| `400 UNKNOWN_TOOL` | The agent called a tool that isn't in the current listing, often one removed since the page loaded | Reload the page so the agent picks up the new list. |
| `502` with `MCP_SERVER_UNREACHABLE`, `MCP_TOOL_LIST_FAILED` or `MCP_TOOL_CALL_FAILED` | Your MCP server is down or the tool threw. A call that runs past 45 seconds is cut off | Check your MCP server's logs. |
| `[webmcp] could not register <name>` | The browser refused that one tool, often over its schema | The other tools still register. Check the tool's `inputSchema`. |
| A new tool doesn't appear | The listing is cached briefly | Wait a minute or two and reload. |
| A widget step never opens | The display tool has no view bound, or it failed | The agent carries the step in words meanwhile. Check the display tool's `_meta` and your server logs. |
| A flow starts over | Flow state lasts for the tab, so a new tab or a new browser starts fresh | Expected. |

## Without the script tag

The React chat component doesn't publish tools. If your page uses it, or carries no Waniwani chat at all, you can register the tools yourself with `createWebMcpBridge` from `@waniwani/sdk/mcp`. It does what the embed does, with the endpoints and ids passed in.

```ts theme={null}
import { createWebMcpBridge, supportsWebMcp } from "@waniwani/sdk/mcp";

const api = "https://app.waniwani.ai/api/mcp/chat";
const token = "wwp_...";
const channelId = "...";

// Stable for the tab, and gone when the tab closes.
function tabSessionId() {
  const key = "my-site:webmcp-session";
  let id = sessionStorage.getItem(key);
  if (!id) {
    id = crypto.randomUUID();
    sessionStorage.setItem(key, id);
  }
  return id;
}

if (supportsWebMcp()) {
  const bridge = await createWebMcpBridge({
    endpoint: `${api}/webmcp`,
    listEndpoint: `${api}/webmcp/tools?token=${token}&channel=${channelId}`,
    headers: { Authorization: `Bearer ${token}` },
    sessionId: tabSessionId(),
    channelId,
    onWidget: (widget) => {
      // Mount widget steps, for example with <WebMcpOverlay> from @waniwani/sdk/chat.
    },
  });

  console.log(bridge?.tools.map((tool) => tool.name));
}
```

`createWebMcpBridge` resolves to `null` on a browser without WebMCP. Leave out `onWidget` and widget steps reach the agent as text. To render them, pass each one to `<WebMcpOverlay>` from `@waniwani/sdk/chat` with `onCallTool={bridge.callTool}` and `resourceEndpoint` set to `${api}/resource?token=${token}`. Call `bridge.dispose()` to unregister every tool. The script tag remains the simpler route, since it also takes care of the widget dialog and the visitor id.


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