<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: TrickyBird</title>
    <description>The latest articles on DEV Community by TrickyBird (@trickybird).</description>
    <link>https://dev.to/trickybird</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4109317%2F1ccd3dd7-f4cc-4d31-a111-9fc4bc81803e.png</url>
      <title>DEV Community: TrickyBird</title>
      <link>https://dev.to/trickybird</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/trickybird"/>
    <language>en</language>
    <item>
      <title>WebMCP behind a web proxy: refuse first, then register one tool</title>
      <dc:creator>TrickyBird</dc:creator>
      <pubDate>Fri, 04 Sep 2026 11:51:59 +0000</pubDate>
      <link>https://dev.to/trickybird/what-a-web-proxy-has-to-change-before-it-can-register-a-webmcp-tool-435k</link>
      <guid>https://dev.to/trickybird/what-a-web-proxy-has-to-change-before-it-can-register-a-webmcp-tool-435k</guid>
      <description>&lt;p&gt;I build TrickyBird, a web proxy, so I'm not neutral here. This is a build note about the part that came before the tool: what we had to switch off first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/webmachinelearning/webmcp" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt; lets a page give the browser's agent a real function instead of a DOM to guess at. You call &lt;code&gt;document.modelContext.registerTool({ name, description, inputSchema, execute })&lt;/code&gt;, and the agent can list the tool and call it. The feature sits behind a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Permissions_Policy" rel="noopener noreferrer"&gt;Permissions Policy&lt;/a&gt; whose default is &lt;code&gt;self&lt;/code&gt;. On a normal site that default does the isolation for you: an ad frame is a different origin, so it cannot register anything into your page.&lt;/p&gt;

&lt;p&gt;A proxy breaks that default. Every frame on a proxied page comes from our origin (the page and its ads alike), so &lt;code&gt;self&lt;/code&gt; covers all of them. A frame that belongs to whatever site the reader opened could register a tool, and the agent would call it inside that reader's session. So the first WebMCP change we made was a refusal. The gateway sends &lt;code&gt;tools=()&lt;/code&gt; in the &lt;code&gt;Permissions-Policy&lt;/code&gt; header of every document it serves. With WebMCP switched on by flag, &lt;code&gt;registerTool&lt;/code&gt; on a proxied page rejects with &lt;code&gt;NotAllowedError&lt;/code&gt;, and so does a call from a fresh &lt;code&gt;about:blank&lt;/code&gt; frame the page builds itself, the frame the collapsed origin would otherwise let in. Both measured in Chrome 152. On top of the header, our injected shim deletes &lt;code&gt;document.modelContext&lt;/code&gt; from proxied pages, so on production today a site that checks for the API finds nothing, instead of an API that answers every call with a refusal. The header stays the real enforcement; a realm the shim never reaches still inherits it.&lt;/p&gt;

&lt;p&gt;Then the tool. It lives on our own home page, which serves nobody's HTML but ours. There is one, &lt;code&gt;open_site&lt;/code&gt;. It takes an address, runs the same check the page's own form runs, and the tab navigates to the proxied page. Give it a phrase that isn't a site and it refuses. It never searches.&lt;/p&gt;

&lt;p&gt;One thing I got wrong on day one, in case you ship the same code. &lt;a href="https://webmachinelearning.github.io/webmcp/" rel="noopener noreferrer"&gt;The draft&lt;/a&gt; says &lt;code&gt;registerTool&lt;/code&gt; returns a promise, and our hook was written for that: &lt;code&gt;registerTool(...).catch(() =&amp;gt; {})&lt;/code&gt;, then return the cleanup. Not every implementation agrees. On some Chrome OS and Edge builds the call returned &lt;code&gt;undefined&lt;/code&gt;, &lt;code&gt;.catch&lt;/code&gt; threw inside the effect before the cleanup existed, and the next mount of the page raised &lt;code&gt;InvalidStateError: Duplicate tool name&lt;/code&gt;. The fix: &lt;code&gt;Promise.resolve(registerTool(...)).catch(() =&amp;gt; {})&lt;/code&gt; inside a &lt;code&gt;try&lt;/code&gt;. A rejection, a synchronous throw and a bare &lt;code&gt;undefined&lt;/code&gt; now settle the same way, and the cleanup is always returned.&lt;/p&gt;

&lt;p&gt;Two things you can check yourself. &lt;a href="https://github.com/GoogleChrome/lighthouse/releases/tag/v13.4.1" rel="noopener noreferrer"&gt;Lighthouse 13.4.1&lt;/a&gt; has an agentic-browsing category. Our home page scores 1 there, the same as before the tool, and the two audits the tool adds show what they should: the registration table lists &lt;code&gt;open_site&lt;/code&gt;, and schema validity scores 1. And Chrome 152 lists &lt;code&gt;open_site&lt;/code&gt; on &lt;a href="https://trickybird.com/" rel="noopener noreferrer"&gt;trickybird.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What do I expect from it? Close to nothing, for now. The only consumer today is the ChatGPT desktop browser, and none of its user agents have turned up in our own traffic. It doesn't have to identify itself, so that may mean less than it sounds.&lt;/p&gt;

</description>
      <category>webmcp</category>
      <category>chrome</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
