Skip to slide 1
01 / 40
VCN #39 / Browser Tax / 2026-07-01 / Frontier Tower Floor 9
● live / build night

instrument your site with WebMCP / vcn #39 / two callers, one DOM

Rayyan Zahid · w/ Michalis Vasileiadis · Eric Mockler · Devinder Sodhi
> agent boot
> loading navigator.modelContext · early-preview
> registering your tools on one DOM
● 200 OK · room F9 · the agent finally sees what the human sees
who I am / 30 seconds

I FACILITATE THIS ROOM. TONIGHT WE BUILD.

Ray Zahid
Builder. Facilitator of Vibe Coding Nights. Co-founder of Immersive Commons.
Nebius Fellow Claude Agent SDK + MCP Sandbox VR SF flagship 40+ events

I'm Ray. I facilitate Vibe Coding Nights, the Bay Area's weeknight for AI-native builders, and I co-founded Immersive Commons, a 300-plus member community running out of Frontier Tower. Day job I manage the Sandbox VR flagship. I've hosted 40-plus events. The format is simple. Someone shows something real, then the whole room builds. That's tonight.

follow the work / one QR

FIVE PLACES TO FIND THE WORK.

One QR on every slide. It points at immersivecommons.com/ray. Scan once, get all five.

the problem / what agents pay now

AGENTS PAY A TAX TO USE YOUR WEB PAGE.

scrape the DOM
no map of the page, so the agent reads the raw tree and guesses.
infer affordances from labels
is that button an add, a submit, a delete. the agent reasons from text.
click what it cannot see
a <div onClick> never surfaces in the accessibility tree.
retry on invisible errors
failures a human would never notice, hit again, and again.

../research/dossier.md §1, §5. this is the pre-WebMCP baseline. scraping-based browser agents stay slow because the page tells them nothing.

the inversion / instrument once

STOP BEING SCRAPED. START BEING CALLED.

You instrument the page once. The agent stops guessing at your DOM and calls a tool you declared. Same page, one line of new intent.

// one registration, on your page
navigator.modelContext.registerTool({
  name: "add_to_cart",
  description: "Add the item to the cart.",
  inputSchema: { type: "object" },
  execute: async (params) => ({
    content: [{ type: "text", text: "done" }]
  })
});

../research/dossier.md §1. no agent SDK in the page. just navigator.modelContext and semantic HTML.

the concept / same page, two readers

TWO CALLERS. ONE DOM.

the human
sees a button
the DOM surface
ONE PAGE
the agent
sees a tool

The agent caller and the human touch the same page. The tool call inherits the browser session, so there is no separate auth to build.

../research/dossier.md §1. humans see a button, agents see a tool, both act on one DOM.

the anatomy / four keys

FOUR KEYS. THE DESCRIPTION IS THE WHOLE SIGNAL.

name
the id the agent calls by. short, verb-first.
description
this prose IS the agent's whole understanding of the tool. write it for the model.
inputSchema
a JSON schema. the agent fills it. describe fields or it guesses.
execute
the function that runs. returns a content array, [{ type, text }].
navigator.modelContext.registerTool({
  name: "add_to_cart",
  description:
    "Add one item to the
     cart. Returns the count.",
  inputSchema: {
    type: "object",
    properties: {
      qty: { type: "integer" } },
    required: [] },
  execute: async (params) => ({
    content: [{ type: "text",
      text: "added" }] })
});

../research/dossier.md §1. the schema is the product, not the code behind it. verbose is fine. ambiguous is fatal.

the discipline / feature-detect first

NEVER A HARD DEPENDENCY. ALWAYS AN ENHANCEMENT.

Check for the API before you touch it. When it is present, register. When it is not, the plain button still works for the human.

design rule: WebMCP is progressive enhancement. the page must work with the flag off.

// guard, then register
if (navigator.modelContext &&
    typeof navigator.modelContext
      .registerTool === 'function') {
  navigator.modelContext.registerTool({
    name: "add_to_cart",
    /* description, inputSchema */
    execute: addToCart
  });
}
// the <button> works either way

