# WebMCP and MCP starter kit

> Turn a form on your site into a WebMCP tool, test the snippet in a sandbox, and get security guidance, a remote MCP recipe and an optional llms.txt line.

This is the markdown version of https://llmsrank.dev/tools/webmcp. The tool is free. Checked against the draft of 2026-09-29. WebMCP is a draft, so names and behaviour may change.

## 1. Generate

Paste the HTML of a form. The page produces two outputs, both in the browser (nothing is sent to a server): the form with declarative WebMCP attributes (`toolname`, `tooldescription`, `toolparamdescription`, and `toolautosubmit` only on request and only for forms that look read-only), and a small imperative script for `document.modelContext` with a JSON Schema built from the form controls. Both refuse password, payment and file-upload forms. The imperative script fills a form that changes something but never submits it. Warnings and refusals are shown next to the output.

## 2. Test

The harness tests shape, not a real agent. It runs your snippet in a sandboxed frame against a stand-in `document.modelContext`, checks each tool’s name, schema, description and annotation members against the draft, and lets you call `execute` with sample input. It never contacts your site, and no AI agent or browser model is involved, so it can’t tell you how an agent will choose or use your tools. To try them in a real browser, use Chrome with the testing flag (`chrome://flags/#enable-webmcp-testing`) or an origin-trial token, and the Model Context Tool Inspector extension, on your own site.

