WebMCP: your website's tools in any MCP client
One registry, two agents — how this site opens its chat tools to Claude Desktop, Claude Code, and Cursor.
Nikolai Townsend — production notes from this site. Stack: TYPO3 13.4, EXT:headless, Next.js 16, @jason.today/webmcp relay.
Two agents work on this site. The first one is mine: the chat assistant in the corner, running inside a TYPO3 middleware, grounded in the site content via RAG, answering in my tone. The second one belongs to the visitor: their own MCP client — Claude Desktop, Claude Code, Cursor, whatever they run. It connects to this site and calls the same tools the chat uses, but under the visitor's context, in their words, ranked by their model. Both agents meet at exactly one point: the tool registry. This article walks through how that is built — the server endpoint, the browser widget, the local relay, the browser-local tools — and which privacy properties fall out of the architecture instead of being bolted on.
The registry is the contract
The obvious way to expose tools to MCP clients would have been a second stack: hand-written tool schemas, a separate dispatcher, its own auth and tests. That creates two truths to keep aligned — every new chat tool needs a mirror, every schema change can drift, and the test surface doubles.
Instead, the registry is the contract. Every tool registered for the chat via a DI tag is automatically visible on the WebMCP endpoint as well. The endpoint is a view of the chat's tool set, not a copy of it. New tools land in one place and are immediately callable on both channels — parity by construction, not by discipline. The chat, in other words, is one of two consumers of the same tool set, not its owner.
The architecture
The path from the visitor's MCP client to a tool result crosses four stations:
- The visitor's MCP client talks to a relay — a small Node process the client spawns on the visitor's own machine.
- The relay holds a WebSocket to the browser tab showing this site. That connection never leaves localhost.
- In the tab, a widget receives the tool call and sends it as an ordinary same-origin request to the frontend's proxy route.
- The proxy forwards it to a TYPO3 PSR-15 middleware, which executes the tool from the registry and returns plain JSON.
The line that matters in this picture runs between stations two and three: everything up to the browser tab happens on the visitor's machine, started by the visitor. My server sees only regular same-origin requests coming out of the tab — indistinguishable from any other request, with no MCP framing to log and no session identity to persist.
The server side: a JSON endpoint in front of the registry
Server-side, the whole feature is one slim PSR-15 middleware in front of the registry. GET /api/chat/tools lists the tool definitions — name, description, input schema — straight from the registry. POST with {name, arguments} executes and returns the result. An unknown tool is a 404, a malformed body a 400, and over-long search queries are rejected before they can become a paid embedding call — the cost guard sits in front of the tool, not behind it. The CORS rules mirror those of the chat endpoint, and the headless frontend adds a same-origin proxy that passes GET and POST through untouched.
That is the entire server-side build: one middleware, one proxy. Everything else already existed for the chat. Two tools come out of the registry today: the vector search over the site content — the same tool the chat calls silently to ground its answers — and the appointment tool, which returns free time slots only. That booked appointments never leave the server is enforced inside the availability service, not at the endpoint, which is exactly why the tool is equally safe on every channel.
The browser side: a headless widget, UI in the chat flyout
The WebMCP widget runs headless on this site: its bundled UI is hidden, and every status change is re-dispatched as a browser event. The visible UI is a status pill in the chat flyout's toolbar — grey when disconnected, pulsing while connecting, green when connected. Clicking it opens a small card with the token input, connect and disconnect, and the current tool count.
Why inside the chat flyout? A second floating button competes with the chat button and covers content underneath. The pill sits where the conversation about AI already happens — and it stays pure UI: the connection is owned entirely by the widget, the pill only renders the status events. A small bridge module fetches the tool definitions from the endpoint when the browser is idle and registers each tool with the widget; executing a tool POSTs back through the same route.
Browser-local tools: the browser context is the session
Next to the server tools there is a second, deliberately separate class: tools that never reach the server, because what they need only exists in the browser. The chat conversation of this site lives in the visitor's localStorage — the browser context is the session. No pairing code, no login, no magic link: the structural advantage over a remote MCP server is that the identity question never comes up in the first place.
Resume the website conversation
Read-only. Hands the website chat transcript to the visitor's client — framed as quoted data, not as instructions. Whoever typed “ignore all previous instructions” into the website chat gets it back in their own client as a quote, not as a command. Prompt-injection hygiene belongs at the handover point.
Save a conversation to the website chat
Writes a transcript into the website chat — behind three gates, in order: input validation with hard caps, the chat's own privacy consent (the same gate the chat uses, because saved messages reach the server and the LLM providers on the next chat run), and a native confirmation dialog the visitor answers in the tab. An MCP client can never write silently, and a decline writes nothing — not even partially.
Open a page
The natural companion to search: find first, then show the visitor the page instead of quoting it. The navigation runs client-side, so the page transition plays exactly as it would for a normal click. A same-origin guard rejects foreign URLs — a connected client may move the visitor around this site, but can never send the tab anywhere else. A nonexistent path simply renders the 404 page, and the tool result points the client back at the search.
Prefill the contact form
Writes a draft — topic and message distilled from the conversation — and opens the contact page for review. Only empty fields are filled, nothing the visitor typed is ever overwritten, and nothing is submitted: the form does not send until the visitor has reviewed it, checked the privacy consent and clicked send themselves. The tool description doubles as the privacy policy for the consuming client: pass name or e-mail only if the visitor volunteered them in the conversation, and never ask for personal data just to fill a form.
The friction model
Which tool needs which safeguard? One rule covers all four: what is visible and reversible stays friction-free — gates exist only where data is written or sent. Navigation is visible and undone with the back button, so no dialog. The form draft stays local until the visitor submits, so no dialog. The conversation import reaches the server on the next chat run, so it takes consent plus an explicit confirmation. Formulate the rule once, and every new tool only has to be classified instead of renegotiated.
Privacy by architecture
The most interesting property of this setup: the privacy promises follow from the topology, not from policy text. The relay runs on the visitor's machine, so there is no server-side WebMCP identity that could be stored. The browser-local tools never reach the server, so server logs cannot see them even in principle. The analytics only ever record that a connection happened and which tool was called — never arguments, never contents, never the token.
What stays accountable is what costs money: the embedding call inside the search runs through the same budgeted and logged pipeline regardless of who invoked the tool. And the privacy policy of this site describes the WebMCP data flow in a dedicated section.
Where the standard stands
WebMCP as a browser standard is early: a W3C community group is working on navigator.modelContext, and Chrome ships the interface as an early preview behind a flag. That describes a capability, not adoption — which is exactly why this site runs the path that works today: a widget plus a local relay, compatible with any MCP client. When the native browser API lands, the transport changes. The registry, the tools and the friction model stay.
Try it on this site
You need an MCP client and a few minutes:
- Register the relay as an MCP server, for example in Claude Code:
claude mcp add webmcp -- npx -y @jason.today/webmcp@latest --mcp - Have your client mint a token (the relay's
webmcp_get-tokentool). - Open the chat flyout on this site, click the WebMCP pill, paste the token, connect.
- From here your client can list and call this site's tools — ask it about the site's content and let it open the best match right in your tab.
One limit of the current topology: one browser tab per site and connection.
Prefer a guided first run? There is a German step-by-step page: Erste Schritte mit WebMCP.
Klingt nach Ihrem Projekt?
Sprechen wir unverbindlich darüber.
Antwort innerhalb 24h — in der Regel deutlich schneller.