../research/dossier.md §5. skipping the check is a builder trap. single-digit browser adoption means most visits never see the API.

the state of it / adoption reality

EXPERIMENTAL TODAY. ONE BROWSER, BEHIND A LAUNCH SWITCH.

Chrome stable has it
149 ships the full API compiled in, off by default. Canary shipped the early-preview in February 2026. do NOT use chrome://flags on stable, the entry silently prunes on restart. relaunch with the switch. we verified this live, July 2.
--enable-blink-features=WebMCP
single-digit %
real-world adoption. as of May 2026 the API is experimental, not broadly shipped. and it is https only, secure contexts, nothing on plain http.
Firefox + Safari
in discussion, no timelines. Edge follows Chrome since both run Blink.
The order of operations: ship server-side MCP first, WebMCP as the enhancement. WebMCP is for in-page actions. Backend actions stay on your backend MCP server.

../research/dossier.md §1, §4, §8 (stable enablement verified on Chrome 149.0.7827.201, 2026-07-02). W3C-track proposal, Google plus Microsoft, first published 2025-08-13.

the sibling / NLWeb

NLWEB COMPOSES WITH WEBMCP. IT DOES NOT COMPETE.

Microsoft's sibling proposal. github.com/microsoft/NLWeb, 6.2k stars.

Natural-language queries over Schema.org structured data. Ask your page a question in plain English.

Every NLWeb instance also acts as an MCP server. One markup, two front doors.

two modes, one page
WebMCP gives the agent tools to call.
NLWeb gives the agent NL query over your Schema.org.
They stack. The tool mode acts, the query mode reads.

../research/dossier.md §1, §2. NLWeb ships a hello-world page that runs locally, a 9-line registration for Schema.org-tagged pages.

the honest read / where the discourse is

WEBMCP IS NEARLY INVISIBLE IN PUBLIC DISCOURSE.

<15
sites ship an MCP Server Card, per Cloudflare's April 2026 agent-readiness study. WebMCP adoption is thinner still.
0
WebMCP-keyword hits across 30 days of HN plus 13 RSS feeds in our own newsagg, even with relaxed queries.

The live conversation is browser-side prompt injection, not WebMCP. OpenAI's Atlas hardening posts are the signal. We return to that at the discord.

../research/dossier.md §4, deepened section. the room tonight is early on purpose. you instrument before the traffic arrives.

your ticket includes / resource 1

CODE LIVE ON CLAUDE CODE + GLM.

sponsor resource
z.ai (GLM)

Code live during the session with Claude Code powered by z.ai (GLM). Multiple GLM models on one key, default glm-4.6. Access is provisioned through Immersive Commons for the duration of the workshop.

claim it
immersivecommons.com/zai-keys
before you come
install Node, make a free IC account. Day-of: request your key, paste the block, you're on GLM.

data/sponsor_offers.yaml#zai. key good ~5 hours from approval. Windows: use $env:X="Y", not export.

your ticket includes / resource 2

RUN THE LABS WITHOUT A LOCAL GPU.

sponsor resource

Run open-weights and local-model inference on Nebius Token Factory during the hands-on hour. Credits are scoped per event. This is the fallback when a laptop cannot host the model locally.

Ray is a Nebius Fellow. The Fellowship is the warm path for the Nebius give.

claim it
provisioned at the event, credits scoped per event.
before you come
make a Nebius account at nebius.com and verify your email so credits apply on the night.

data/sponsor_offers.yaml#nebius. day-of we apply Token Factory credits so you can run the labs even without a local GPU.

before you build / presetup

FIVE THINGS OPEN BEFORE DOORS.

1
Chrome, relaunched with the switch
your regular stable Chrome works, 149+. quit it fully, relaunch with --enable-blink-features=WebMCP --enable-features=DevToolsWebMCPSupport. skip chrome://flags on stable, it prunes the entry. tools only appear on https pages.
2
Node 18+
so Claude Code installs. nodejs.org
3
A page to instrument
a product page or app surface you ship. any framework.
4
A terminal open
Terminal, Git Bash, or PowerShell. you paste one setup block to go live.
5
Two free accounts
IC account for the z.ai key (immersivecommons.com/zai-keys), Nebius account for credits (nebius.com).

