Inspiration

Agent protocols assume every company will build and host an agent. Most won't. When agents show up, most of the web will still be ordinary websites - full of things worth asking about, with no way to be asked and no way to say no.

Why WebMCP fits this: a boundary between what an agent can ask and what it can take only exists when the page has an owner. Backend MCP is server-to-server with nobody watching, and its tool list is fixed config. WebMCP is different because a person is already there.

What it does

The agent asks. You grant.

The agent can call propose_data_source(url, description). It doesn't fetch anything. It puts an approval banner on the page and stops.

You read it and click Approve. Konect4AI turns the URL into a structured API, the page refreshes its capability list, and the new tool shows up in the agent's toolbox - same conversation, no reload. Click Reject and nothing runs.

The agent asks. It doesn't get the data.

ask_data_source(jobId, question) is answered by my own model, using my own API key. The tool returns the answer. The rows behind it are shown to me, in the panel next to it, and stripped before the agent sees anything.

The page shows this split:

  • SENT TO AGENT - the question and one answer.
  • NOT RETURNED THROUGH WEBMCP - the rows the answer came from, with a receipt: source, records consulted, which model computed it, backend verification.

This is a boundary on what the tool returns. It is not browser-level isolation, and the page says so. I'd rather claim something smaller and have it be true.

You decide per source what an agent can do.

Every source has an access mode. ASK-ONLY (default) means the agent can ask questions but the capability isn't registered as a callable tool. DIRECT means it is. Flipping it re-registers the toolset right away.

I added this after watching ChatGPT do the obvious thing: given both a bounded ask tool and an unbounded fetch tool, it used the fetch tool.

People and agents use the same tools in the same place.

There's an ask row under the two panels. It calls the same endpoint the agent calls and lands in the same history, labeled human instead of webmcp.

And the same registry works for agents that never open a browser.

A small A2A adapter publishes an Agent Card at /.well-known/agent-card.json, built live from the same registry. A site I connected ten minutes ago is discoverable by an external agent - without anyone building an agent for it.

How we built it

The tools aren't mocks. Each one runs on Konect4AI, a production system that turns a URL into a structured, scheduled, queryable API.

  • POST /api/v1/scraping/requests - creates a capability, behind approval
  • GET /api/v1/scraping/jobs - the tool-to-job mapping
  • POST /api/byok/ask - the answer path that keeps the data server-side
  • POST /mcp/{key} - existing MCP capabilities, turned into WebMCP tools
  • GET /generated/{id} - the rows shown to me, never to the agent

Registration handles the spec moving: document.modelContext ?? navigator.modelContext, a proper CallToolResult, and the spec's AbortSignal passed through to the backend fetch, so cancelling an agent actually cancels the work.

scripts/acceptance.sh runs 18 checks against the live deployment, including the boundary itself: the page gets the rows, the receipt says rawRowsReturnedThroughWebMCP: false, an unknown jobId is rejected with a list of what's actually available, and no credential shows up in any public response.

Challenges we ran into

An agent takes the shortest path you leave open. Twice. First it called the unbounded fetch tool instead of the ask tool. Then, blocked, it read the source URL out of a tool description and went to fetch the data itself. I removed source URLs from agent-visible descriptions and made ask-only the default. Both. fixes came from watching a real agent, not from thinking about one.

My own robots.txt check blocked one of my proposals. I approved a source and got back robots_disallowed. GitHub disallows /*/*/commits/ under User-agent: *, which covered three sources I was already using. I moved them to the public API. Consent isn't only about what an agent takes from a page. It's about what the page takes from the web.

Creating a capability costs real compute. A public URL that lets any visitor trigger paid fetching is a liability. Instead of removing it, I put a human in front of it - which turned out to be the design.

Accomplishments that we're proud of

The boundary is checkable, not just claimed. A judge doesn't have to believe the description: the receipt is on the page, and the acceptance script tests it against the live deployment.

I shipped against a moving spec and kept the claims small enough to survive it. WebMCP moved from navigator to document; A2A moved from v0.2.5 to v1.0 with renamed methods. The app supports both WebMCP locations, and the README names the exact A2A subset implemented and lists what isn't.

Every tool runs on a production system. The earthquake count is different every time, because the data is live.

What we learned

The interesting part of WebMCP isn't that a page can expose tools. It's that the page has an owner and the visiting agent doesn't. Most of the design questions come from that, and most of the answers are about holding back rather than adding capability.

What's next for Ask, Don't Take

The spec already lets a page expose tools to specific other origins (exposedTo) and discover theirs (getTools({fromOrigins})), within one page tree and behind the tools Permissions Policy. That would make cooperating sites into each other's tools, with the human still holding the session. After that: per-tool consent scopes, an exportable approval log, and revoking a tool mid-conversation so the agent watches it disappear.

Built With

Share this project:

Updates

Submission history