I Added WebMCP to My Site. Here’s What It Took.

By Marko Denic
Contents
  1. What is WebMCP?
  2. What I built
  3. The easy part: declarative tools
  4. The fun part: imperative tools
  5. Turning it on: the origin trial
  6. Testing it
  7. Lighthouse now checks this
  8. Rough edges
  9. Is it worth it?

Every AI agent that lands on my CSS generators does the same thing: it looks at a slider, guesses what it does, drags it, reads the preview, and tries again. It works. Eventually. It’s also a terrible way to get border-radius: 40px 0 40px 0;.

So I added WebMCP to markodenic.com. Now an agent can ask the page directly: “give me a pill-shaped radius”, and get the CSS back in one call.

Here’s what I built, how, and what I’d tell you before you try it.

What is WebMCP?

WebMCP is a proposed web standard that lets a page expose its own tools to AI agents running in the browser. Instead of guessing which buttons to click, the agent gets a list: tool name, description, input schema. Then it calls one.

If you know MCP: same idea, different place. MCP runs on a server, separate from your site. WebMCP runs inside the page, with the user’s session, in the browser they already have open.

It’s early. Chrome has it behind an origin trial (more on that below), and the spec is still a Community Group draft. That’s exactly why I wanted to try it now.

What I built

Seven tools across the site:

Page Tool Returns
Glassmorphism generator generate_glassmorphism CSS
Neumorphism generator generate_neumorphism CSS
Border radius generator generate_border_radius CSS
Pulse animation generator generate_pulse_animation CSS
CSS buttons get_css_button CSS for one of 100+ buttons
QR code generator generate_qr_code PNG data URL
Blog + homepage search_articles Search results page

The whole thing is about 200 lines. No new dependencies.

The easy part: declarative tools

If you already have a form, you’re almost done. Add three attributes:

html
<form action="/blog/" method="get"
      toolname="search_articles"
      tooldescription="Search the blog's articles on web development, CSS, HTML and JavaScript. Opens the results page."
      toolautosubmit>
    <input type="search" name="s" toolparamdescription="Search keywords">
</form>

That’s it. The browser turns the form into a tool, the inputs into parameters, and toolautosubmit lets the agent submit it without an extra click. Browsers without WebMCP ignore unknown attributes, so nothing changes for anyone else.

The fun part: imperative tools

The generators are Vue components, so each one registers its tool when it mounts and removes it when it unmounts. I wrote one small mixin:

javascript
export default {
    mounted () {
        if (!document.modelContext) return;
        this.agentToolController = new AbortController();
        document.modelContext.registerTool(this.agentTool(), { signal: this.agentToolController.signal });
    },

    unmounted () {
        this.agentToolController?.abort();
    },
};

Two things worth pointing out:

  • Feature detection first. No document.modelContext, no tool. Zero risk for normal visitors.
  • Removing a tool = aborting its signal. Pass { signal } to registerTool, call abort() later.

Then each component describes its tool. Here’s the border radius one (slightly trimmed):

javascript
agentTool () {
    return {
        name: 'generate_border_radius',
        description: 'Set the border radius of each corner in the generator and return the CSS. Omitted corners match topLeft. Max is 150px or 50%.',
        inputSchema: {
            type: 'object',
            properties: {
                unit: { type: 'string', enum: ['px', '%'] },
                topLeft: { type: 'number', minimum: 0 },
                topRight: { type: 'number', minimum: 0 },
                bottomRight: { type: 'number', minimum: 0 },
                bottomLeft: { type: 'number', minimum: 0 },
            },
            required: ['topLeft'],
        },
        execute: ({ unit = 'px', topLeft, topRight = topLeft, bottomRight = topLeft, bottomLeft = topLeft }) => {
            this.corners = { topLeft, topRight, bottomRight, bottomLeft };
            this.setUnit(unit);
            return `border-radius: ${this.radiusCss};`;
        },
    };
},

My favourite detail: execute doesn’t compute anything on its own. It sets the same state the sliders set. So when an agent calls the tool, the user sees the preview change on the page. The agent and the human are using the same generator.

Keep descriptions short. Say what the tool does and the limits the agent needs to know (“Max is 150px or 50%”). The model reads every word.

Turning it on: the origin trial

Here’s the part that surprised me. Shipping the code isn’t enough. Without an origin trial token, document.modelContext doesn’t exist for your visitors, only for people who enabled a Chrome flag.

  1. Register your origin for the WebMCP trial at developer.chrome.com/origintrials.
  2. Add the token to your <head>:
html
<meta http-equiv="origin-trial" content="YOUR_TOKEN">

The current trial runs on Chrome 149 to 156 and ends on November 17, 2026. Put that date in your calendar.

For local or staging testing, the token won’t match your domain, so enable chrome://flags/#enable-webmcp-testing instead.

Testing it

Open a generator page, then the console:

javascript
const [tool] = await document.modelContext.getTools();
await document.modelContext.executeTool(tool, JSON.stringify({ topLeft: 40, topRight: 0, bottomRight: 40, bottomLeft: 0 }));
// 'border-radius: 40px 0 40px 0;'

Note the JSON.stringify. In Chrome 152, passing a plain object fails with “Failed to parse input arguments”. It’s the first thing you’ll hit.

I also wanted proof that I didn’t break the generators for everyone else. So I ran the old and new version of every component side by side in headless Chrome, fed both the same 200 random settings, and compared the rendered HTML and every copied snippet. 1,207 states, around 26,000 snippets, zero differences.

Lighthouse now checks this

Lighthouse has a new Agentic Browsing category. Here’s what each check needs:

  • WebMCP form coverage: a form with declarative tool attributes.
  • WebMCP schemas are valid: your tools’ input schemas.
  • llms.txt follows recommendations: I already had one. (What it is: llms.txt.)
  • ai-catalog.json schema is valid: a file at /.well-known/ai-catalog.json listing things agents can connect to.

The last one took some thinking. The catalog is meant for MCP servers, agents and agent skills, not page tools. I don’t run an MCP server, so I wrote an agent skill instead: a short markdown file telling agents when to use my generators and which tool lives on which page. The catalog lists that one skill.

Result on the homepage: 6/6.

Rough edges

  • executeTool wants a string. Covered above.
  • Discovery. Tools only exist on the page that registers them. An agent can’t know my border radius page has a tool until it opens it. There’s no standard way yet to say “these pages expose these tools”.
  • It’s a trial. The API can change, and it can disappear. Feature detection is not optional.

Is it worth it?

Honestly? For traffic, not yet. There’s no sign Google Search ranks you for it, and I can’t tell you how many agents call these tools today.

But it cost me an afternoon, it’s invisible to anyone who doesn’t need it, and my generators are now something an agent can use properly instead of fumbling with sliders. If you have tools, calculators, forms or search on your site, try it. Start with the declarative attributes on one form. It takes five minutes.

Try the tools yourself: markodenic.com/tools.