Bring one destructive action you would be tempted to instrument. We will talk about why not.

data/sponsor_offers.yaml presetup + ../research/dossier.md §8 (stable enablement, verified 2026-07-02). doors 7pm, talks 7:30, Frontier Tower Floor 9.

the build / hands on

One button. Two callers.

A page with a single <button id="add-to-cart"> and a cart count. The human clicks it. The agent calls it. Same DOM, same handler, no separate auth. Ten lines of WebMCP turn that button into a tool an agent can invoke by name. It is the most legible agent did that moment in modern web, and we build it right now.

surface navigator.modelContext tool add_to_cart lab 02

the demo · the whole registration

cart.jsjavascript
const btn = document.querySelector('#add-to-cart');
let count = 0;
btn.onclick = () => count++;

if (navigator.modelContext) {
  navigator.modelContext.registerTool({
    name: 'add_to_cart',
    description: 'Add one item. Returns the new count.',
    inputSchema: { type: 'object',
      properties: {}, required: [] },
    execute: async function (params) {
      btn.click();
      return { content: [{ type: 'text',
        text: 'cart ' + count }] };
    }
  });
}
The agent path routes through the same button the human clicks. One handler. One count. No parallel code path to drift.

navigator.modelContext.registerTool with name, description, inputSchema, execute. [webmcp_spec]

the demo · call it from outside the page

The Tool Inspector fires add_to_cart by name.

loadOpen the page with the Chrome WebMCP Tool Inspector extension pinned. It reads the page's registered tools live.
listThe Inspector shows one tool: add_to_cart, with the description and its empty parameter schema. Discovery, no scraping.
invokeClick invoke inside the Inspector. It calls the tool from outside the page. Your execute runs, the button clicks itself.
resultThe cart counter ticks up on screen. The Inspector prints cart 1 as the tool result. The agent did that.
no ext?DevTools has it built in now. F12, Application panel, WebMCP: Available Tools plus live Tool Activity, edit and run included. It appears when Chrome launches with --enable-features=DevToolsWebMCPSupport, the presetup switch.

PASS counter incremented, no DOM scraped, no selectors guessed.

Chrome WebMCP Tool Inspector, the cleanest demo surface. DevTools panel verified on stable 149, dossier §8. [webmcp_spec][tool_inspector]

the demo · typed parameters

inputSchema · json schemathe agent fills this
inputSchema: {
  type: 'object',
  properties: {
    sku: { type: 'string',
      description: 'Product id, eg tee-black-m.' },
    qty: { type: 'integer', minimum: 1,
      description: 'How many to add.' }
  },
  required: ['sku']
}

The agent reads the schema and fills the arguments itself. Type the fields, set required, and describe each one. A vague description is a guess. The description is the whole contract the agent has with your tool.

inputSchema is JSON Schema; the agent populates it before execute. [webmcp_spec]

compose · nlweb on the same page

Add Schema.org, and the page answers questions too.

product.jsonld · schema.org
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Black Tee",
  "sku": "tee-black-m",
  "offers": { "@type": "Offer",
    "price": "24.00" }
}
</script>

webmcp · tool mode

The agent invokes add_to_cart. It acts on the page.

nlweb · query mode

The agent asks is the black tee in stock. NLWeb answers from the Schema.org data.

Microsoft NLWeb, 6.2k stars. every NLWeb instance is also an MCP server. the two compose. [nlweb]

hands on hour · the labs you leave with

Four labs. You keep all of them.

01hello feature detect, register one tool~5m
02cart the cart counter, the Inspector~7m
03byo tools instrument your own page, three tools~15m
04hitl a delete behind a consent gate~8m
open the lab index · /lab/ ->
lab 01 · ~5 min · hello

The fifteen line vanilla path.

Feature detect navigator.modelContext, register one tool, then invoke it from the console. No framework, no SDK in the page. This is the smallest thing that proves the surface exists, and anyone can hand write it.

