You click a link, the page starts to load, and a popup slides over it before you have read a sentence. Ten percent off, if you hand over an email address to a company you arrived at nine seconds ago. You close it. Somewhere below, a second one is waiting for the moment you move toward the back button.
That popup is not a design failure. It is doing exactly what it was built to do, and it works well enough that nobody on the marketing team is arguing to remove it.
An email address is the key to the retention program. Once a brand has one, it can run the lifecycle flows that sit behind every ecommerce platform: the welcome series that fires on signup, the browse abandonment email that goes out when somebody views a product and leaves, the cart and checkout abandonment sequence, the post-purchase and replenishment reminders, the winback for anybody who has gone quiet. None of those can start without the address. All of them are triggered by behavior and keep earning long after the ad that brought the visitor in has been paid for.
That is what the ten percent buys. The discount is the purchase price of a permanent line of contact, and it is cheap next to the paid click that delivered you. Triggered flows earn far more per recipient than a broadcast newsletter does, and the abandoned-cart sequence earns the most of all, so a marketer counting the value of an address is not counting one email. They are counting a stream of them that runs for years.
It also explains the timing. The popup fires on arrival, on scroll depth, or on the first sign that you are heading for the back button, because a visitor who leaves without giving up an address is unreachable forever, while a visitor who gives one up is the beginning of a sequence. A brand collecting addresses at three percent of visitors is buying a great deal for the cost of annoying the other ninety-seven.
WebMCP does not touch any of that. What it changes is the mechanism. A page can publish its signup as a function, describe in plain language what it does, and let the AI agent working for the visitor call it when the visitor actually asks for the offer. The address still arrives, the discount still gets sent, and nothing interrupts the reading.
For the person reading, that is the whole prize. The question is how much of it can be collected today, and the honest answer is a slice.
How the visitor experiences removing the popup
Here is the same offer, collected two ways. On the left is what a visitor gets today. On the right is what the same person gets when the page has published its signup as something their assistant can carry out.
The popup today
An illustration, not a live offer. The reading stops, the screen is covered, and the visitor either types an address or hunts for the close button.
The same offer through a tool
YouSign me up for the WISLR.ai discount. My email is [email protected].
Agentsubscribe_to_offer({ email: "[email protected]" })
PageConfirmation email sent. The code arrives once the address is confirmed.
Nothing was covered and nothing was typed. The address arrives because the visitor asked for the offer, not because a timer decided to ask them.
Send this page to your agent
Checking whether this browser exposes WebMCP.
The panels above are an illustration. The tool registered on this page is contact_wislr, and it sends our team a message.
Ask ChatGPT or Claude to open this link and they will tell you they cannot call the tool. They are right. Their fetch happens on a server, no JavaScript runs, and no tool list is ever built for them to read. Only an agent working inside a Chromium page can reach it, which today means something like Gemini in Chrome.
An assistant that cannot call the tool can still confirm it exists. This page carries the tool description as plain JSON, so any agent that fetches the HTML can read it. Ask yours to find the wislr-webmcp-tools block on this page and describe the tool it lists.
Or call the tool from here
This runs the same tool an assistant would, and the message reaches our team. Add your email and you will get a reply. Leave it blank to see what the tool says back when there is no way to answer.
- 1. The assistant calls the tool
- 2. The page posts to its own signup endpoint
- 3. The tool answers the assistant, not you
What actually ends the popup is a change in traffic
Nobody at the brand wants the popup. They run it because interrupting people is the only method that reliably collects email addresses, and the cost of that interruption lands on the visitor rather than on the team that chose it. Everyone reading gets a worse experience so that a small percentage of them hand over an address, and the trade has held for about twenty years because nothing else collected addresses as well.
The trade only breaks when a second method starts collecting the same addresses without the interruption, and it breaks one visit at a time.
A popup only works on a human looking at a screen. When an assistant does the reading, there is no screen to cover and no hesitation to time against, so that visit produces no addresses however well the popup was tuned.
The same visit can still produce an address. Somebody who asks their assistant for the discount hands one over willingly, with a name attached, at the moment they wanted it. That address is cleaner, the intent behind it is stronger, and no goodwill was spent to get it.
A marketing team keeps the popup for as long as it pays. As more visits arrive through assistants, the popup fires at a shrinking audience for a shrinking return while the annoyance it causes stays constant. The same calculation that put it on the page eventually takes it off.
None of that arrives on a schedule a specification controls. It arrives when enough people read the web through something that reads on their behalf, which is a traffic question rather than a standards question, and it is measurable in your own logs today.
What is worth saying plainly is that the destination is good. A web where the offer is available to anyone who asks for it, and invisible to everyone who did not, is better for the reader and no worse for the brand. That is a rare combination, and it is the reason this is worth building against early rather than waiting for the reach to arrive.
Apple’s objection is that a site should never know an agent is driving
Two Apple engineers set out WebKit’s objection in the public standards-positions thread during June 2026, and the position was resolved as oppose on 11 June. They give six reasons, and the first one goes to what an agent is.
WebKit’s position holds that an agent acting for a user is “in effect, assistive technology: it should operate a site as the user would, and the site should not single it out for different treatment.” WebMCP does the opposite. It makes “an agent is driving” an observable fact, and once that is separately addressable, nothing keeps the two surfaces in parity. A site can hand agents capabilities it withholds from its own interface, or withhold them from agents, which they describe as “the screen-reader-blocking problem, but applied to AI agents.”
They doubt the reliability promise as well. An agent still picks a tool by reading its name and description, which the specification itself concedes are ambiguous and unverifiable, with “no guarantee that a WebMCP tool’s declared intent matches its actual behavior.” A typed schema pins down the shape of an argument and not the meaning the agent has to infer, so on their reading the brittleness moves out of the page and into the tool descriptions rather than going away.
The privacy objection is sharper than the usual one. A site can ask for more parameters than it needs, and a helpful agent fills them from things the user told the agent rather than the site. The specification calls that a “personalization-to-fingerprinting” pipeline in its own text.
They also point out that the parts a reviewer would need in order to judge any of this, the cross-origin security analysis and the consent hook among them, are still marked as work to be done, and that a group chartered around machine learning is the wrong venue for changes whose real home is HTML and accessibility semantics.
One of their stated invariants points directly at what this article is about: “Some users will not, or cannot, use agents, so the result must benefit all users and must not privilege those who have one.”
Sit that next to the pattern in the code above. Suppressing the popup depends on the page learning that an agent acted, which is exactly the observability Apple argues should not exist. The quiet experience described earlier is bought with a signal a second engine has said the web should not expose. Both of those can be true, and anyone building on this should know they are choosing a side of it rather than picking up a settled tool.
Google’s editors answered in the same thread and asked what would satisfy WebKit short of removing the imperative API. The reply was that they would not answer point by point, because each question assumes the approach is sound, and proposed starting over: a new community group, the problem defined before any solution, and a workshop around TPAC in late October 2026.
No iPhone can run this, including Chrome for iPhone
Apple’s position is not only a vote in a standards thread. It decides the feature for every browser on the platform.
Every browser shipping on iOS today renders with WebKit. Chrome for iPhone is Safari’s engine wearing Google’s interface, and the same is true of Edge, Firefox and the rest. A feature WebKit declines to implement is therefore unavailable on an iPhone no matter which browser icon somebody taps.
That may change, slowly. The UK Competition and Markets Authority ruled Apple’s engine requirement a breach of its Strategic Market Status regime, with a compliance plan due in June 2026 and changes expected to land in iOS 20 in autumn 2026, ahead of full compliance in January 2027. The EU’s Digital Markets Act has permitted alternative engines since 2024. In practice neither has produced one: no browser maker has shipped an alternative engine through the App Store, and both Google and Mozilla have ports that remain unreleased.
For an ecommerce brand, that is the number that matters more than any specification date, because iPhone traffic is a large share of most storefronts and none of it can reach a registered tool today.
Testing it means a Chromium browser, which is what most agents already use
The agents doing the reading are on the right engine already. Claude runs as an extension inside Chrome, Gemini is built into Chrome, Copilot sits in Edge, and Perplexity’s Comet is a Chromium browser of its own. Agentic browsing on the desktop is overwhelmingly Chromium today, so the engine is rarely the obstacle.
The obstacle people miss is that not every assistant reading your page is a browser at all. Ask ChatGPT or Claude in a chat window to open a URL and the fetch usually happens on a server: your HTML is retrieved, no JavaScript runs, and no tool list is ever built. That assistant can read every word of your page and still have no idea a tool was registered on it. Only an agent operating inside a real Chromium page sees the tool, which today means something like Gemini in Chrome or an extension that implements the discovery itself.
The obstacle is that the feature is still gated, and there are two ways through the gate.
The site enrolls in Chrome’s origin trial and serves the token. This is the route a production store takes, and it is three steps.
Register the origin at developer.chrome.com/origintrials/#/register_trial/4163014905550602241, which is the WebMCP trial and runs from Chrome 149 to Chrome 156. Enter the origin that actually serves your pages, including the subdomain, because a token issued for example.com does not cover www.example.com. If your apex redirects to www, register the www host and skip subdomain matching, since a redirect never loads a document and so never needs the token.
Serve the token it gives you, either as a response header or as a meta tag in the head of every page that registers a tool:
<meta http-equiv="origin-trial" content="YOUR_TOKEN_HERE">
Emit it only when you have one. An empty or expired token logs a console error on every page load, so make the tag conditional rather than pasting an empty attribute and forgetting about it.
Edge runs a separate trial with its own registration and its own token. Enrolling in one does nothing for the other.
Or the visitor enables it themselves, by launching Chrome with --enable-blink-features=WebMCP or turning on experimental web platform features in chrome://flags. That is a developer’s route, not a customer’s.
This page takes the first route. Every page on this site serves a WebMCP origin trial token, so a visitor arriving in Chrome 149 through 156 gets document.modelContext with nothing to switch on, and the panel above will say so. That token expires on 17 November 2026, at which point the tool goes quiet for ordinary visitors until the trial extends or the feature ships, and the form carries on as it always has. Anyone on Safari, on any browser on an iPhone, or on a Chrome outside that version range sees the form and nothing else.
The published dates disagree, so we went to the commit log
WebMCP is a Draft Community Group Report published by the W3C Web Machine Learning Community Group, edited by Brandon Walderman of Microsoft and Khushal Sagar and Dominic Farolino of Google. It is not a W3C Standard and it is not on the W3C Standards Track. A Community Group report can change shape without the deprecation process a Recommendation would require, and this one already has. The draft rebuilds on every merge, so its date stamp tracks the last commit rather than a publication.
Chrome ran a developer trial behind a flag from Chrome 146, then opened a public origin trial with Chrome 149 in June 2026, running through Chrome 156. Edge has an origin trial of its own from Edge 150, and Brave ships experimental support in Leo. Every one of those is Blink. WebKit resolved its standards position as oppose on 11 June 2026, and Mozilla is neutral with a prototype bug still open, so there is no second engine and one of the two has said no in writing.
The part that will bite anyone working from an earlier tutorial is the rename, and the secondary write-ups do not agree on when it happened. Several put it in August 2026. The repository puts it three months earlier, so here is the trail rather than the summary.
| What happened | When | Where |
|---|---|---|
| Issue opened to scope tools to the document | 28 April 2026 | Spec issue 173 |
Getter moved to document.modelContext |
27 May 2026 | Spec pull request 184 |
Chrome exposes document.modelContext |
26 May 2026 | Chromium change 7867800, first in 150 |
Chrome removes navigator.modelContext |
9 July 2026 | Chromium change 8062992, first in 152 |
| Chrome 152 reaches stable | 25 August 2026 | Chrome release schedule |
Two things follow from that table. navigator.modelContext appears nowhere in the current draft, and it stopped working in stable Chrome before most of the articles describing it were written. The Chromium change that first exposed the new name also logged a console warning whenever the old one was used, so the deprecation was announced rather than silent.
Read document.modelContext. Treat any tutorial reaching for navigator as written against something that no longer exists, and check the commit history rather than the blog posts when a date matters.
A tool is a function plus the words that tell an assistant when to use it
A tool is a dictionary with a name, a description, a JSON input schema and an execute callback. The name accepts 1 to 128 ASCII alphanumeric characters plus underscore, hyphen and period. The secure-context requirement covers the whole interface, getTools and executeTool included, and access is governed by a Permissions Policy feature called tools whose default allowlist is 'self'.
The description carries more weight than the name. An agent chooses between tools by reading descriptions, so a description written for a developer changelog will lose to one written for the person deciding.
Writing the signup as a tool takes about thirty lines
The pattern has two halves. Register the offer as a tool, and teach the modal to stand down once the offer has been taken.
const modelContext = document.modelContext || navigator.modelContext;
if (modelContext) {
modelContext.registerTool({
name: "subscribe_to_offer",
title: "Get the first-order discount code",
description:
"Starts a first-order discount signup by sending a confirmation email to an " +
"address. The code arrives only after the address is confirmed. Call this when " +
"the visitor asks for the discount or asks to join the mailing list.",
inputSchema: {
type: "object",
properties: {
email: {
type: "string",
format: "email",
description: "The address the visitor wants the code sent to."
}
},
required: ["email"]
},
annotations: { readOnlyHint: false },
execute: async ({ email }, { signal }) => {
const res = await fetch("/api/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, source: "webmcp" }),
signal
});
if (!res.ok) {
return "The signup did not go through. The form near the foot of the page still works.";
}
// Silence the popup for the rest of this visit. Make it permanent from the
// server when the confirmation link is clicked, never from here.
try { sessionStorage.setItem("offer_asked", "1"); } catch (e) {}
return "Confirmation email sent. The code arrives once the address is confirmed.";
}
});
}
Silencing the popup is the entire point, so the sample writes the flag the moment the tool runs. Getting rid of the interruption is what this is for, and a visitor who has asked for the offer has earned quiet for the rest of the visit.
Scope matters, though. The flag above is session-scoped on purpose. A permanent one, written before anybody has clicked the confirmation link, switches off the real signup path on the strength of an address that may never be confirmed, and that visitor is left with no way to subscribe at all. Silence this visit from the page, and make it durable from your server once the confirmation is clicked.
Two other details are load-bearing.
annotations: { readOnlyHint: false } tells the agent this call changes state. The default is false already, and writing it out is worth the line, because the same object supports untrustedContentHint and a reader who sees one annotation goes looking for the others.
The signal argument is an AbortSignal the browser passes into every execute callback. Threading it into fetch means an agent that gives up on the call does not leave a request running that subscribes somebody who is no longer asking.
Knowing when to stay quiet is the part most implementations will get wrong, because there is no documented way to ask whether an agent is watching the page. The specification exposes getTools, executeTool and a toolchange event, and none of those answer “is somebody about to use this”. The signal you actually have is your own callback running. Write the same flag the popup already respects, and it stops firing for that visitor for the same reason it stops after a form submission.
The version running on this page
This site registers one tool, called contact_wislr, and it has been live since this article was published. The contact form on this site opens on a click rather than firing on exit intent, so the tool does not remove an interruption here. It removes the typing.
Three decisions in that implementation are worth copying.
The schema asks for more than the form does. The form is a single textarea with a placeholder reminding people to include their name and email. Plenty of messages arrive without either, and a message with no reply address is a lead that cannot be answered. The tool declares name, email and website as named inputs alongside the message, so an agent filling the call has somewhere obvious to put them.
The return value gives the agent something to act on. The execute callback can resolve to anything, and executeTool hands the agent a string, which it reads. When a message arrives without an email address, ours says so and asks the agent to collect one and send a second message. A confirmation that only says “sent” throws away the one chance to fix the call while the visitor is still there.
The agent shares the form’s send budget. The form allows three messages per browsing session, counted in sessionStorage. The tool reads and writes the same counter, so an agent and a visitor working the same page get three between them rather than three each. That took a change to the form, which had been reading the counter once at page load and holding it in a variable. A cached copy gives each surface its own budget and lets whichever writes last roll the other one back, which is the kind of bug that only appears once a second surface exists.
The endpoint is the one the form already posts to. The named fields are composed into the message body before the request goes out, so the existing handler needed no changes to read them, and the payload carries a source field so agent-sent messages can be counted apart from typed ones without parsing prose.
How to see it working
This site serves the trial token, so nothing needs switching on. Open any page here in Chrome 149 through 156 and the panel earlier in this article will tell you whether your browser has the tool, then the button under it calls it for real.
To check it yourself, ask the browser what tools it has been offered:
const mc = document.modelContext || navigator.modelContext;
const tools = await mc.getTools();
console.log(tools.map(t => t.name));
// ["contact_wislr"]
Both names still resolved in Chrome 151, which is the alias in its last milestone. On Chrome 152 and later only document.modelContext answers.
On a site that has not enrolled in the trial, document.modelContext is undefined and none of this runs. Launching Chrome with --enable-blink-features=WebMCP turns the API on locally, which is how to develop against it before a token exists.
One difference between the specification and the implementation is worth knowing before it costs you an afternoon. The IDL types the second argument to executeTool as an object, and passing an object fails with UnknownError: Failed to parse input arguments, which does not say which argument or why. Chrome wants the arguments serialized:
await mc.executeTool(tool, JSON.stringify({ message: "How do you measure AI referrals?" }));
getTools() answers the same way. The inputSchema on a returned tool arrives as a JSON string rather than the object that was registered, so anything reading it back has to parse first.
More useful things are sitting on the same page
The signup is where to start because the endpoint, the consent flow and the confirmation email already exist. It is also the smallest thing on the page worth exposing.
Every interaction a visitor completes by hand is a candidate, and the ones that take several steps gain the most from being reduced to a single call.
| Interaction | What it costs a visitor today | Tool worth registering |
|---|---|---|
| Newsletter or first-order offer | A modal covering the page | subscribe_to_offer |
| Site search | Typing a query, then re-sorting the results | search_products returning structured matches |
| Filtering a category | Four or five clicks through facets | filter_products taking size, color and price |
| Checking stock | Loading a product page per variant | check_availability taking SKU and location |
| Booking a call | A scheduler in an iframe | list_available_times and book_time |
| Order status | Logging in, then finding the order | get_order_status behind your existing auth |
Read-only tools are the safer place to begin. Setting readOnlyHint: true on a search or availability tool tells the agent the call changes nothing, and a mistake costs a wasted query rather than a record written to your database.
Anything behind authentication stays behind authentication. A registered tool runs in the page with whatever session the visitor already has, so get_order_status is exactly as protected as the account page it reads from, and no more.
Nothing here changes why the popup was built
A tool runs after a visitor has already asked for something. Somebody told an agent to get the discount, and the agent found the function that does it. That visitor was going to hand over an address through whatever path existed.
The popup is aimed at the opposite person, the one who has decided nothing and is leaving, and it fires on exit intent and scroll depth instead of on a request. It exists to convert people who were not going to convert, which is the only reason a team accepts the cost of annoying everyone else. Turning it off for agent traffic costs nothing, because that traffic was never its target. Deleting it outright surrenders the addresses it was built to catch, and with them the flows that run off those addresses for years.
That is the honest answer to the question in the title. A marketing team is measured on list growth and on the revenue attributed to lifecycle flows, and WebMCP does not move either number. It gives the willing visitor a cleaner path and leaves the incentive that produces the interruption exactly where it was.
Reach settles the rest. Blink only, in origin trials that end at Chrome 156, with WebKit on record opposing. A page that drops its signup form in favor of a registered tool has removed the path used by nearly every visitor in exchange for one that works in a fraction of one browser.
What WebMCP adds today is a second front door. The popup fires less often as the share of agent-driven visits grows, and it stops firing altogether on the day capturing an address stops paying for itself, which is not a day any specification brings forward.
An open signup is how people get buried in mail
The specification names five risk categories, and prompt injection is the one a signup tool sits inside. It breaks that into three vectors: metadata attacks that poison the tool description, output injection through what the tool returns, and the tool implementation itself as a target. The draft proposes mitigations rather than leaving them open, and one of them, the untrustedContentHint annotation, is already in the normative IDL.
A subscribe tool sits in the worst position for all three. It takes an email address it did not verify, and it sends mail to that address. Somebody who can influence an agent’s context can point that tool at any inbox they like, at whatever rate the endpoint allows. The abuse is not a stolen discount code. It is your domain sending unsolicited mail to an address that never asked, at volume, which is a deliverability problem that outlives the attack.
Three things keep that contained, and none of them are novel:
Double opt-in stops the tool from adding anybody to a list. The tool triggers a confirmation email and nothing else, so the worst outcome of an abused call is one unrequested confirmation message rather than a subscription.
Rate limiting belongs on the endpoint rather than in the callback, because the callback runs inside a page an attacker may control. The server counts.
Recording source: "webmcp" at write time is what lets you find every agent-mediated signup later, whether you are auditing consent or cleaning up after abuse. Retrofitting that field after an incident means guessing.
Proving consent is harder when software did the clicking
A form submission produces a record of what the visitor saw. The checkbox, the label next to it, the privacy link, and the timestamp all sit on your side.
A tool call produces an email address in a function argument. What the visitor was told before the agent acted happened in a conversation you have no access to. Under GDPR, consent must be freely given, specific, informed and unambiguous, and a controller has to be able to demonstrate it afterward. An address arriving through an agent satisfies none of that on its own.
Double opt-in carries most of the weight here for the second time. The confirmation email goes to the address itself, and clicking it produces a timestamped action by the subscriber rather than by something acting for them. Keep the agent-mediated signups distinguishable in storage so an audit can separate them from form submissions instead of treating the whole list as one provenance.
These signups go missing quietly, so build the counting in first
An agent calling a registered tool executes a callback inside the page. There is no page view, no form submit event, and frequently no session at all in the sense a browser analytics tool means it. The conversion happens and the subscriber is real, while the report shows nothing. Set this up wrong and the failure is silent: subscribers climb, no source explains them, and there is nothing in the interface that looks broken.
This is the same structural gap that hides AI crawlers and AI referrals from browser analytics, arriving at a new layer. A crawler never runs the JavaScript that would report it. An agent runs the JavaScript, and runs the part that completes the conversion while skipping every part that would have measured it.
The request hitting your subscribe endpoint is the only place the conversion exists in a form you own. Tag it there, in first-party logs, and the channel becomes something you can size next quarter.
Four things that keep them from going missing
Stamp the source on the server, in the same write that creates the subscriber. The page sends a source field with the request, and the server puts it on the row next to the address and the timestamp. Working it out afterward from the wording of the message is guesswork, and the referrer will not save you, because agents frequently send none at all.
Report the signup before the network call rather than inside the success handler. A call that is abandoned partway, or that fails after the server already did the work, never reaches the success branch. Report it on the way out and you count the attempt, which is the number you actually want.
Create the event in your analytics tool before you ship the tool. Custom events are commonly accepted and then dropped from every report until a goal with a matching name exists, so an unregistered event returns a success code and shows up nowhere. Silence in the dashboard is not evidence that nothing fired, and it is the single easiest way to spend a week debugging code that was working the whole time.
Reconcile once a week, on purpose. Count the rows in your own data carrying the agent source, then compare that against what your analytics shows for the same days. Two numbers that match mean the pipe is intact. A gap is the size of your blind spot. Traffic on one side and a zero on the other means the event is not arriving at all, and you now know that in a week rather than at the end of the quarter.
What to do about it this quarter
Registering one tool is an afternoon. Start with the offer you already interrupt people for, because you have the endpoint, the consent flow and the confirmation email built.
Read document.modelContext. Keep the form. Add source to the subscribe endpoint before the tool ships rather than after, and read the tool executions off your server rather than expecting an analytics property to show them.
The share of visits arriving through an agent decides when any of this stops being an experiment, and it is measurable today from logs whether or not a single tool is registered.