# WebMCP on frontmcp.dev: A FrontMCP Server in the Docs Page

> WebMCP lets a website offer tools to the agent in a reader's browser. How frontmcp.dev offers 13 from a FrontMCP server in the page, and what we learned.

Source: https://frontmcp.dev/blog/webmcp-docs-site
Published: 2026-10-26
Author: David Antoon
Example version: FrontMCP 1.10.11
Technical checks recorded: 2026-10-26
Scope: Dated article; use the maintained reference for current instructions. Its presence in a release snapshot does not verify its code against that release.

*[Illustration: A frontmcp.dev docs page runs a FrontMCP server with search_docs, get_page, get_current_page, get_version, navigate, run_in_playground and seven more tools for the page's Playgrounds. It offers them through document.modelContext to the agent in the browser, which lists 13 tools, calls search_docs with the query webmcp, and gets the WebMCP reference page as the best match.]*

An agent working in someone's browser reads a docs site the way a person skims one: it takes in the page, finds a link, follows it and reads again. It can't use the site's search, can't run the examples, and only knows which release the docs describe if that's on the screen.

[WebMCP](https://webmachinelearning.github.io/webmcp/) gives a page a better option. WebMCP is a browser API, `document.modelContext`, through which a web page offers tools to the agents in the user's browser. Each tool has a name, a description and an input schema, like an MCP tool, and runs in the page. frontmcp.dev now offers 13: six for reading the docs and seven for running code in the page's Playgrounds. They come from a FrontMCP server that runs in the page, offered by FrontMCP's [WebMCP plugin](https://frontmcp.dev/reference/plugins/webmcp).

This post covers why a website would offer tools at all and what frontmcp.dev's tools do. It also covers how they stay out of the way of readers without an agent, and what we learned shipping them. The measurements are from Chrome for Testing 153 against the live site on October 26, 2026.

<a id="why-a-website-would-offer-tools-to-agents"></a>

## Why a website would offer tools to agents

Without WebMCP, an agent in the browser works from what it sees. It takes a screenshot or reads the page, finds the control it needs, acts, and looks again. Every step depends on the page's layout, and every step is another turn for the model. A WebMCP tool replaces that loop with a function call: the agent sends input that matches a schema, and gets data back.

*[Illustration: The same question, where FrontMCP's rate-limit settings are, answered two ways. Without WebMCP, the agent takes a screenshot, finds the search box, clicks it and types rate limit, takes a screenshot of the results, clicks one, and takes a screenshot to read the page: six actions chosen from what's on the screen. With WebMCP, it calls search_docs with the query rate limit, gets a match, Rate limits at /reference/auth/production#rate-limits, then calls get_page with that path and gets the page as Markdown: two calls.]*

That's useful to a website in more ways than speed:

- **The agent asks for what it needs.** Searching the docs is one call to `search_docs`, not a click, some typing and another screenshot. A redesign doesn't break it, because the tool doesn't depend on where the search box is.
- **The page already knows the user.** A tool runs in the page, so it can use what the page has: the signed-in user, the open record, what's on the screen. There's no separate server to deploy and no separate sign-in. With FrontMCP, [`authContext`](https://frontmcp.dev/reference/plugins/webmcp#calling-as-the-signed-in-user) makes tools act as the page's user.
- **The person sees what happens.** Tools act in the page the person is looking at. So a site can stop a tool short of the final step: fill in a reply for the person to send, or add to a cart and let them pay.
- **The site sets the limits.** Agents get the tools the site offers and nothing else. Their input is checked against each tool's schema, and hints say which tools only read.

*[Illustration: Three sites and the tools each could offer the agent in the browser. The docs site, frontmcp.dev, offers search_docs and get_page, which are read-only, and run_in_playground, which runs code; the reader follows the links to the pages the agent found. A help desk, the WebMCP plugin reference's example, offers search_tickets, which is read-only, fill_reply_form, offered to the page only, and close_ticket, which is consequential; the support agent reads the reply and sends it. A hypothetical store offers search_products and compare_sizes, read-only, and add_to_cart, consequential; the shopper reviews the cart and pays.]*

The help desk is the example on the [plugin's reference page](https://frontmcp.dev/reference/plugins/webmcp#offering-a-pages-tools-to-agents), and the store is hypothetical. The rest of this post is about the docs site, which is running now.

<a id="what-an-agent-can-do-on-the-site"></a>

## What an agent can do on the site

| Tool | What it does |
| --- | --- |
| `search_docs` | Searches the docs with the index the header's search box uses, and returns titles, paths with section anchors, and snippets |
| `get_page` | Returns a page as Markdown, from its `.md` copy |
| `list_pages` | Returns the sidebar's sections and pages, and the blog's posts |
| `get_version` | Returns the FrontMCP release the docs cover, the MCP revision its examples speak, and the latest release notes |
| `get_current_page` | Returns the open page's path, title and section headings, with their anchors |
| `navigate` | Opens one of the site's pages in the tab, as a link does, and scrolls to a section |
| `run_in_playground` | Compiles and runs FrontMCP code, and returns the compile errors, or the server's tools, resources and prompts, the response to a request and the test results |
| `list_playgrounds`, `get_playground` | Read the page's Playgrounds, with the reader's edits |
| `edit_playground`, `call_playground`, `run_playground_tests` | Act on one of the page's Playgrounds as the reader would: edit its code, send a request from its Call tab, run its tests |
| `open_playground` | Opens code in the full [Playground](https://frontmcp.dev/playground) page and returns its link |

So an agent helping someone with FrontMCP can check which release the docs describe, find the page that answers the question, and run the code it's about to suggest on the real FrontMCP runtime first.

This is what that looks like from the browser's side, in the console of a docs page:

```js
const tools = await document.modelContext.getTools();
const search = tools.find((tool) => tool.name === "search_docs");
await document.modelContext.executeTool(search, JSON.stringify({ query: "webmcp", limit: 3 }));
```

Chrome hands the result back as a JSON string:

```json
{"query":"webmcp","results":[{"title":"WebMCP","path":"/reference/plugins/webmcp","section":"reference","snippet":"Expose a browser-based FrontMCP server's tools through document.modelContext for compatible browser agents."}, …]}
```

We also sent `run_in_playground` one file, a tool that adds two numbers, with a request for it:

```json
{
  "files": [{ "name": "tools.ts", "code": "import { Tool, ToolContext, z } from \"@frontmcp/sdk\"; …" }],
  "call": { "method": "tools/call", "params": { "name": "add", "arguments": { "a": 2, "b": 3 } } }
}
```

The answer came back in 1.9 seconds, with the server's catalog and the response, `"structuredContent":{"sum":5}`. Most of that time was the first run loading FrontMCP's runtime and esbuild, which compiles the code, into a Web Worker.

The tools that act on a Playground do it where the reader can see it. `edit_playground` opens the Playground's section if it's collapsed and scrolls it into view. It puts the new code in the editor as a reader's edit, so Reset brings back the example. A request an agent sends shows in the Call tab, and a test run shows in the Tests tab.

<a id="the-server-in-the-page"></a>

## The server in the page

The tools are ordinary FrontMCP tools. This is `get_current_page`, as the site has it:

```ts
export const getCurrentPage = tool({
  name: "get_current_page",
  title: "The page the user is on",
  description:
    "Returns the frontmcp.dev page open in this tab: its path, title, and the section headings on it, each with the anchor that links to it. Use it to know what the user is reading before you answer, or to point them to a section.",
  inputSchema: {},
  annotations: { readOnlyHint: true },
})(async () => ({
  path: sitePath(location.pathname).path,
  title: document.querySelector("main h1")?.textContent?.trim() || document.title,
  headings: Array.from(document.querySelectorAll<HTMLElement>("main :is(h2, h3)[id]"), (heading) => ({
    level: Number(heading.tagName[1]),
    text: heading.textContent?.trim() ?? "",
    anchor: `#${heading.id}`,
  })),
}));
```

The server is FrontMCP's `create()`, with the plugin. The site's version also wraps `modelContext` to count calls, which comes up [below](https://frontmcp.dev/blog/webmcp-docs-site#keep-the-plugins-mark):

```ts
import { create } from "@frontmcp/sdk";
import { WebMcpPlugin, type ModelContext } from "@frontmcp/plugin-webmcp";

export function startSiteServer(modelContext: ModelContext) {
  return create({
    info: { name: "frontmcp.dev", title: "FrontMCP docs", version: SDK_VERSION },
    tools: [...docsTools, ...playgroundTools],
    plugins: [WebMcpPlugin.init({ modelContext })],
    tasks: { enabled: false },
  });
}
```

[`create()`](https://frontmcp.dev/reference/sdk/create) builds the server in the page, with no transport. The plugin lists the server's tools and registers each one with `document.modelContext.registerTool()`. An agent's call runs through the server's own call flow, as an MCP client's would. The arguments are checked against the tool's schema, and hooks and limits apply.

*[Illustration: A WebMCP call on frontmcp.dev. The page registers its 13 tools with Chrome's document.modelContext from tools.json. The agent calls getTools and gets 13 tools with schemas and hints, then calls executeTool for search_docs. Chrome runs the tool in the page with the query webmcp. On the first call only, the page loads FrontMCP, 96 files. The FrontMCP server's call flow checks the schema and runs hooks and limits, the result goes back to Chrome, and Chrome gives the agent the result as a JSON string.]*

[Tool annotations](https://frontmcp.dev/reference/sdk/tool#describing-side-effects-with-annotations) become WebMCP hints:

| MCP annotation | WebMCP hint |
| --- | --- |
| `readOnlyHint` | `readOnlyHint` |
| `destructiveHint` | `consequentialHint` |
| `openWorldHint` | `untrustedContentHint` |

The four tools whose results include what code produced (`run_in_playground`, `edit_playground`, `call_playground` and `run_playground_tests`) carry `untrustedContentHint`.

<a id="nothing-extra-for-readers-without-an-agent"></a>

## Nothing extra for readers without an agent

Most readers have no agent in the page, and most browsers have no WebMCP. An earlier version loaded FrontMCP as soon as the page loaded, whenever `document.modelContext` was there. That turned out to include Lighthouse: the Chrome for Testing build it launches turns WebMCP on through its built-in field-trial configuration. Every Lighthouse run evaluated 2.7 MB of script after the page loaded. That was one 250 ms task at mobile CPU speed, which Lighthouse counted as about 360 ms of Total Blocking Time.

Now the page works in three steps:

*[Illustration: What frontmcp.dev loads for WebMCP, and when, measured on the live site on October 26, 2026. When the page loads, every reader runs a few lines that check for document.modelContext and stop without it: nothing more. After the load event, once the page is idle, browsers with WebMCP register the 13 tools from tools.json: one file, 5.4 KB. On an agent's first call, FrontMCP loads and the server starts in the page: 96 files, about 540 KB, 311 ms on a laptop. Every call after that takes 1 to 4 ms.]*

| Step | When | What the page loads |
| --- | --- | --- |
| Check | On every page, in the main bundle | Nothing more. A few lines check for `document.modelContext.registerTool` and stop when it's missing |
| Register | After the `load` event, once the browser is idle | One file, 5.4 KB compressed: the 13 tools' names, descriptions, schemas and hints, recorded at build time |
| Start | On an agent's first call | [FrontMCP's browser build](https://frontmcp.dev/reference/deployment/browser#the-browser-build), the plugin and the tools: 96 files, about 540 KB compressed |

The first step is the whole loader:

```ts
export function loadWebMcp() {
  const modelContext = (document as Document & { modelContext?: ModelContext }).modelContext;
  if (typeof modelContext?.registerTool !== "function") return;
  const register = async () => (await import("./register")).registerTools(modelContext);
  const whenIdle = () => ("requestIdleCallback" in window ? requestIdleCallback(register, { timeout: 500 }) : setTimeout(register, 200));
  if (document.readyState === "complete") whenIdle();
  else window.addEventListener("load", whenIdle, { once: true });
}
```

The second step uses two functions the plugin has for this. At build time, `listWebMcpTools()` starts the server with a stand-in `modelContext`, records what the plugin registers, and disposes of the server. The site writes the result to `tools.json`, and its checks fail when that file is out of date. In the page, `registerWebMcpTools()` registers those descriptions, and loads the server when an agent first calls a tool:

```ts
import { registerWebMcpTools, type ModelContext } from "@frontmcp/plugin-webmcp/register";
import tools from "./tools.json";

export function registerTools(modelContext: ModelContext) {
  return registerWebMcpTools(modelContext, tools, async (handoff) => (await loadSiteServerModule())(handoff));
}
```

`@frontmcp/plugin-webmcp/register` doesn't import the SDK, so a page with WebMCP loads that one small file until an agent calls a tool. First calls that arrive together share one load. Once the server is up, the registered tools stay registered, and each call goes through the server.

On the live site, an agent's first call to `get_version` took 311 ms, loading included, with no long task. With the CPU slowed four times, as Lighthouse does for mobile, it took 352 ms, with two long tasks of 67 and 88 ms. Later calls took 1 to 4 ms. The first `search_docs` also downloads the 77 KB search index. These are single runs on an Apple-silicon laptop over a broadband connection.

<a id="a-bundle-of-its-own"></a>

### A bundle of its own

Imported the usual way, the server's code didn't stay apart from the site's. Rolldown, the bundler under Vite, moved modules the tools share with the site, like the header's search and Zod, into new shared chunks. Every page loaded one or two more files.

The fix was to import the server's entry as a worker:

```ts
// A worker build only to get a bundle of its own: FrontMCP shares no chunk with the site. It runs on the page.
import siteServer from "./start?worker&url";
```

Vite builds a `?worker` import as a separate bundle that shares no chunk with the page's code, and `&url` gives its URL instead of a `Worker` constructor. The page loads that URL with `import()`, so the code runs on the page, not in a worker. A worker build keeps no exports, so the module announces its start function with an event on `window`:

```ts
window.dispatchEvent(new CustomEvent<StartSiteServer>(SERVER_LOADED, { detail: startSiteServer }));
```

<a id="code-an-agent-runs"></a>

## Code an agent runs

`run_in_playground` and `edit_playground` run code an agent wrote. It runs where a reader's edits run: in a Web Worker on frontmcp.dev's origin, with no access to the page's DOM. `run_in_playground` gets a worker of its own for each run, which is terminated afterwards, and a 60-second limit. Like a reader's code, it can `fetch()` anything CORS allows and use the origin's IndexedDB and Cache Storage. The site keeps nothing of the reader's there.

What the code does, compile errors and failing tests included, comes back as a result. A call fails only when the agent asked for something the site can't do, like reading a page on another site. The error then says what to do instead. That matters more than it sounds, as the next section shows.

<a id="what-we-learned"></a>

## What we learned

<a id="chrome-doesnt-pass-a-tools-error-message-to-the-agent"></a>

### Chrome doesn't pass a tool's error message to the agent

When a tool throws, Chrome 153 gives the agent `UnknownError: Tool was executed but the invocation failed. For example, the script function threw an error`, and writes the message to the page's console:

```text
WebMCP tool execution failed: Uncaught Error: https://example.com/ isn't on frontmcp.dev. Give a path on this site, like /learn/your-first-tool.
```

An agent that reads only the tool's result never sees "Give a path on this site". If a tool has something the agent needs to know, it belongs in the result.

<a id="not-every-hint-arrives"></a>

### Not every hint arrives

`edit_playground` replaces the reader's code, so it's registered with `consequentialHint` as well. Chrome 153's `getTools()` returns `readOnlyHint` and `untrustedContentHint`, but not `consequentialHint`: it lists `edit_playground` with `untrustedContentHint` only. Don't count on that hint to make an agent ask first.

<a id="test-tools-dont-see-the-page-your-readers-see"></a>

### Test tools don't see the page your readers see

Chrome for Testing has WebMCP on through its field-trial configuration, and Lighthouse runs it that way. Playwright turns field trials off, but on frontmcp.dev the origin-trial token turns WebMCP back on. With no flags, Chrome for Testing 153 under Playwright had `document.modelContext` on frontmcp.dev, and not on agentfront.dev, which serves no token. To measure what a reader without WebMCP gets, the site's Lighthouse script passes `--disable-features=WebMCP`.

<a id="keep-the-plugins-mark"></a>

### Keep the plugin's mark

The site counts agent calls by tool, success and duration, without arguments or results ([site usage and privacy](https://frontmcp.dev/reference/site-privacy)). It does that by wrapping `modelContext` before it reaches the plugin. `registerWebMcpTools()` marks the `modelContext` it hands the server with a symbol, and the plugin looks for that mark to find its registered tools. A wrapper that doesn't copy it fails the first call with `The server has no WebMcpPlugin on the modelContext it was given`. Spreading the original `modelContext` into the wrapper keeps it.

<a id="tools-in-a-page-have-no-manifest-entry"></a>

### Tools in a page have no manifest entry

frontmcp.dev lists what it offers agents in an [Agentic Resource Discovery](https://agenticresourcediscovery.org/spec/) manifest, `/.well-known/ard.json`: the [docs MCP server](https://frontmcp.dev/blog/docs-mcp-server) and the llms.txt files. Neither the spec nor Lighthouse has a media type for a page's WebMCP tools, so they aren't in it; agents find them in the page. Lighthouse 13.5's Agentic Browsing category scored `/learn/your-first-tool` 0.98. It listed the 13 tools and found their schemas valid. It flagged the Markdown type of the llms.txt entries, which isn't on its list, and noted one form without WebMCP annotations: the Playground's Call tab.

<a id="adding-webmcp-to-your-site"></a>

## Adding WebMCP to your site

If your page builds a FrontMCP server with [`create()`](https://frontmcp.dev/reference/sdk/create), install the plugin with the SDK at the same version:

```bash
npm install @frontmcp/sdk @frontmcp/plugin-webmcp
```

Add `WebMcpPlugin.init()` to the server's plugins. Without WebMCP the plugin does nothing, so the same server works in every browser. Three things to know from the start:

- **Page-only tools.** [`availableWhen: { surface: ["webmcp"] }`](https://frontmcp.dev/reference/sdk/register-tool#the-webmcp-call-surface) offers a tool only to browser agents, like one that fills in a form for the user to review. `["mcp"]` keeps a tool from them.
- **The caller.** Without `authContext`, tools see an anonymous caller, `anon:webmcp`. Pass the signed-in user to have tools act as them.
- **Names.** `prefix` keeps your tools' names apart from other scripts' on the page.

For visitors, register your site for Chrome's WebMCP origin trial and serve its token; frontmcp.dev sends it as an `Origin-Trial` header. To try a page locally, turn on `chrome://flags/#enable-webmcp-testing`. The [plugin's reference](https://frontmcp.dev/reference/plugins/webmcp) has the rest, including a test that runs in Node with a stand-in `modelContext`.

<a id="try-it-on-frontmcpdev"></a>

## Try it on frontmcp.dev

frontmcp.dev serves Chrome's WebMCP origin-trial token, so in a Chrome version the trial covers, 149 to 162, every page has `document.modelContext`. Open any docs page, then run the snippet [above](https://frontmcp.dev/blog/webmcp-docs-site#what-an-agent-can-do-on-the-site) in DevTools' console. An agent in your browser that uses WebMCP finds the tools the same way.

<a id="whats-next"></a>

## What's next

WebMCP is a draft. The plugin follows the draft of 2026-09-30, and the origin trial ends on March 30, 2027. Coding agents outside the browser can read the same docs from the [docs MCP server](https://frontmcp.dev/learn/docs-for-your-agent). As calls are counted, we'll see which tools agents use, and that will decide what changes next. If an agent gets something wrong on the site, [open an issue](https://github.com/agentfront/frontmcp/issues) with what it asked for.