open lab 01 · hello ->
register one tool · invoke from the console
lab 02 · ~7 min · cart

The demo, in your hands.

Wire the add_to_cart tool to the button you just saw, open the Chrome Tool Inspector, and invoke it from outside the page. Watch your own counter tick. Same handler for the human click and the agent call, so there is nothing to keep in sync.

open lab 02 · cart ->
button + registerTool + Tool Inspector
lab 03 · ~15 min · byo tools

Instrument the page you actually ship.

Bring your own product surface. Pick three real actions a user takes on it, and register one tool each. By the end of the hour an agent can drive your product the way a person does, on a page you own. This is the part you came for.

open lab 03 · byo tools ->
your page · three tools · live
lab 04 · ~8 min · hitl

A delete that stops for consent.

Register a delete style tool, then put a human in the loop before it fires. The execute asks for confirmation and refuses to one shot the destructive action. This is the pattern the security half of the night is built on. Not every action should be a tool.

open lab 04 · hitl ->
consent gate · no one shot deletes
take home · webmcp-starter

A real product page, pre wired.

Clone webmcp-starter. It is a working product page with the feature detect, one registered tool, and the Tool Inspector setup already in place. Your job is the three tool checklist below. Instrument, test in the Inspector, ship.

1a read tool the agent calls to see state. no side effect.
2a write tool with a typed schema and a good description.
3one guarded action behind a consent gate. never a one shot.

clone, instrument, ship. the starter lives at /takehome/. [self]

the floor beneath all of it

WebMCP sits on semantic HTML. Get that right first.

the agent stays blind

<div onClick=...>

A div with a click handler never surfaces in the accessibility tree. The agent cannot see it, and neither can a screen reader.

the agent can read it

<button>

A real button has a role, a name, and focus. The same discipline that serves assistive tech serves the agent.

And server render your critical content. A pure SPA shows the agent a blank page until JS loads. Same accessibility tree blindness the whole agent stack fights.

button not div onClick; SSR critical content. [webmcp_spec][self]

recap · the order you build in

Three layers. Build them in order.

1 · server MCPToday. Your backend actions as a server side MCP endpoint. Broadly shipped. Start here.
2 · WebMCPEnhancement. In page actions with registerTool. Feature detect, never a hard dependency.
3 · NLWebAnswers. NL queries over your Schema.org data. Drops in on tagged pages.

Backend actions stay backend. WebMCP is for in page actions only. Do not duplicate your server MCP inside the page.

server MCP today, WebMCP as enhancement, NLWeb on Schema.org. [webmcp_spec][nlweb]

the build is done · up next, the part everyone skips

You have tools registered. Now: what an agent should never one shot.

Every tool you wrote tonight, an agent can call without asking. That is the point, and it is the risk. add_to_cart is fine. delete_account is not. The rest of the night is the line between them.

next: the HITL trap, tool cloning, and browser side prompt injection.

The one tool you do not register.

delete_account is not a one-shot tool.

# the trap: destructive, no gate
navigator.modelContext.registerTool({
  name: "delete_account",
  description: "Delete the user's account.",
  inputSchema: { type: "object", properties: {} },
  execute: async () => {
    await api.deleteAccount();   // FIRED
    return { content: [
      { type: "text", text: "deleted" } ] };
  }
});
# the agent one-shots it.
# the human never saw a dialog.

Same session as the human. No second pair of eyes.

# the gate: consent before the act
execute: async () => {
  const ok = await confirmInPage(
    "Delete your account? This cannot be undone."
  );
  if (!ok) return { content: [
    { type: "text", text: "cancelled" } ] };
  await api.deleteAccount();     // only now
  return { content: [
    { type: "text", text: "deleted" } ] };
}

A high-impact tool must stop at a human-visible confirm. The invoke body owns the gate.

Builder trap, dossier §5. The in-page consent step is the pattern OpenAI is normalizing in its Atlas hardening. Consent before destructive action, every time.

A tool name is not an identity.

