The endpoint answered correctly the first time I asked it anything. Six tools, fifteen resources, a real client connecting over the real protocol. It was broken for three days and every test I ran said it was fine.
What I built
Two things, which are easy to confuse and do completely different jobs.
The first is an MCP server at /mcp — the same Cloudflare Worker that renders these pages also speaks the Model Context Protocol. It exposes six tools: the dates and remaining seats at the roundtable I host, search across these field notes, any note in full, what a Clarity Sprint involves, and one tool that requests a seat. Fifteen notes are also published as resources. No authentication; the content is public and the one tool that writes is guarded differently.
The second is WebMCP — a browser API, currently behind a flag in Chrome, that lets a page hand tools to an agent running in that browser. Cloudflare has a switch for it that injects a bridge script into every page. The bridge asks the site's own /mcp what tools exist and registers whatever comes back.
So the two are not alternatives. The server is the thing; the bridge is a second door into it. Which matters, because when the bridge does nothing, the natural conclusion is that the bridge is broken.
Four reports, mostly wrong
I asked several agents and readiness checkers to look at the site while I was building this. Between them they produced a lot of confident findings. Here is what they said, against what was actually true.
- “
.well-known/mcp.jsonnot found.” It returned 200. It had returned 200 for two days. - “No AI directives in robots.txt.” There were ten named user-agent groups in it.
- “No MCP Link header.” Present on every response on the domain.
- “No
readOnlyHintordestructiveHintannotations.” All six tools carried them. I printed the raw JSON to be sure. - “The email and phone fields clear instantly.” Reported twice, by two different agents. I could not reproduce it with real keyboard input, with scripted input, or by navigating between form steps.
One of them also awarded points for WebMCP HTML attributes that appear nowhere in the built site. A tool that invents a pass will invent a failure. This is the second time this year that what something called itself turned out to be the least reliable field in the request.
Four reports agreed the form could not be used by an agent. All four were wrong about why, and one of them was right anyway.
That last part is the reason I did not dismiss them. The observable claim — an agent cannot complete this form — kept coming back. Every explanation offered for it was false. So the finding was real and the diagnosis was noise, which is the least comfortable combination to be handed.
The one header
I stopped testing the form and started testing the thing the bridge actually does, which is to POST tools/list to /mcp from the page it is running on. From a page on my own site, that request came back:
403 {"jsonrpc":"2.0","error":{"code":-32000,
"message":"Invalid Origin: hovhannes.io"}}The MCP handler protects against DNS rebinding by refusing any request whose Origin header it does not recognise, and what it recognises by default is localhost and *.workers.dev. A custom domain is on neither list. So the bridge asked, got refused, logged that it was registering no site tools, and exposed nothing — which is exactly what four agents described and none of them could explain.

