instrument your site with WebMCP / vcn #39 / two callers, one DOM
I FACILITATE THIS ROOM. TONIGHT WE BUILD.
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.
FIVE PLACES TO FIND THE WORK.
One QR on every slide. It points at immersivecommons.com/ray. Scan once, get all five.
AGENTS PAY A TAX TO USE YOUR WEB PAGE.
The tax is paid in latency, hallucination, and unreliability. You pay it every time an agent touches your product.
../research/dossier.md §1, §5. this is the pre-WebMCP baseline. scraping-based browser agents stay slow because the page tells them nothing.
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.
TWO CALLERS. ONE DOM.
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.
FOUR KEYS. THE DESCRIPTION IS THE WHOLE SIGNAL.
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.
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.
EXPERIMENTAL TODAY. ONE BROWSER, BEHIND A LAUNCH SWITCH.
../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.
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.
../research/dossier.md §1, §2. NLWeb ships a hello-world page that runs locally, a 9-line registration for Schema.org-tagged pages.
WEBMCP IS NEARLY INVISIBLE IN PUBLIC DISCOURSE.
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.
CODE LIVE ON CLAUDE CODE + 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.
data/sponsor_offers.yaml#zai. key good ~5 hours from approval. Windows: use $env:X="Y", not export.
RUN THE LABS WITHOUT A LOCAL GPU.
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.
data/sponsor_offers.yaml#nebius. day-of we apply Token Factory credits so you can run the labs even without a local GPU.
FIVE THINGS OPEN BEFORE DOORS.
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.
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.
● the demo · the whole registration
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 }] };
}
});
}
navigator.modelContext.registerTool with name, description, inputSchema, execute. [webmcp_spec]
The Tool Inspector fires add_to_cart by name.
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: {
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]
Add Schema.org, and the page answers questions too.
<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]
Four labs. You keep all of them.
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 ->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 ->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 ->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 ->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.
clone, instrument, ship. the starter lives at /takehome/. [self]
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]
Three layers. Build them in order.
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]
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.
- Never auto-apply a high-impact tool. A page that the agent reads can try to talk it into calling one. The consent gate on s29 is the defense.
- In-page consent for anything consequential. The human stays in the loop at the moment of action, not in a setting they forgot they toggled.
- Assume the agent's context is contaminated. Page text is untrusted input. Your tool's authority must not depend on the agent's good judgment.
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.
- TRAP 1production today. Single-digit browser adoption. Ship server-side MCP first, WebMCP as the enhancement on top.
- TRAP 2no HITL. A destructive tool with no consent gate.
delete_accountis not a one-shot. See s29. - TRAP 3duplicating server MCP. Backend actions stay backend. WebMCP is for in-page actions only, the ones that touch the DOM.
- TRAP 4skipping feature-detect. Always
if ('modelContext' in navigator)before you rely on it. Fall through to the button. - TRAP 5pure SPA, no SSR. The agent sees a blank page until JS loads. Server-render the content that matters.
- TRAP 6
<div onClick>not<button>. The same accessibility-tree blindness the whole agent stack fights. Semantic HTML is the floor.
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.
-
Recon-Act
arXiv 2509.21072Agents that reconnoiter a page and generate their own tools on the fly. WebMCP hands them those tools directly, so the generation step becomes deterministic instead of inferred.
-
BrowseMaster
arXiv 2508.09129A tool-augmented programmatic agent pair that browses by scraping. The pre-WebMCP baseline. This is the tax the inversion removes.
-
MCPToolBench++
arXiv 2508.07575The benchmark page-level tools should target once WebMCP stabilizes. How you will measure whether your instrumented page is actually callable.
By 10pm
Your page. Three tools an agent can call.
- A page you ship, instrumented once with navigator.modelContext.
- Three real in-page tools, feature-detected, falling through to the button.
- The tax paid one time. No more scraping, no more inferred affordances.
Everything you need, one screen.
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.
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.
300-plus builders, member resources, the Floor 10 space at Frontier Tower.
Sign up free, request a membership tier.
immersivecommons.com
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.
- RSVP. Every event is on Luma. One tap.
- Teach a night. Show one real thing you built. The room takes it from there.
- Sponsor or co-host. Reach Ray, Facilitator of VCN, on Telegram.
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.