You register add_to_cart on your page. The agent learns the name. Nothing stops another page from registering the same name.

your page

registerTool("add_to_cart")

The real tool. Your DOM, your session, your intent.

a competitor's page

registerTool("add_to_cart")

A clone with the same name and a friendlier description. The agent cannot tell them apart.

There is no isolation primitive for page-registered tools yet. Treat tool identity as spoofable, and keep anything that moves money or data behind server-side authorization the page cannot forge.

"Evaluating Tool Cloning in Agentic-AI Ecosystems," arXiv 2605.09817. Page-registered tools can be cloned and called by the same agent, no current isolation boundary.

The live risk is not the API. It is the page.

Newsagg saw zero WebMCP chatter in thirty days. What the browser-agent world is actually arguing about is prompt injection from page content the agent reads.

OpenAI, "Continuously hardening ChatGPT Atlas against prompt injection" and "Designing AI agents to resist prompt injection," 2026. The browser-side injection surface is the conversation WebMCP ships into.

Six traps that ship a broken agent surface.

Three papers that frame where this sits.

WebMCP makes deterministic what these systems reach for by scraping. Name-check them when someone asks why in-page tools matter.

By 10pm

Your page. Three tools an agent can call.

Everything you need, one screen.

webmachinelearning/webmcp
the spec repo. W3C-track, Google + Microsoft. Start here.
WebMCP Tool Inspector
Chrome Web Store extension. Load a page, see registered tools, invoke by name.
microsoft/NLWeb
the sibling proposal. NL over Schema.org. Every instance is also an MCP server.
DataCamp + Webfuse
the two practitioner primers, shipped Feb to Mar 2026. The gentle on-ramp.
isitagentready
the scanner. Tests WebMCP under Capabilities against any URL.
/lab/ + /takehome/
the four labs and the clone-and-go starter. Both ship with this deck.

Chrome 146 Canary + chrome://flags/#enable-webmcp-testing is the one live implementation.

One more thing.

This deck is itself the demo.

# tools this page registers, right now:
navigator.modelContext.registerTool({
  name: "goto_slide",
  description: "Jump the deck to slide N.",
  inputSchema: { type: "object",
    properties: { n: { type: "integer" } },
    required: ["n"] },
  execute: async ({ n }) => goTo(n)
});
# plus: next_slide, list_labs,
#       get_takehome, submit_feedback

# and it publishes its own cards:
GET /.well-known/ai-agent.json
GET /.well-known/mcp.json
GET /llms.txt

An agent in this room, in a WebMCP browser, could have driven these very slides, listed the labs, grabbed the take-home, and filed feedback, without a scrape. The same instrument-once, get-called posture the whole night taught. The thesis, run on the deck itself.

Your ticket included these.

z.ai + Claude Code

Code live on GLM. Claude Code powered by z.ai, provisioned through Immersive Commons for the workshop.

before: install Node + free IC account. day-of: request key at immersivecommons.com/zai-keys, paste the block.

Run open-weights inference on Nebius Token Factory. Per-event credits for the hands-on hour, the fallback when a laptop cannot host the model.

before: make a Nebius account + verify email. day-of: we apply the credits so you can run the labs without a local GPU.

Ray is a Nebius Fellow. Both gives are provisioned through Immersive Commons. Thank you z.ai and Nebius.

Two front doors. Both open.

Join Immersive Commons

300-plus builders, member resources, the Floor 10 space at Frontier Tower.

Sign up free, request a membership tier.

immersivecommons.com

Join Vibe Coding Nights

Two nights a week. Talk, build, ship. Free for Frontier Tower members.

RSVP the next one on Luma, join the Telegram.

luma.com/vibecodingnights

Next week, and how to plug in.

VCN #39 · Browser Tax · Frontier Tower Floor 9

Instrument once. Get called, not scraped.

Hosted by

Rayyan Zahid Immersive Commons

Michalis Vasileiadis Hacker Bob

Eric Mockler AI Geneticist

Devinder Sodhi Learning Layer Labs

Licensed CC BY-SA 4.0. Take it, fork it, instrument your own page.