Here is the part worth keeping. A request with no Origin header is not a browser, and is allowed through. That describes curl. It describes every command-line MCP client. It describes Claude, ChatGPT and Cursor, which connect from a server rather than a browser. It describes all four readiness checkers. And it describes every test I had written, because I wrote them with curl.
The endpoint answered perfectly to everything I was able to point at it. The single caller that failed was the single caller I had no way to imitate by hand — and it was the one the whole feature existed for.
A test that cannot send the header that triggers the failure is not a weak test. It is a test of something else.
The fix was one option: allow this hostname. Not "*" — an endpoint that accepts any origin lets any page on the web make a request from a visitor's browser, and one of my tools writes to a database. Verified afterwards, in this order: my own origin returns 200, a foreign origin returns 403, and a caller sending no origin at all is unaffected.
The experiment that failed
Chrome also supports a declarative form of WebMCP: put toolname and tooldescription on a <form> and the browser registers a tool that fills it. Two of the checkers marked its absence as the largest single deduction on the page. I had refused it twice on reasoning, which is a bad way to refuse anything, so I built it and measured what happened.
Chrome registered the tool correctly — twelve parameters derived from the form fields, six of them required. Then, on my four-step application form, three things happened in order.
- 1. It filled invisibly.
Every field was set, including the ones on steps three and four. The person looking at the screen still saw step one, apparently untouched. “The user reviews and confirms” does not work when there is nothing to review.
- 2. The call never returned.
Over three minutes, still pending. An agent cannot say “done, please check this” while it is still waiting.
- 3. Then it reported failure.
Chrome's completion signal for a declarative form tool is the form's
submit. My form clears itself after a successful send, so that somebody's name and phone number do not sit in the DOM afterwards. Chrome reads that reset as a cancellation:Tool execution cancelled by a form reset. The request went through. The agent was told it had not.
An agent that believes it failed will try again. That is the one outcome worse than having no tool at all, so the attributes are not on the form. The experiment is the reason, not my opinion — and the checker's fifteen points stay unclaimed.
What the platform will not carry
An MCP tool can declare annotations: readOnlyHint, destructiveHint, and a few others. They are how a client knows which tools are safe to run without asking. Mine declare them, and I checked the wire to be certain.
In the browser they are gone. Chrome's WebMCP Tool object has no annotations field at all — not empty, absent:
ownKeys: [description, inputSchema, name, origin, title, window]The two tools Cloudflare's own bridge registers alongside mine are missing it too, which tells you this is the API and not the bridge. So the safety signal that a client would read cannot travel through the browser door at all. The only channel that does travel is the description, which is why the one tool that writes says so in words, in the first sentence a client reads.
I would rather write that sentence than assume the field will arrive later. When the structured field does not exist, the rule has to survive inside the prose — which is the difference between an instruction and a constraint showing up in a place I did not expect it.
Letting a stranger write
Five of the six tools only read. The sixth requests a seat at the table, and it is public: anybody's agent can call it. That is the point — a person asking their assistant to get them a seat should not have to open a four-step form built for a mouse.
The form protects that action with a bot check. Over MCP there is no browser to run one in, and an agent is supposed to be there, so the challenge has no true answer. What replaced it is not proof, it is a budget and a pause.
- Two calls, not one.The first validates everything, resolves the date against the real schedule, and reports exactly what would be sent without sending it. Only a second call writes. It is the same boundary as the one a coding agent should stop at, moved to the other side of the wire.
- A budget instead of a challenge.Five guest seats exist each month, so two requests a day per caller is already more than the table can absorb. The count lives inside the insert, not in a check before it — counting and then writing are two statements, and two callers who both counted one below the cap both write.
- Disclosure, not silence.The confirmation email says an AI agent made this request and how to undo it. That email is the only evidence the person whose name is on it ever agreed.
- Three fields the agent does not decide.Publishing a real person's name on a public page is their consent to give. A photo cannot arrive down a tool call. A referral code is credit for a signup — the one field worth inventing.
None of that makes the tool unabusable. It makes the worst afternoon it can produce something I can read in a minute and delete.
What I would tell you before you turn it on
Four things, in the order they cost me time.
- Test from a browser on your own domain, first. Not curl. The failure mode I hit is invisible to every client that does not send an
Originheader, and almost nothing you reach for sends one. - Do not trust a readiness score. Four of them looked at this site. Between them they were wrong about the discovery document, robots.txt, the Link header and the tool annotations, and one awarded points for markup that does not exist. Reproduce every claim before you change code for it — and take them seriously enough to test, because probing a wrong report is how I found two real defects nearby.
- Decide what a stranger may write before you publish the endpoint. Read-only is a different risk category, and it is fine to ship that first. A public write tool is a design problem, not a configuration one.
- Know which primitive puts you on a paid plan. None of this cost anything to add — the endpoint, the database behind it and the scheduled job all sit inside the free allowances. What moved this site onto a paid plan was one unrelated feature, decided months earlier.
- Publish it somewhere clients look. A Link header, a
<link>in every head and a line inllms.txtare all found only by something already reading your site. Clients read a registry. Until you are in one, the only people who can connect are the people you told.
The protocol was the easy part. It worked the first time and has not needed touching since. Everything that took the days was the same shape of problem: something true about how the site is reached that no test I could write was in a position to see.
Further reading
Primary sources behind this note
- Model Context Protocol: transportsThe streamable HTTP transport, and why a client has to accept both media types.
- Chrome: WebMCPThe browser API, its experimental status, and the declarative form of tool registration.
- Cloudflare: WebMCP on the edgeThe bridge that injects into every page and what its packs do.
Reader response
Was this field note useful?
Build on what you can verify
Want the same clarity about your product?
Apply for a Clarity SprintField notes by email
Thanks, you're on the list. ✓
Email signup did not go through. Please try again.
A short note when there is something worth sharing. No spam.