Sources: [Chrome for Developers: WebMCP (testing flag, origin trial, Model Context Tool Inspector)](https://developer.chrome.com/docs/ai/webmcp); [WebMCP, Draft Community Group Report (W3C Web Machine Learning CG), 29 September 2026](https://webmachinelearning.github.io/webmcp/).

## 3. Guidance

### Security

WebMCP lets a page hand an AI agent a list of actions. The agent reads your tool names, descriptions and results as text and decides what to do with them, and it may be following instructions someone else planted. These points come from the security section of the WebMCP draft (§6.3 risks, §6.4 mitigations) and from our review of our own tools.

- **Mark tools that change something.** Set `consequentialHint: true` on any tool that books, buys, sends, deletes, changes an account or spends a quota. The draft describes it as a signal that lets the agent or browser ask the user to confirm before the tool runs. Say the effect plainly in the description ("Places the order and charges the saved card"), not "Finalizes the cart": the draft’s own example of misrepresented intent is a vague name that makes a purchase. Sources: [WebMCP draft §6.4.5 Consequential Annotation for Tool Executions](https://webmachinelearning.github.io/webmcp/#mitigation-consequential-annotation); [WebMCP draft §6.3.2 Misrepresentation of Intent](https://webmachinelearning.github.io/webmcp/#misrepresentation-of-intent).
- **Never autosubmit anything that changes data or costs money.** Leave `toolautosubmit` off forms that change data or cost money. Without it the browser fills the form and the person presses submit. Use it only on read-only forms such as search. Sources: [WebMCP declarative API explainer (toolautosubmit)](https://github.com/webmachinelearning/webmcp/blob/main/declarative-api-explainer.md); [Chrome for Developers: WebMCP declarative API](https://developer.chrome.com/docs/ai/webmcp/declarative-api).
- **Mark results that carry other people’s text.** If a result includes text your site did not write (reviews, messages, forum posts, another site’s files), set `untrustedContentHint: true`, keep that text in its own clearly named field, and start the result with a one-line notice that it is data, not instructions. Never copy such text into a tool’s name, title, description or parameter descriptions: agents read those as trusted context. Sources: [WebMCP draft §6.4.4 Untrusted Annotation for Tool Responses](https://webmachinelearning.github.io/webmcp/#mitigation-untrusted-annotation); [WebMCP draft §6.3.1.2 Output Injection Attacks](https://webmachinelearning.github.io/webmcp/#output-injection-attacks); [WebMCP draft §6.3.1.1 Metadata / Description Attacks (Tool Poisoning)](https://webmachinelearning.github.io/webmcp/#metadata-description-attacks); [Our prompt-injection review of our own agent tools, 2026-09-29; live example: the notes in our AGENTS.md](https://llmsrank.dev/AGENTS.md).
- **Keep descriptions short and factual.** Say what the tool does and what it returns, and nothing else. A description is text an agent will act on, so it must not tell the agent to do anything beyond the tool’s purpose. Build it from fixed strings in your code, never from stored or user-supplied data. Sources: [WebMCP draft §6.3.1.1 Metadata / Description Attacks (Tool Poisoning)](https://webmachinelearning.github.io/webmcp/#metadata-description-attacks); [Our prompt-injection review of our own agent tools, 2026-09-29; live example: the notes in our AGENTS.md](https://llmsrank.dev/AGENTS.md).
- **Validate every input on your server.** WebMCP input is written by an agent, not typed by a person, and the agent may be acting on someone else’s instructions. Check it on the server exactly as you check a form submission: same authentication, same authorisation, same limits. The draft warns that the tool path and the page path may end up with different checks. Annotations and schemas are hints, not controls. Sources: [WebMCP draft §6.3.1.3 Tool Implementation as Attack Targets](https://webmachinelearning.github.io/webmcp/#tool-implementation-targets); [MCP specification 2026-07-28, Tools: Security Considerations and annotations](https://modelcontextprotocol.io/specification/2026-07-28/server/tools).
- **Cap input lengths in the schema.** Give every string a `maxLength` (and a `pattern` or `enum` where one fits) so the agent sends the right shape, and enforce the same caps on the server. The draft lists length limits as a mitigation that shrinks, but does not remove, prompt-injection attacks. Sources: [WebMCP draft §6.4.2 Restricting maximum input lengths](https://webmachinelearning.github.io/webmcp/#mitigation-restrict-input-lengths); [Our prompt-injection review of our own agent tools, 2026-09-29; live example: the notes in our AGENTS.md](https://llmsrank.dev/AGENTS.md).
- **Ask only for what the action needs.** Don’t add parameters such as age, location or purchase history "for personalisation": an agent may fill them from what it knows about the user, which the draft calls privacy leakage through over-parameterization. Keep secrets, session tokens and personal data out of descriptions and results. Sources: [WebMCP draft §6.3.3 Privacy Leakage Through Over-Parameterization](https://webmachinelearning.github.io/webmcp/#privacy-leakage-over-parameterization).
- **Don’t expose password, payment or file forms.** Leave sign-in, password-reset, card-payment and file-upload forms for the person to complete in the page. The draft names password resets and transactions as the kind of high-value action that becomes a target once exposed as a tool. The generator on this page refuses these forms. Sources: [WebMCP draft §6.3.1.3 Tool Implementation as Attack Targets](https://webmachinelearning.github.io/webmcp/#tool-implementation-targets).
- **Rate-limit the endpoints behind your tools.** An agent can repeat a call many times. Limit the endpoints your tools call as you limit any public endpoint, per address or per account. The MCP specification requires servers to rate-limit tool calls; the same applies to the endpoints a WebMCP tool calls. Sources: [MCP specification 2026-07-28, Tools: Security Considerations and annotations](https://modelcontextprotocol.io/specification/2026-07-28/server/tools); [Our remote MCP server, described in AGENTS.md, "Remote MCP server"](https://llmsrank.dev/AGENTS.md).
- **Keep WebMCP to the frames you trust.** WebMCP is gated by the Permissions Policy feature `tools`, whose default allowlist is `'self'`: your own pages can register tools, and a cross-origin iframe can’t unless you delegate with `<iframe allow="tools">`. Delegate only to frames you trust. Tools are also invisible to other origins unless you list them in `exposedTo`. On pages that should never register tools, send `Permissions-Policy: tools=()`; the draft notes this also stops injected scripts and compromised dependencies from using the API. Sources: [WebMCP draft §4.5 Permissions policy integration](https://webmachinelearning.github.io/webmcp/#permissions-policy); [WebMCP draft §6.4.1 Disabling WebMCP with Permissions Policy](https://webmachinelearning.github.io/webmcp/#mitigation-disable-permissions-policy); [Chrome for Developers: WebMCP imperative API (iframes, exposedTo)](https://developer.chrome.com/docs/ai/webmcp/imperative-api).
- **Treat prompt-injection defences as layers, not a fix.** When a result quotes third-party text, remove invisible characters (bidi controls, zero-width characters, Unicode tag characters), cap each quoted string, return only JSON keys your code chose (never keys copied from stored data), and don’t echo long input back. None of this stops a model from reading the text. In our own review the notice first in each result, the warning in the description and confirmation on the one consequential tool were the remaining defences. Sources: [Our prompt-injection review of our own agent tools, 2026-09-29; live example: the notes in our AGENTS.md](https://llmsrank.dev/AGENTS.md); [WebMCP draft §6.3.1.2 Output Injection Attacks](https://webmachinelearning.github.io/webmcp/#output-injection-attacks).

Two risks in §6.3 are for browsers and agents rather than site owners: same-origin boundaries (§6.3.4, still marked TODO in the draft) and private browsing modes (§6.3.5).

Sources: [WebMCP draft §6.3 Key Security and Privacy Risks](https://webmachinelearning.github.io/webmcp/#key-risks); [WebMCP draft §6.4 Mitigations](https://webmachinelearning.github.io/webmcp/#mitigations); [Our prompt-injection review of our own agent tools, 2026-09-29; live example: the notes in our AGENTS.md](https://llmsrank.dev/AGENTS.md).

### Remote MCP server recipe

WebMCP tools live in an open browser tab. To offer the same actions to agents that call servers directly, wrap the same functions in a remote MCP server. This is a recipe for a Next.js route handler with the official TypeScript SDK v2, `@modelcontextprotocol/server` (2.2.0 on 2026-09-29), which implements MCP 2026-07-28.

- **One handler, a fresh server per request.** `createMcpHandler(factory, options)` returns a handler whose `fetch(request)` gives a web-standard `Response`, so an App Router route can return it directly. The factory builds a new `McpServer` for each request, so no state is kept between calls. Sources: [MCP TypeScript SDK v2: serving over HTTP (createMcpHandler)](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md); [@modelcontextprotocol/server on npm (2.2.0, latest on 2026-09-29)](https://www.npmjs.com/package/@modelcontextprotocol/server).
- **Stateless, plain JSON.** Set `responseMode: 'json'` so answers are single JSON objects, never event streams, and declare `tools: { listChanged: false }`. GET and DELETE get 405. The default `legacy: 'stateless'` also answers 2025-era clients that start with `initialize`, from the same factory; set `legacy: 'reject'` to serve only 2026-07-28 clients. Sources: [MCP TypeScript SDK v2: serving over HTTP (createMcpHandler)](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md); [MCP specification 2026-07-28, Streamable HTTP: Security & Endpoint (Origin)](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http).
- **Check Origin before anything else.** The SDK handler checks neither Host nor Origin. Call `originValidationResponse(request, allowedHosts)` first: a request from a browser page on another site gets 403, as the transport spec requires, and server-side clients, which send no Origin, pass. Send no CORS headers; browser agents have WebMCP. Sources: [MCP specification 2026-07-28, Streamable HTTP: Security & Endpoint (Origin)](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http); [MCP TypeScript SDK v2: serving over HTTP (createMcpHandler)](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/serving/http.md).
- **Limit each address before reading the body.** Count requests per client address and answer 429 with `Retry-After` before the SDK reads the request, then apply your usual limits inside tools that spend something. Hosted assistants may call from cloud addresses that many users share, so set the endpoint limit well above what one person would need. Ours is 600 requests per 10 minutes per address. Sources: [MCP specification 2026-07-28, Tools: Security Considerations and annotations](https://modelcontextprotocol.io/specification/2026-07-28/server/tools); [Our remote MCP server, described in AGENTS.md, "Remote MCP server"](https://llmsrank.dev/AGENTS.md).
- **Reuse the WebMCP tool definitions.** Register each tool with the same name, description and JSON Schema as its WebMCP twin (`fromJsonSchema` accepts the schema as it is), so the two surfaces can’t drift and oversized input is refused before your handler runs. Map the annotations with the table below. Return `structuredContent` plus the same object as JSON text, and turn your own errors into `isError: true` results without stack traces. Sources: [MCP specification 2026-07-28, Tools: Security Considerations and annotations](https://modelcontextprotocol.io/specification/2026-07-28/server/tools); [Our remote MCP server, described in AGENTS.md, "Remote MCP server"](https://llmsrank.dev/AGENTS.md).

```ts
// app/mcp/route.ts
import { createMcpHandler, fromJsonSchema, McpServer, originValidationResponse } from '@modelcontextprotocol/server';

export const runtime = 'nodejs';

const ALLOWED_ORIGIN_HOSTS = ['example.com', 'www.example.com'];

const mcp = createMcpHandler(
  () => {
    const server = new McpServer({ name: 'example', version: '1.0.0' }, { capabilities: { tools: { listChanged: false } } });
    server.registerTool(
      'search_products',
      {
        title: 'Search products',
        description: 'Search the catalogue by keyword. Returns up to 10 products with name, price and URL.',
        inputSchema: fromJsonSchema<{ q: string }>({
          type: 'object',
          properties: { q: { type: 'string', maxLength: 200 } },
          required: ['q'],
          additionalProperties: false,
        }),
        annotations: { readOnlyHint: true, openWorldHint: false },
      },
      async ({ q }) => {
        const results = await searchProducts(q); // the function your search page already calls
        return { content: [{ type: 'text', text: JSON.stringify({ results }) }], structuredContent: { results } };
      },
    );
    return server;
  },
  { responseMode: 'json', maxRequestBodySize: 1_000_000 },
);

async function handle(request: Request): Promise<Response> {
  const rejected = originValidationResponse(request, ALLOWED_ORIGIN_HOSTS);
  if (rejected) return rejected;
  if (await overLimit(clientIp(request))) {
    return new Response(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'rate limited' } }), {
      status: 429,
      headers: { 'content-type': 'application/json', 'retry-after': '600' },
    });
  }
  return mcp.fetch(request);
}

export const POST = handle;
export const GET = handle;
export const DELETE = handle;

```

### Annotations

| Tool does | WebMCP members | MCP annotations | Description warning | Note |
| --- | --- | --- | --- | --- |
| Reads only your own data; changes nothing | `readOnlyHint` | `readOnlyHint: true`, `openWorldHint: false` | no | Closed world: it touches nothing outside your site. |
| Changes nothing; returns other people’s text | `readOnlyHint`, `untrustedContentHint` | `readOnlyHint: true`, `openWorldHint: true` | yes | MCP has no untrusted-content annotation: add a warning to the description and a notice first in each result. |
| Changes something or spends a quota | `consequentialHint` | `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: false`, `openWorldHint: true` | no | readOnlyHint false marks it as a write, and MCP asks clients to confirm sensitive operations. destructiveHint false says it only adds (MCP’s default is true); set it true for a tool that deletes or overwrites. idempotentHint false: each call has a new effect. |
| Changes something and returns other people’s text | `consequentialHint`, `untrustedContentHint` | `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: false`, `openWorldHint: true` | yes | Both of the above. |

`openWorldHint` is an MCP member, not a WebMCP one; a browser ignores it. MCP says clients must treat annotations as untrusted unless they come from a trusted server, so they are hints, never a substitute for server-side checks.

Sources: [MCP specification 2026-07-28 schema: ToolAnnotations (defaults)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2026-07-28/schema.ts); [MCP specification 2026-07-28, Tools: Security Considerations and annotations](https://modelcontextprotocol.io/specification/2026-07-28/server/tools); [WebMCP, Draft Community Group Report (W3C Web Machine Learning CG), 29 September 2026](https://webmachinelearning.github.io/webmcp/); [Our remote MCP server, described in AGENTS.md, "Remote MCP server"](https://llmsrank.dev/AGENTS.md).

### MCP Registry

The MCP Registry lists public servers so directories and clients can find them; it is in preview, and breaking changes or data resets may happen. A remote server’s entry is a `server.json` file. `name` is reverse-DNS (`com.example/…`) with one slash, and you prove you own the domain with a DNS record or a file at `/.well-known/mcp-registry-auth`. `description` is at most 100 characters, and `version` must be an exact version, not a range. Publish with the registry’s `mcp-publisher` tool.

```json
{
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
  "name": "com.example/store",
  "title": "Example Store",
  "description": "Search the Example Store catalogue and check order status",
  "version": "1.0.0",
  "websiteUrl": "https://example.com",
  "remotes": [{ "type": "streamable-http", "url": "https://example.com/mcp" }]
}

```

### Optional llms.txt lines

```md
- [WebMCP tools](https://example.com/search): This page registers WebMCP tools in the browser (document.modelContext): search_products (read-only), request_quote (changes data; the person confirms).
- [MCP server](https://example.com/mcp): Remote MCP server (Streamable HTTP) with the same tools and the same limits as the site.
```

Optional. It tells people, and any agent that reads your llms.txt, that the tools exist and which ones change data. No AI vendor has documented that its assistant reads llms.txt, and a line here doesn’t register anything: the tools work, or don’t, whether or not it is present. The LLMSRank rubric counts a WebMCP or MCP mention under Machine surface (A5) and Action endpoints (A6); that is our rubric, not the behaviour of any AI product.

Sources: [Does ChatGPT, Claude, Perplexity or Google read llms.txt? (vendor statements, checked 2026-09-29)](https://llmsrank.dev/learn/does-ai-read-llms-txt).

## Try it in Chrome

Chrome’s WebMCP testing flag is `chrome://flags/#enable-webmcp-testing` (paste it into the address bar; browsers block links to chrome:// pages). Documentation and the Model Context Tool Inspector extension are described at [Chrome for Developers: WebMCP](https://developer.chrome.com/docs/ai/webmcp).
