webmcp-starter

Netlify template · WebMCP origin trial

Make your site
callable by AI agents.

This page is a working single-page site — a link hub with a guestbook — that exposes exactly three WebMCP tools. You don't install it or copy files: you copy a prompt, hand it to your coding agent, and the agent builds and deploys the whole thing on Netlify with Agent Runners. Every file it writes is small and heavily commented, because the source is the documentation.

Checking for WebMCP support…

01 Two prompts. That's the template.

There is nothing to fork and nothing to download. Pick the prompt that matches what you have today, copy it, and paste it into a coding agent — Claude Code, Cursor, or a Netlify Agent Runner. Both prompts carry the current WebMCP API contract on purpose, so the agent receiving one won't fall back on the deprecated calls it may have memorised.

  1. Copy a prompt

    One click on the button in either header below. Prompt 1 builds this starter from nothing; prompt 2 adds tools to a site you already run.

  2. Hand it to your agent

    Paste it and let the agent work. With Netlify Agent Runners the coding agent runs on Netlify's own infrastructure and ships a deploy preview you can open and review, so "build it" and "deploy it" are the same step. There is no build step to configure and Netlify Blobs provisions itself on first write, so the guestbook works on the very first deploy.

  3. Verify in the browser

    WebMCP is an origin trial, so today you opt in locally. In Chrome 149 or newer, open chrome://flags/#enable-webmcp-testing, set it to Enabled, and relaunch.

    Then load the deployed site and read the status line at the top of the page. It reads live from document.modelContext, so tools registered means an agent in this browser can see them right now. To poke at each tool by hand, install the Model Context Tool Inspector extension (setup instructions in Chrome's WebMCP guide) — it lists registered tools, validates your JSON Schema, and lets you call them with your own arguments.

    Note: WebMCP needs a secure context and an origin-isolated document. Both are already true on Netlify, and the prompts tell the agent to write a netlify.toml that sends Origin-Agent-Cluster: ?1 explicitly so a later change can't silently switch the API off.

Prompt 1 — build this starter from scratch

Use this when you have no site yet, or you want your own copy of this one. It describes the three tools, the file layout, the comment style, and the self-documenting page in enough detail that an agent can reproduce what you're looking at — then deploy it and test it.

prompt · build this starter, then deploy it
Build a WebMCP-enabled personal site and deploy it to Netlify. It is a
working link hub with a guestbook that exposes exactly three WebMCP tools,
and the deployed page doubles as its own documentation.

STACK — no build step, deliberately.
- Static files, served exactly as written, so the code a visitor reads in
  DevTools is byte-for-byte the code on disk:
    public/index.html
    public/data/profile.json
    public/assets/webmcp.js   (the tool declarations — the centerpiece)
    public/assets/data.js     (every read and write, shared by page + tools)
    public/assets/ui.js       (rendering, form handling, copy buttons)
    public/assets/style.css   (hand-written, dark developer-tool aesthetic)
- One Netlify Function: netlify/functions/messages.mts, written in TypeScript
  with Config/Context types from @netlify/functions and an in-code
  `export const config = { path: '/api/messages' }`. Use Netlify.env.get()
  rather than process.env.
- Netlify Blobs for the guestbook — no provisioning and no migrations, so it
  works on the first deploy. One blob per message, keyed
  messages/<ISO timestamp>-<random>.json: timestamp-prefixed keys sort
  chronologically as plain strings, and separate blobs mean two concurrent
  writers can't clobber each other the way one shared JSON array would. Use
  strong consistency, because an agent usually re-reads immediately after
  writing to confirm and would otherwise miss its own write.
- netlify.toml: publish = "public", the functions directory, and headers
  Origin-Agent-Cluster: ?1 plus Permissions-Policy: tools=(self).
- No framework, no bundler, no front-end TypeScript. ES modules, single
  quotes, 2-space indent, no trailing semicolons, ~100 column wrap.

CONTENT — one source of truth.
public/data/profile.json holds name, tagline, location, availability, and a
links array of { label, url, description, tags }. The page renders from it
and the tools answer from it. Put every read and write in data.js and have
BOTH the UI and the tools call it. Never build a second code path for agents:
a tool is a thin wrapper around a capability the site already has, so a human
visitor and an agent can never be told different things.

THE THREE TOOLS — public/assets/webmcp.js.
Use exactly this API. It is current, and older patterns you may recall have
been removed from the spec:

  const modelContext = document.modelContext ?? navigator.modelContext
  if (modelContext && 'registerTool' in modelContext) {
    const controller = new AbortController()
    await modelContext.registerTool(definition, { signal: controller.signal })
  }

1. get_profile — READ. No arguments, so inputSchema is
   { type: 'object', properties: {} }. Returns name, tagline, location,
   availability, linkCount, and links (label, url, description).
   annotations: { readOnlyHint: true }.
2. search_links — SEARCH. One required string argument, query, with its own
   description telling the model to pass a keyword like "music" rather than
   the user's entire sentence. Return { query, resultCount, results } instead
   of a bare array: an empty array is ambiguous ("did it fail?"), while
   resultCount: 0 next to the echoed query reads as a real answer.
   annotations: { readOnlyHint: true }.
3. leave_message — WRITE. Arguments: name (optional, defaults to
   "Anonymous") and message (required, 280 chars max). POSTs to
   /api/messages. Deliberately NO readOnlyHint — its absence signals a side
   effect, which is what makes an agent confirm the exact wording with the
   user first. After a successful write, dispatch a
   'webmcp:messages-changed' CustomEvent on document so the visible guestbook
   re-renders: the user is watching while the agent works. Return
   { ok, saved, totalMessages }.

Rules for all three:
- snake_case, verb-first names. Descriptions written for a model: say what
  the tool returns and when to choose it over the others.
- inputSchema is REQUIRED and always a JSON Schema object. Give every
  property its own description.
- Return compact, self-describing data. Throw errors whose text tells the
  agent how to retry ("message is required and must be 1-280 characters"),
  not "Error: 400".
- Validate and enforce limits in the Function too. A tool declaration is a
  hint to a cooperative agent, never a security boundary.
- Feature-detect and keep the site fully usable when modelContext is absent.
- There is no unregisterTool(). To remove a tool, abort the AbortSignal you
  passed at registration — that is how a single-page app exposes
  route-specific tools: register on mount, abort on unmount.
- Do NOT use navigator.modelContext.provideContext(), clearContext(), or
  unregisterTool(). They were removed.

STYLE — the source is the documentation.
Comment far more heavily than you normally would, explain WHY rather than
what, and write for a stranger opening the file for the first time. Every
tool declaration gets a comment block covering what each field buys you.
Mark the sections of webmcp.js with //#region name … //#endregion, using
exactly these names: detect, tool:get_profile, tool:search_links,
tool:leave_message, register.

THE PAGE — the site and its docs are one artifact.
index.html must contain, in this order: the live profile, links, and
guestbook (with a plain HTML form that posts to the same endpoint the write
tool uses); a status line that reads document.modelContext at load and
reports honestly whether tools registered and how many; an annotated
walkthrough whose code blocks are fetched from assets/webmcp.js at runtime
and sliced out by those region markers, so the documentation cannot drift
from what actually runs; and a file map explaining why each file exists.
Render every user-supplied string with textContent, never innerHTML. Expose
the tool array on window for DevTools poking.

DEPLOY, THEN PROVE IT.
Deploy to Netlify and give me the live URL. Tell me how to see the tools:
Chrome 149+, enable chrome://flags/#enable-webmcp-testing, relaunch, and
install the Model Context Tool Inspector extension. Then exercise the
deployed site yourself and report the result of each step, quoting any error
text verbatim:
  1. List every registered tool with its name, description, and input schema.
  2. Call get_profile and tell me in one sentence who the page belongs to.
  3. Call search_links with "music", then with a query you expect to match
     nothing, and show me what the empty result looks like.
  4. Call leave_message, then re-read the guestbook to confirm the write
     landed and give me the new total.
  5. Call leave_message with an empty message on purpose and quote the error.
Use the tools only for those checks — no DOM reading, no simulated clicks —
and say so plainly if a tool is unavailable.

Prompt 2 — add WebMCP to a site you already have

Use this when the site already exists and you only want the tools. Point a coding agent at your project, paste this, and it inventories what the site can already do before wrapping those capabilities as tools — no parallel API layer, no second code path for agents.

prompt · add WebMCP to an existing site
Add WebMCP to this site so AI agents can operate it directly instead of
scraping the DOM. Read the codebase first, then implement.

STEP 1 — Inventory.
List the 3 to 5 highest-value things a user actually comes here to do
(search, filter, add to cart, book, submit, navigate to a record). Show me
that list and which existing function or endpoint already implements each
one. Do not invent new capabilities and do not build a parallel API layer:
each tool must call code the site already has.

STEP 2 — Implement, in one new module.
Create a single file (e.g. src/webmcp.js) that declares the tools and is
imported once at app startup. Use exactly this API — it is current, and
older patterns you may recall have been removed from the spec:

  const modelContext = document.modelContext ?? navigator.modelContext;
  if (modelContext && 'registerTool' in modelContext) {
    const controller = new AbortController();
    await modelContext.registerTool({
      name: 'search_products',
      description: 'Search the catalog by keyword. Returns matching products.',
      inputSchema: {
        type: 'object',
        properties: {
          query: { type: 'string', description: 'A keyword, e.g. "denim".' }
        },
        required: ['query']
      },
      execute: async ({ query }) => searchProducts(query),
      annotations: { readOnlyHint: true }
    }, { signal: controller.signal });
  }

Rules that matter:
- await registerTool. Register per document, on startup.
- inputSchema is REQUIRED and must be a JSON Schema object. With no
  arguments, use properties: {}. Give every property its own description.
- Name tools snake_case and verb-first. Write descriptions for the model:
  say what the tool returns and when to choose it over the others.
- Set annotations.readOnlyHint: true on anything that only reads. Leave it
  off for writes so the agent knows to confirm with the user first.
- Return compact, self-describing data. Include a count and echo the query
  for searches. Throw errors whose text tells the agent how to retry.
- Any tool that changes state must also update the visible UI, because the
  user is watching while the agent works.
- Validate and authorize on the server as you already do. A tool
  declaration is a hint to a cooperative agent, not a security boundary.
- There is no unregisterTool(). To remove a tool, abort the AbortSignal you
  passed at registration. In an SPA, register route-specific tools on mount
  and abort on unmount so only valid actions are ever offered.
- Do NOT use navigator.modelContext.provideContext(), clearContext(), or
  unregisterTool(). They were removed.
- WebMCP needs HTTPS and an origin-isolated document. Do not send
  Origin-Agent-Cluster: ?0, and do not rely on document.domain.

STEP 3 — Make it verifiable.
Add a small visible indicator that reports whether modelContext was found
and which tools registered. Keep the site fully functional when WebMCP is
absent: feature-detect, never assume.

STEP 4 — Document it.
Comment the module so the next developer can add a fifth tool by copying a
fourth. Then tell me how to test: Chrome 149+, enable
chrome://flags/#enable-webmcp-testing, relaunch, and use the Model Context
Tool Inspector extension.

Show me the STEP 1 list before you write any code.

02 The site itself

Deliberately tiny. A profile, some links, a guestbook. Everything below renders from data/profile.json — the same file the get_profile and search_links tools answer from, so a human and an agent can never be told different things.

Guestbook

Stored in Netlify Blobs. Writable by you, or by an agent via leave_message.

  • Loading messages…

03 The tool declarations, annotated

Every snippet below is fetched from public/assets/webmcp.js at page load and printed verbatim. It cannot drift from what actually runs, because it is what actually runs. This is also the code prompt 1 tells an agent to produce, so you can read the shape before you ask for it.

Feature detection first

No browser enables WebMCP by default yet. Detect it, and keep the page perfectly usable when it's missing — progressive enhancement, exactly as you'd treat any other origin trial.

Loading source…

get_profile — the read tool

The anatomy of a declaration: name, description, inputSchema, execute, annotations. The comment block explains what each field buys you. Note that the description is written for the model — it is the only signal the agent has about when this tool is the right one to reach for.

Loading source…

search_links — the search tool

Same shape, but now with arguments. The per-property description inside inputSchema is the highest-leverage text in the whole file: it's how an agent knows to pass "music" rather than the user's entire sentence.

Loading source…

leave_message — the write tool

Writes need more care. There is no readOnlyHint here, and its absence is the point — it signals a side effect, which is what makes an agent confirm with the user first. The tool also validates nothing itself: real limits live in the function, because a tool declaration is a hint to a cooperative agent, never a security boundary.

Loading source…

Registration, and where to add your fourth tool

Registration is a loop. WebMCP has no unregisterTool() — you pass an AbortController signal and abort it to remove a tool, which is how a single-page app exposes tools only on the routes where they're valid.

Loading source…

04 Every file, and why it exists

What prompt 1 produces. A dozen small files — you can read the whole thing over one coffee.

public/index.html
This page. The site and its documentation are the same artifact.
public/data/profile.json
All content. The first and often only file you edit.
public/assets/webmcp.js
The three tool declarations and registration. Start here.
public/assets/data.js
Reads and writes, shared by the page and the tools. One implementation, two callers.
public/assets/ui.js
Rendering, the guestbook form, and the live code snippets above.
public/assets/style.css
Styling. No framework, no build.
netlify/functions/messages.mts
The only server code: reads and writes the guestbook in Netlify Blobs.
netlify.toml
Publish directory and the headers WebMCP wants.
package.json · README.md · AGENTS.md
One dependency, human docs, and agent docs.