<?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: wolfejam.dev</title>
    <description>The latest articles on DEV Community by wolfejam.dev (@wolfejam).</description>
    <link>https://dev.to/wolfejam</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%2F3591934%2F481fd96a-6927-40bf-8e0d-83eb940f0d40.jpeg</url>
      <title>DEV Community: wolfejam.dev</title>
      <link>https://dev.to/wolfejam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wolfejam"/>
    <language>en</language>
    <item>
      <title>Context Over MCP, Part VI: Context You Can See</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Thu, 01 Oct 2026 13:09:12 +0000</pubDate>
      <link>https://dev.to/wolfejam/context-over-mcp-part-vi-context-you-can-see-1daa</link>
      <guid>https://dev.to/wolfejam/context-over-mcp-part-vi-context-you-can-see-1daa</guid>
      <description>&lt;p&gt;Part I asked: &lt;em&gt;Invisible AGENTS.md?&lt;/em&gt; Here's the fix.&lt;/p&gt;

&lt;p&gt;Your coding agent works from context: the project's &lt;code&gt;AGENTS.md&lt;/code&gt;, the facts&lt;br&gt;
it remembered last week, what it thinks this project is. It reads all of&lt;br&gt;
that before it writes a line. You don't see any of it. When the agent does&lt;br&gt;
something odd, you go digging: open the repo, find the file, scroll, guess&lt;br&gt;
which part it took to heart.&lt;/p&gt;

&lt;p&gt;Context is vital. But context you can't see is context you can't check.&lt;br&gt;
Cards are better when they're visible.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR — see what your AI sees. Nothing to configure.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Talk to it (goose):&lt;/strong&gt; add the &lt;code&gt;mcp-context-card&lt;/code&gt; extension, open your
project, and say &lt;em&gt;"Show me my context card."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminal:&lt;/strong&gt; in your project, run &lt;code&gt;npx mcp-context-card card&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way the full card opens in your browser. The rest of this post is&lt;br&gt;
the deeper dive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The repo:&lt;/strong&gt; &lt;a href="https://github.com/Wolfe-Jam/mcp-context-card" rel="noopener noreferrer"&gt;mcp-context-card&lt;/a&gt;: one MIT server for a project's context, memory and identity. 10 tools, 134 tests, on npm and the official MCP Registry, tested in &lt;a href="https://github.com/aaif-goose/goose" rel="noopener noreferrer"&gt;goose&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvx8yulsplmdhdyws1qc1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvx8yulsplmdhdyws1qc1.gif" alt="The context card, collapsed to one screen, then every section expanded: identity, AGENTS.md, memory and discovery" width="600" height="432"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it: the goose way
&lt;/h2&gt;

&lt;p&gt;This is the easy route. You talk to goose; goose shows you the card.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add the extension.&lt;/strong&gt; In goose Desktop: &lt;strong&gt;Extensions → Add custom
extension&lt;/strong&gt;. Type &lt;em&gt;Standard IO&lt;/em&gt;, command:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   npx &lt;span class="nt"&gt;-y&lt;/span&gt; mcp-context-card
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Nothing else. No path, no environment variable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open your project.&lt;/strong&gt; Point goose's working directory at the project
you want to see: the folder chip at the bottom of the chat.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ask:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Show me my context card.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The server's reply leads with what it did, as a plain fact: &lt;strong&gt;New file:&lt;/strong&gt;&lt;br&gt;
(or &lt;strong&gt;Updated:&lt;/strong&gt;) &lt;code&gt;context-card.html&lt;/code&gt; in your project, and the path. The&lt;br&gt;
model may put that in its own words; in the screenshot below it says the&lt;br&gt;
card was "saved to the project and opened in your browser."&lt;/p&gt;

&lt;p&gt;Then comes the card as text: the project's name and description, its&lt;br&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt; sections, the first sentence of each memory fact word for word,&lt;br&gt;
and where each one lives. At the same moment the full card opens in your&lt;br&gt;
browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqbnueb8fkzz4e1tvgwo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqbnueb8fkzz4e1tvgwo.png" alt="goose showing the card as text, with the full card opened in the&lt;br&gt;
browser" width="799" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How did it know which project? The server asks the host which folder&lt;br&gt;
you're working in, through the standard MCP &lt;em&gt;roots&lt;/em&gt; request, and reads&lt;br&gt;
that folder. In goose, that's the folder chip. Switch it, and the next card&lt;br&gt;
follows.&lt;/p&gt;

&lt;p&gt;The file it saved is yours to keep. It's a snapshot of what your agent&lt;br&gt;
reads, next to the code it describes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it: the terminal way
&lt;/h2&gt;

&lt;p&gt;No AI, no host. In your project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mcp-context-card card
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It writes &lt;code&gt;context-card.html&lt;/code&gt; and opens it in your browser. Add&lt;br&gt;
&lt;code&gt;--expanded&lt;/code&gt; to open every section, for a screenshot or a review with&lt;br&gt;
someone else.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the card shows, and why it helps
&lt;/h2&gt;

&lt;p&gt;One page, four parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity&lt;/strong&gt; — what this project says it is: name, version, status,
license, one line of description.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context&lt;/strong&gt; — your &lt;code&gt;AGENTS.md&lt;/code&gt;, every section, collapsed so the card
scans in one screen. Open any section, or all of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; — every fact the agent has been asked to remember, with a mark
on the ones that are verified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovery&lt;/strong&gt; — where each of those lives, and in what format, so a
machine can find it too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the same material your agent reads. Seen as a page, it does three&lt;br&gt;
things the files alone don't:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It shows intent.&lt;/strong&gt; The top of the card is the purpose of the project,
stated once. If that line is wrong, everything downstream is aimed wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It shows alignment.&lt;/strong&gt; Reading the card, you see what the agent will
see. A stale build command or a fact that stopped being true stands out
on a card in a way it never does in a file you haven't opened in a month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It puts you back in the loop.&lt;/strong&gt; You check the context before the agent
acts on it, not after, without digging through the repo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A thirty-second review before a big task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the one-line purpose still true?&lt;/li&gt;
&lt;li&gt;Are the build and test commands the ones you actually use?&lt;/li&gt;
&lt;li&gt;Is anything in memory out of date?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whatever's wrong on the card is wrong in what the agent reads. Fix the&lt;br&gt;
file, ask again, and the card follows. It's generated from your files every&lt;br&gt;
time, so it can't drift from them.&lt;/p&gt;
&lt;h2&gt;
  
  
  An empty project is a starting point
&lt;/h2&gt;

&lt;p&gt;Try it on a project with no &lt;code&gt;AGENTS.md&lt;/code&gt; and the card doesn't stop at&lt;br&gt;
"nothing here". Each empty section says what to do next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context:&lt;/strong&gt; &lt;em&gt;No AGENTS.md yet. Ask your agent to draft one.&lt;/em&gt; The server's
&lt;code&gt;author_agents_md&lt;/code&gt; tool builds one from the repo's real build and test
commands, with nothing invented. Outside a chat, &lt;code&gt;npx agents-md-facts&lt;/code&gt;
writes the same facts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory:&lt;/strong&gt; &lt;em&gt;No facts yet. Ask your agent to remember something, and it
lands here.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the first card shows you the gap and the next step in the same place.&lt;/p&gt;
&lt;h2&gt;
  
  
  In other hosts
&lt;/h2&gt;

&lt;p&gt;The same server works in any MCP host. For hosts that take a JSON entry&lt;br&gt;
(Claude Desktop, Cursor, and others):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"context-card"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-context-card"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Claude Code, from your project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add context-card &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; mcp-context-card
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server finds your project the same way everywhere: the host's MCP&lt;br&gt;
roots first, then the folder it was started in, if that has an&lt;br&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt;. Ask &lt;em&gt;"Which project are you reading?"&lt;/em&gt; and it tells you the&lt;br&gt;
path and how it found it. To pin one project regardless, set&lt;br&gt;
&lt;code&gt;MCP_CONTEXT_CARD_ROOT&lt;/code&gt; to its path.&lt;/p&gt;

&lt;p&gt;What you see depends on the host:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hosts that support &lt;a href="https://github.com/modelcontextprotocol/ext-apps" rel="noopener noreferrer"&gt;MCP Apps&lt;/a&gt;&lt;/strong&gt; (the MCP extension for interactive UI) show&lt;br&gt;
the full card inline, in the chat. When the host says so as it connects,&lt;br&gt;
the model gets a one-line summary instead of a page of HTML.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx38iq0z2lp87t2aizkyg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx38iq0z2lp87t2aizkyg.jpg" alt="The card rendered inline by the MCP Apps reference host" width="799" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the MCP Apps reference host drawing the card. It renders apps but&lt;br&gt;
doesn't announce that when it connects, so its Tool Result panel still&lt;br&gt;
shows the full HTML: the server can only shorten what it knows the host&lt;br&gt;
will draw.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every other host&lt;/strong&gt; gets the text card in the chat, as in the goose example&lt;br&gt;
above, and the full card in the browser. The reply also carries a link to&lt;br&gt;
the file, and its address in a code block you can copy, for hosts that won't&lt;br&gt;
open a local file link.&lt;/p&gt;

&lt;p&gt;Before this, asking for the card put about 16,500 characters of raw HTML&lt;br&gt;
into the chat. Now the reply is about 1,700, and it's something you can&lt;br&gt;
read. Ask for the full detail if you want every memory fact in full; the&lt;br&gt;
saved page always has everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep it true, with you in the loop
&lt;/h2&gt;

&lt;p&gt;A card you can see changes how you work with memory.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember that the API tests need &lt;code&gt;DATABASE_URL&lt;/code&gt; set.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;remember&lt;/code&gt; and &lt;code&gt;forget&lt;/code&gt; are marked as tools that change your project, so a&lt;br&gt;
host that respects that asks you first. In goose, that's the approval&lt;br&gt;
mode: &lt;strong&gt;Manual&lt;/strong&gt; asks before every tool, and &lt;strong&gt;Smart&lt;/strong&gt; asks before any&lt;br&gt;
tool marked as changing your project. You'll see the request and approve&lt;br&gt;
it. Then ask for the card again, and there's the fact, on the page.&lt;/p&gt;

&lt;p&gt;When a fact goes stale, you'll notice it on the card before the agent acts&lt;br&gt;
on it. Ask it to forget or correct it. Memory is a plain file in your repo,&lt;br&gt;
so it goes through code review like everything else.&lt;/p&gt;

&lt;p&gt;That's the loop: the agent reads context, you see the same context, and&lt;br&gt;
either of you can fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works, for builders
&lt;/h2&gt;

&lt;p&gt;If you run your own MCP server, the pattern is small, and it needs nothing&lt;br&gt;
beyond the standard SDK.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ask the host which project.&lt;/strong&gt; Clients that support &lt;em&gt;roots&lt;/em&gt; list the
folders the user is working in. Read the first &lt;code&gt;file://&lt;/code&gt; root, and listen
for the roots-changed notification. No config for the user to get wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declare a UI resource.&lt;/strong&gt; A &lt;code&gt;ui://&lt;/code&gt; resource with the MIME type
&lt;code&gt;text/html;profile=mcp-app&lt;/code&gt;, serving the card as one self-contained HTML
page: inline CSS, no external requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link the tool to it.&lt;/strong&gt; The tool that renders the card carries
&lt;code&gt;_meta.ui.resourceUri&lt;/code&gt; pointing at that resource. Hosts that support MCP
Apps fetch it and draw it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check what the client can do.&lt;/strong&gt; A client that supports MCP Apps can
declare it when it connects. Send those clients a one-line summary; send everyone
else what they got before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't strand the rest.&lt;/strong&gt; A second tool saves the card, opens it when the
server is local, and replies with the card as text, leading with what it
did. Server instructions tell the model to use it instead of pasting HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the text honest.&lt;/strong&gt; Each memory fact in the short view is cut at
the end of its first sentence, exactly as stored. When we cut mid-sentence
with "…", the model tidied the endings into sentences of its own. Memory
is the one thing it shouldn't reword.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is MIT: &lt;a href="https://github.com/Wolfe-Jam/mcp-context-card" rel="noopener noreferrer"&gt;mcp-context-card&lt;/a&gt;.&lt;br&gt;
To build an MCP App of your own and try it in goose, the goose docs walk&lt;br&gt;
through it: &lt;a href="https://goose-docs.ai/docs/tutorials/building-mcp-apps" rel="noopener noreferrer"&gt;Building MCP Apps for goose&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check it yourself
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add the extension to goose with just &lt;code&gt;npx -y mcp-context-card&lt;/code&gt;, open a
project, and ask &lt;em&gt;"Show me my context card."&lt;/em&gt; The card is that project's,
saved in that project's folder, and it opens in your browser.&lt;/li&gt;
&lt;li&gt;Switch goose to a different project and ask again. The card follows.&lt;/li&gt;
&lt;li&gt;In a terminal, in any project with an &lt;code&gt;AGENTS.md&lt;/code&gt;, run
&lt;code&gt;npx mcp-context-card card&lt;/code&gt;. Its sections match your file's headings.
Change a line in &lt;code&gt;AGENTS.md&lt;/code&gt;, run it again, and the card shows the change.&lt;/li&gt;
&lt;li&gt;Ask the agent to remember a fact, then ask for the card again. The fact
is on it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;(Checked 27–30 September 2026: mcp-context-card 1.3.0, installed from npm,&lt;br&gt;
in goose Desktop 1.52.0 and in the MCP Apps reference host.)&lt;/p&gt;

&lt;p&gt;Context was always there. Now you can see it, and so can everyone&lt;br&gt;
reviewing the work with you.&lt;/p&gt;

&lt;p&gt;Thanks for reading, and to everyone who's followed Context Over MCP since Part I.&lt;/p&gt;

&lt;p&gt;Tried it? Tell me in the comments what your card showed you, and what it got wrong. That's what shapes the next version.&lt;/p&gt;

&lt;p&gt;No time right now? ★ bookmark &lt;a href="https://github.com/Wolfe-Jam/mcp-context-card" rel="noopener noreferrer"&gt;the repo&lt;/a&gt; and come back before your next big task. One command, and you'll see what your agent sees.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The series, Context Over MCP:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/pulse/invisible-agentsmd-meet-visible-mcp-server-card-james-wolfe-harrison-pojbe/" rel="noopener noreferrer"&gt;Part I — Invisible AGENTS.md?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/pulse/publishing-mcp-server-official-registry-parts-nobody-wolfe-harrison-zfxve/" rel="noopener noreferrer"&gt;Part II — Publishing to the Registry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfejam/context-over-mcp-part-iii-horses-for-courses-4286"&gt;Part III — Horses for Courses&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfejam/context-over-mcp-part-iv-no-working-directory-at-all-g0f"&gt;Part IV — No Working Directory At All&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfejam/context-over-mcp-build-a-webmcp-tool-from-scratch-92e"&gt;Companion — Build a WebMCP Tool From Scratch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/wolfejam/context-over-mcp-part-v-the-form-is-the-tool-1cad"&gt;Part V — The Form Is the Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part VI — Context You Can See&lt;/strong&gt; (this post)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>goose</category>
      <category>ai</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Context Over MCP: Part V The Form Is the Tool.</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Fri, 25 Sep 2026 22:38:16 +0000</pubDate>
      <link>https://dev.to/wolfejam/context-over-mcp-part-v-the-form-is-the-tool-1cad</link>
      <guid>https://dev.to/wolfejam/context-over-mcp-part-v-the-form-is-the-tool-1cad</guid>
      <description>&lt;p&gt;Part IV showed a WebMCP page with three tools. The companion walked through&lt;br&gt;
building one yourself, the JavaScript way: &lt;code&gt;registerTool()&lt;/code&gt;, a schema, a&lt;br&gt;
handler. It mentioned the other way in one sentence and moved on.&lt;/p&gt;

&lt;p&gt;This is the other way. No &lt;code&gt;registerTool()&lt;/code&gt;, and no schema written by hand.&lt;br&gt;
It's an HTML form, with a few attributes that tell the browser it's also a tool.&lt;/p&gt;

&lt;p&gt;The playground from Part IV is now listed in the W3C Web Machine Learning&lt;br&gt;
group's &lt;a href="https://github.com/webmachinelearning/awesome-webmcp#demos" rel="noopener noreferrer"&gt;awesome-webmcp&lt;/a&gt;,&lt;br&gt;
under Demos. That page is the production version of what you're about to build.&lt;/p&gt;

&lt;p&gt;Save a blank &lt;code&gt;index.html&lt;/code&gt;, serve it with &lt;code&gt;python3 -m http.server&lt;/code&gt;, and open&lt;br&gt;
&lt;code&gt;http://localhost:8000&lt;/code&gt; in Chrome with &lt;code&gt;chrome://flags/#enable-webmcp-testing&lt;/code&gt;&lt;br&gt;
turned on. Opened straight from disk, the page has no origin and the tool won't register.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — start with a form that already works
&lt;/h2&gt;

&lt;p&gt;Before it's a tool, it's a form a person can use. This one estimates a&lt;br&gt;
shipping price. It's read-only: it calculates a number and places no order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"ship"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Weight (kg)
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"weight_kg"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;"30"&lt;/span&gt; &lt;span class="na"&gt;step=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Destination
    &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"zone"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"domestic"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Domestic&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"eu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;EU&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Rest of world&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Speed
    &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"speed"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"standard"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Standard&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"express"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Express&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Estimate&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Result: &lt;span class="nt"&gt;&amp;lt;output&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"result"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/output&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;domestic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;eu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;world&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;weight_kg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;weight_kg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_weight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;weight_kg must be between 0.1 and 30&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasOwn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RATES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_zone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zone must be domestic, eu or world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;RATES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.8&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;price_eur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ship&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`€&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_eur&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Submit it by hand. You get a price, and the URL doesn't change. This is the&lt;br&gt;
same order as the companion's Step 2: the logic is a plain function before&lt;br&gt;
anything calls it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2 — add three attributes, and it's a tool
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"ship"&lt;/span&gt;
      &lt;span class="na"&gt;toolname=&lt;/span&gt;&lt;span class="s"&gt;"estimate_shipping"&lt;/span&gt;
      &lt;span class="na"&gt;tooldescription=&lt;/span&gt;&lt;span class="s"&gt;"Estimate the shipping price for one parcel. Returns price_eur. Does not place an order."&lt;/span&gt;
      &lt;span class="na"&gt;toolautosubmit&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Weight (kg)
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"weight_kg"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;"30"&lt;/span&gt; &lt;span class="na"&gt;step=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;
           &lt;span class="na"&gt;toolparamdescription=&lt;/span&gt;&lt;span class="s"&gt;"Parcel weight in kilograms, 0.1 to 30"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Destination
    &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"zone"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="na"&gt;toolparamdescription=&lt;/span&gt;&lt;span class="s"&gt;"Destination zone: domestic, eu or world"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"domestic"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Domestic&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"eu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;EU&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Rest of world&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Speed
    &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"speed"&lt;/span&gt; &lt;span class="na"&gt;toolparamdescription=&lt;/span&gt;&lt;span class="s"&gt;"standard or express"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"standard"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Standard&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"express"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Express&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Estimate&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Result: &lt;span class="nt"&gt;&amp;lt;output&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"result"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/output&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;toolname&lt;/code&gt; names the tool.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tooldescription&lt;/code&gt; tells the agent what it does. Say what it &lt;em&gt;doesn't&lt;/em&gt; do too ("does not place an order"), so the agent doesn't have to guess.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;toolparamdescription&lt;/code&gt; on each control becomes that parameter's description.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't write the schema. The browser builds the tool's input schema from&lt;br&gt;
the form's named controls, and it reads more of the form than you might&lt;br&gt;
expect. Here's what Chrome 153 generated from the form above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"weight_kg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maximum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"multipleOf"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                   &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Parcel weight in kilograms, 0.1 to 30"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"zone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"domestic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"world"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
                   &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Destination zone: domestic, eu or world"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"speed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"standard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
                   &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"standard or express"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"weight_kg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zone"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt; and &lt;code&gt;step&lt;/code&gt; became &lt;code&gt;minimum&lt;/code&gt;, &lt;code&gt;maximum&lt;/code&gt; and &lt;code&gt;multipleOf&lt;/code&gt;. Each&lt;br&gt;
&lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; became an &lt;code&gt;enum&lt;/code&gt; (Chrome also adds each option's label as a&lt;br&gt;
&lt;code&gt;title&lt;/code&gt;, trimmed here). &lt;code&gt;required&lt;/code&gt; became &lt;code&gt;required&lt;/code&gt;. That's convenient, and&lt;br&gt;
it's also the main thing to remember in Step 5.&lt;/p&gt;

&lt;p&gt;Notice the &lt;code&gt;&amp;lt;output&amp;gt;&lt;/code&gt; has an &lt;code&gt;id&lt;/code&gt; and no &lt;code&gt;name&lt;/code&gt;. That's deliberate: it shows&lt;br&gt;
the result and isn't something the agent should fill in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3 — answer the agent, not just the screen
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;toolautosubmit&lt;/code&gt;, the agent's call fills the fields and submits the form.&lt;br&gt;
Your submit handler runs as usual. To hand the result back as the tool's&lt;br&gt;
output, call &lt;code&gt;respondWith()&lt;/code&gt; on the submit event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`€&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_eur&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;respondWith&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;preventDefault()&lt;/code&gt; first.&lt;/strong&gt; Without it, the form navigates, and the page that registered the tool goes away mid-call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The form's rules are the tool's rules.&lt;/strong&gt; Because &lt;code&gt;max&lt;/code&gt; and the &lt;code&gt;&amp;lt;option&amp;gt;&lt;/code&gt; list are in the schema, Chrome checks them before your handler runs. An agent that asks for 99 kg gets &lt;code&gt;Form validation failed: weight_kg: Value must be less than or equal to 30.&lt;/code&gt;, and one that asks for zone &lt;code&gt;"mars"&lt;/code&gt; gets &lt;code&gt;Invalid value "mars" for parameter zone&lt;/code&gt;. The checks in &lt;code&gt;estimate()&lt;/code&gt; are a second line of defense, for the day someone edits the form and forgets the function. Anything that gets past the form comes back as &lt;code&gt;{ error, message }&lt;/code&gt;, as in the companion's Step 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;typeof&lt;/code&gt; guard.&lt;/strong&gt; In a browser without WebMCP, &lt;code&gt;respondWith&lt;/code&gt; doesn't exist, the guard skips it, and the form is still a working form.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Chrome also sets &lt;code&gt;event.agentInvoked&lt;/code&gt; to true when an agent submitted the form,&lt;br&gt;
if you want to behave differently for agents. Here, the answer is the same for&lt;br&gt;
anyone who asks.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4 — decide who presses Submit
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;toolautosubmit&lt;/code&gt; is a decision, not a default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With it:&lt;/strong&gt; the agent fills the fields and the form submits. That's right for&lt;br&gt;
this tool: it's read-only and nothing happens that someone would want to review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without it:&lt;/strong&gt; the agent fills the fields, and a person has to click Submit.&lt;br&gt;
That's right for anything with consequences, and it's the reason the attribute&lt;br&gt;
is opt-in.&lt;/p&gt;

&lt;p&gt;One thing to know before you remove it: from the caller's side, that call&lt;br&gt;
stays pending until someone submits or cancels. A conventional MCP client&lt;br&gt;
talking to the page can't tell "waiting for a human" from "stuck" (see&lt;br&gt;
&lt;a href="https://github.com/webmachinelearning/webmcp/issues/307" rel="noopener noreferrer"&gt;webmcp#307&lt;/a&gt;). If a&lt;br&gt;
person needs to confirm, make the waiting visible on the page.&lt;/p&gt;

&lt;p&gt;Chrome gives you hooks for that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="nd"&gt;:tool-form-active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#00D4D4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="nd"&gt;:tool-submit-active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="m"&gt;#00D4D4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;toolactivated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;estimate_shipping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;An agent filled this form. Check it, then press Estimate.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;toolactivated&lt;/code&gt; fires when the agent has filled the fields. It fires on&lt;br&gt;
&lt;code&gt;window&lt;/code&gt;, not on the form (a listener on the form never hears it), so check&lt;br&gt;
&lt;code&gt;event.toolName&lt;/code&gt; if the page has more than one tool.&lt;/p&gt;

&lt;p&gt;If the page resets the form while the agent waits, the call ends with an&lt;br&gt;
error: &lt;code&gt;Tool execution cancelled by a form reset&lt;/code&gt;. Chrome's docs also describe&lt;br&gt;
a &lt;code&gt;toolcancel&lt;/code&gt; event; in Chrome 153 I didn't see it fire on a reset, so don't&lt;br&gt;
depend on it yet.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 5 — the fields that should never become parameters
&lt;/h2&gt;

&lt;p&gt;Step 2's convenience has a cost. Every named control becomes something an&lt;br&gt;
agent can fill in. For a shipping form, that's fine. For a checkout, login or&lt;br&gt;
identity form, it means card numbers, CVVs, one-time codes and national ID&lt;br&gt;
numbers become agent-fillable parameters, and they end up in a transcript.&lt;br&gt;
A checkout form with &lt;code&gt;name="cvv"&lt;/code&gt; becomes a tool with a &lt;code&gt;cvv&lt;/code&gt; parameter.&lt;/p&gt;

&lt;p&gt;The spec doesn't have guidance for this yet&lt;br&gt;
(&lt;a href="https://github.com/webmachinelearning/webmcp/issues/316" rel="noopener noreferrer"&gt;webmcp#316&lt;/a&gt; is open).&lt;br&gt;
Until it does, a simple rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't make a sensitive form a tool.&lt;/strong&gt; Make a smaller form that is one: "estimate", "check availability", "find a slot". Let the person handle the part with their card or code themselves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If a form must be a tool, leave sensitive controls out of it.&lt;/strong&gt; A control without a &lt;code&gt;name&lt;/code&gt; isn't submitted with the form. Do that on purpose, and say it in &lt;code&gt;tooldescription&lt;/code&gt; ("payment details are entered by the user").&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read-only tools can auto-submit. Anything else shouldn't.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The whole page
&lt;/h2&gt;

&lt;p&gt;Everything from Steps 1–4 in one file, for copying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Shipping estimate&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="nd"&gt;:tool-form-active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#00D4D4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="nd"&gt;:tool-submit-active&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="m"&gt;#00D4D4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"ship"&lt;/span&gt;
      &lt;span class="na"&gt;toolname=&lt;/span&gt;&lt;span class="s"&gt;"estimate_shipping"&lt;/span&gt;
      &lt;span class="na"&gt;tooldescription=&lt;/span&gt;&lt;span class="s"&gt;"Estimate the shipping price for one parcel. Returns price_eur. Does not place an order."&lt;/span&gt;
      &lt;span class="na"&gt;toolautosubmit&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Weight (kg)
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"weight_kg"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;"30"&lt;/span&gt; &lt;span class="na"&gt;step=&lt;/span&gt;&lt;span class="s"&gt;"0.1"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;
           &lt;span class="na"&gt;toolparamdescription=&lt;/span&gt;&lt;span class="s"&gt;"Parcel weight in kilograms, 0.1 to 30"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Destination
    &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"zone"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="na"&gt;toolparamdescription=&lt;/span&gt;&lt;span class="s"&gt;"Destination zone: domestic, eu or world"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"domestic"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Domestic&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"eu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;EU&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Rest of world&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Speed
    &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"speed"&lt;/span&gt; &lt;span class="na"&gt;toolparamdescription=&lt;/span&gt;&lt;span class="s"&gt;"standard or express"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"standard"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Standard&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"express"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Express&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Estimate&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Result: &lt;span class="nt"&gt;&amp;lt;output&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"result"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/output&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;domestic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;eu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;world&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;weight_kg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;weight_kg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_weight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;weight_kg must be between 0.1 and 30&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasOwn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RATES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_zone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zone must be domestic, eu or world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;RATES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.8&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;price_eur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ship&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`€&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_eur&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;respondWith&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;toolactivated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;estimate_shipping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;An agent filled this form. Check it, then press Estimate.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6 — verify it before you add anything else
&lt;/h2&gt;

&lt;p&gt;In Chrome with the WebMCP flag on, &lt;code&gt;document.modelContext&lt;/code&gt; also has&lt;br&gt;
&lt;code&gt;getTools()&lt;/code&gt; and &lt;code&gt;executeTool()&lt;/code&gt;, so you can play the agent from the console.&lt;br&gt;
Run these in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The tool is there, with the schema from Step 2.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTools&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
   &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tool.name&lt;/code&gt; is &lt;code&gt;estimate_shipping&lt;/code&gt;, and the schema has &lt;code&gt;minimum&lt;/code&gt;, &lt;code&gt;maximum&lt;/code&gt;, the two &lt;code&gt;enum&lt;/code&gt;s and &lt;code&gt;required: ["weight_kg", "zone"]&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A valid call returns a value, and the page stays put.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;weight_kg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns &lt;code&gt;'{"price_eur":12,"zone":"eu","speed":"standard"}'&lt;/code&gt;, the page shows €12, and the URL doesn't change. Two things to note: &lt;code&gt;executeTool&lt;/code&gt; takes the tool object from &lt;code&gt;getTools()&lt;/code&gt;, not its name, and the result comes back as a JSON string.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Invalid calls are rejected by the form.&lt;/strong&gt; Try &lt;code&gt;{ weight_kg: 99, zone: 'eu' }&lt;/code&gt; and then &lt;code&gt;{ weight_kg: 1, zone: 'mars' }&lt;/code&gt;. Both reject, with &lt;code&gt;Form validation failed: weight_kg: Value must be less than or equal to 30.&lt;/code&gt; and &lt;code&gt;Invalid value "mars" for parameter zone&lt;/code&gt;. Your handler never runs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Without autosubmit, the call waits for a person.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ship&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;removeAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;toolautosubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;weight_kg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;world&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fields fill, the outline appears, the result line says an agent filled the form, and &lt;code&gt;pending&lt;/code&gt; doesn't settle. Press Estimate: it resolves with &lt;code&gt;'{"price_eur":45.9,"zone":"world","speed":"express"}'&lt;/code&gt; and the outline goes away.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nothing leaves the tab.&lt;/strong&gt; Open DevTools → Network and repeat step 2. No requests.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;(Checked in Chrome 153 with &lt;code&gt;#enable-webmcp-testing&lt;/code&gt; on, 25 September 2026.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape, in production
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fill_6ws&lt;/code&gt; on &lt;a href="https://faf.one/webmcp" rel="noopener noreferrer"&gt;faf.one/webmcp&lt;/a&gt; is this pattern: a plain&lt;br&gt;
form with &lt;code&gt;toolname&lt;/code&gt;, &lt;code&gt;tooldescription&lt;/code&gt;, &lt;code&gt;toolautosubmit&lt;/code&gt; and a&lt;br&gt;
&lt;code&gt;toolparamdescription&lt;/code&gt; on each of its six fields. It calls&lt;br&gt;
&lt;code&gt;preventDefault()&lt;/code&gt;, answers with &lt;code&gt;respondWith({ yaml })&lt;/code&gt;, and doesn't navigate.&lt;br&gt;
It auto-submits for the same reason Step 4 gives: it builds text and changes nothing.&lt;/p&gt;

&lt;p&gt;Pick by what the page already has. If it already has a form that does the&lt;br&gt;
job, the form is the tool. If it doesn't, &lt;code&gt;registerTool()&lt;/code&gt; is there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Series: &lt;a href="https://www.linkedin.com/pulse/invisible-agentsmd-meet-visible-mcp-server-card-james-wolfe-harrison-pojbe/" rel="noopener noreferrer"&gt;Part I — Invisible AGENTS.md?&lt;/a&gt; · &lt;a href="https://www.linkedin.com/pulse/publishing-mcp-server-official-registry-parts-nobody-wolfe-harrison-zfxve/" rel="noopener noreferrer"&gt;Part II — Publishing to the Registry&lt;/a&gt; · &lt;a href="https://dev.to/wolfejam/context-over-mcp-part-iii-horses-for-courses-4286"&gt;Part III — Horses for Courses&lt;/a&gt; · &lt;a href="https://dev.to/wolfejam/context-over-mcp-part-iv-no-working-directory-at-all-g0f"&gt;Part IV — No Working Directory At All&lt;/a&gt; · &lt;a href="https://dev.to/wolfejam/context-over-mcp-build-a-webmcp-tool-from-scratch-92e"&gt;Build a WebMCP Tool From Scratch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webmcp</category>
      <category>mcp</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>"Pack of Cards: five agent-card specs, drawn as one map"</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Tue, 22 Sep 2026 15:59:18 +0000</pubDate>
      <link>https://dev.to/wolfejam/pack-of-cards-five-agent-card-specs-drawn-as-one-map-1dlb</link>
      <guid>https://dev.to/wolfejam/pack-of-cards-five-agent-card-specs-drawn-as-one-map-1dlb</guid>
      <description>&lt;p&gt;TL;DR: Machines find an agent or a server through cards — small files they read. Five seem prominent. Every spec lives in public git, so we read every version and drew them as one map, after Harry Beck's Underground diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  In Plain English
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Five cards.&lt;/strong&gt; Five specs, all in public git.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eight choices.&lt;/strong&gt; Every card picks a name field, an identity, a discovery path, a media type, an encoding, an extension point, a trust model and a versioning rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One centre.&lt;/strong&gt; The centre line is what most cards share. Distance is what only one card does.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  First, the one that is not a card
&lt;/h2&gt;

&lt;p&gt;We followed the MCP Registry closely, so start there. &lt;code&gt;server.json&lt;/code&gt; is the Registry entry — how a server gets published, rather than a card a server serves at its own door. It is not on the map, and it is the piece faf-cli writes today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;faf cards --target registry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;32,237 latest-version entries, counted page by page from the Registry API on 2026-09-15. Server Card Rev 1 borrowed its shape, which is where that line branches from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the cards
&lt;/h2&gt;

&lt;p&gt;Five. A2A Agent Card, MCP Server Card, AI Catalog, ARD and &lt;code&gt;.fafa&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A2A reached Released v1.0 and joined AAIF at Growth Stage on 2026-08-27 — the one other specs point at when they need to describe an agent and would rather not invent a card. AI Catalog is moving fastest: MCP's own MCP Catalog is evolving into it, and the spec turned over &lt;code&gt;#37&lt;/code&gt; and &lt;code&gt;#77&lt;/code&gt; five weeks apart. ARD says Proposal. &lt;code&gt;.fafa&lt;/code&gt; is ours.&lt;/p&gt;

&lt;p&gt;They overlap, they disagree, and they are all in public git. So we read every version.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to read it
&lt;/h2&gt;

&lt;p&gt;Beck's 1933 diagram dropped geography and kept order. We did the same with time.&lt;/p&gt;

&lt;p&gt;Time runs west to east. Each card is a line. Each station is a spec change, spaced by order, not by calendar. A ring marks the day two cards start or stop sharing a choice.&lt;/p&gt;

&lt;p&gt;Two views. &lt;strong&gt;Protocol ↔ Envelope&lt;/strong&gt; (the default): cards defined by one protocol sit above the centre, protocol-neutral envelopes below. &lt;strong&gt;Strict ↔ Loose&lt;/strong&gt;: each card's required fields against the median.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F83fls3ygxhxsh1iye8un.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F83fls3ygxhxsh1iye8un.png" alt="The map as a band: five card lines running west to east" width="800" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The map as a band. The &lt;a href="https://faf.one/pack-of-cards#map" rel="noopener noreferrer"&gt;live version&lt;/a&gt; is interactive — hover a station, follow a tick to the spec at that commit, switch the view.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it shows
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A2A opens alone.&lt;/strong&gt; The map starts with A2A v0.2.0 in May 2025. Nothing to compare it with until MCP Server Card's first draft, SEP-2127, on January 21, 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twins.&lt;/strong&gt; On June 25, 2026, AI Catalog merged #37 and #36. With ARD v0.9 already on &lt;code&gt;urn:air&lt;/code&gt;, the two met on the centre line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A step in.&lt;/strong&gt; On July 20, MCP Server Card moved discovery to &lt;code&gt;/.well-known/ai-catalog.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The split.&lt;/strong&gt; On August 26, ARD v0.91 took its own &lt;code&gt;ard.json&lt;/code&gt;, JSON-LD and a trust manifest. The twins parted. AI Catalog moved that day with no spec change of its own. Distance is relative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Furthest out: &lt;code&gt;.fafa&lt;/code&gt;.&lt;/strong&gt; The only card with an IANA-registered media type, and the only one written in YAML. Registering moved it one step further out. Different, not wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;30 spec versions. 269 quotes, each checked against its line at its commit. Every station links to the spec at that commit, and &lt;a href="https://faf.one/pack-of-cards/register" rel="noopener noreferrer"&gt;every value links to the line it was quoted from&lt;/a&gt; — the register is public, so any figure here can be checked.&lt;/p&gt;

&lt;p&gt;We make &lt;code&gt;.fafa&lt;/code&gt;. So the rules came first, and &lt;code&gt;.fafa&lt;/code&gt; landed where the rules put it.&lt;/p&gt;

&lt;p&gt;The choice list is a judgement. A different list draws a different map. The quotes are public, so anyone can check it, or redraw it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some interesting facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A2A was alone for 245 days.&lt;/strong&gt; The first card landed on 2025-05-21. There was nothing to compare it with until 2026-01-21.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five cards, five different extension points.&lt;/strong&gt; Of the eight choices we track, &lt;code&gt;extension&lt;/code&gt; is the only one where no two cards agree.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A2A asks for eight required fields. AI Catalog asks for three.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Catalog shipped eight versions in 141 days. A2A shipped six in 479.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Three ways to name a card
&lt;/h2&gt;

&lt;p&gt;A media type is the label a machine reads before it opens the file.&lt;/p&gt;

&lt;p&gt;None of these five specs requires one. They landed on three different answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A2A Agent Card and ARD name no media type.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI Catalog uses &lt;code&gt;application/ai-catalog+json&lt;/code&gt;. MCP Server Card uses &lt;code&gt;application/mcp-server-card+json&lt;/code&gt;.&lt;/strong&gt; Neither is registered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.fafa&lt;/code&gt; uses &lt;code&gt;application/vnd.fafa+yaml&lt;/code&gt;, registered with IANA on 2026-06-26.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every value above links to the line it was quoted from.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost us to keep up
&lt;/h2&gt;

&lt;p&gt;The map carries our own lane too, because keeping up is the hard part and we have not always managed it.&lt;/p&gt;

&lt;p&gt;AI Catalog merged &lt;code&gt;#37&lt;/code&gt; on June 25. We conformed on June 26 — &lt;strong&gt;one day&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;AI Catalog merged &lt;code&gt;#77&lt;/code&gt; on July 30, moving extensions to a map. We answered in &lt;strong&gt;three days&lt;/strong&gt; and used an array. It stayed wrong for &lt;strong&gt;forty-five&lt;/strong&gt;, and working group parsers rejected the document the whole time.&lt;/p&gt;

&lt;p&gt;Speed is not currency on its own. The check is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five, out of thirty-six
&lt;/h2&gt;

&lt;p&gt;Five cards are drawn. We checked thirty-one more against one test: a public spec with git history, describing an agent or a server so machines can find it. Eight pass, seven are borderline, sixteen are out — each listed with the reason, on the page.&lt;/p&gt;

&lt;p&gt;A card that does not make the map is not a lesser card. It is a card that answers a different question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get your pack
&lt;/h2&gt;

&lt;p&gt;Seven answers describe an agent or a server: name, short name, domain, what it does, version, where it runs, what it can do. From those, faf-cli writes the &lt;code&gt;.fafa&lt;/code&gt; and projects every card that has a place for it — correct to each spec, checked by each spec's own validator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx faf-cli@latest cards --target catalog,ard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://faf.one/pack-of-cards" rel="noopener noreferrer"&gt;The map&lt;/a&gt; · &lt;a href="https://faf.one/pack-of-cards/register" rel="noopener noreferrer"&gt;the register&lt;/a&gt; · &lt;a href="https://docs.faf.one/cards" rel="noopener noreferrer"&gt;how the cards work&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>a2a</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Context Over MCP: Build a WebMCP Tool From Scratch</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Tue, 15 Sep 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/wolfejam/context-over-mcp-build-a-webmcp-tool-from-scratch-92e</link>
      <guid>https://dev.to/wolfejam/context-over-mcp-build-a-webmcp-tool-from-scratch-92e</guid>
      <description>&lt;p&gt;Part IV showed three tools already registered, already working, on someone&lt;br&gt;
else's page. This is the version where you register one on yours — a real&lt;br&gt;
tool, with the same failure modes any WebMCP tool has, fixed the same way.&lt;/p&gt;

&lt;p&gt;No build step, no bundler, no dependency you haven't chosen yourself. Save a&lt;br&gt;
blank &lt;code&gt;index.html&lt;/code&gt;, serve it with &lt;code&gt;python3 -m http.server&lt;/code&gt;, open&lt;br&gt;
&lt;code&gt;http://localhost:8000&lt;/code&gt;, and follow along. Opened straight from disk, the page&lt;br&gt;
has no origin and the tools won't run.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — detect WebMCP, don't assume it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;document.modelContext&lt;/code&gt; exists natively in Chrome behind a flag&lt;br&gt;
(&lt;code&gt;chrome://flags/#enable-webmcp-testing&lt;/code&gt;). Everywhere else, a polyfill can&lt;br&gt;
provide the same surface. Check for the real thing first, fall back&lt;br&gt;
explicitly, and know which one you got — don't silently assume either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getModelContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelContext&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initializeWebMCPPolyfill&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://esm.sh/@mcp-b/webmcp-polyfill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;initializeWebMCPPolyfill&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelContext&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;registerTool&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;polyfill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;source&lt;/code&gt; comes back &lt;code&gt;'none'&lt;/code&gt;, stop here and fix that before writing a&lt;br&gt;
single tool — everything below assumes a real context object.&lt;/p&gt;

&lt;p&gt;Every block below goes inside the same &lt;code&gt;&amp;lt;script type="module"&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2 — write the tool as a plain function first
&lt;/h2&gt;

&lt;p&gt;Don't reach for a real YAML parser or a scoring library yet. Prove the shape&lt;br&gt;
works with something you can read in five seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scoreFacts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;goal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;who&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;what&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;why&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;present&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`^&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;s*&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;m&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;present&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;populated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;present&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;required&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;present&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five required keys, a regex check per key, a percentage. That's the whole&lt;br&gt;
scorer. It's not what a real implementation should ship — it's what lets you&lt;br&gt;
verify the &lt;em&gt;registration&lt;/em&gt; works before you go anywhere near real parsing.&lt;br&gt;
(The &lt;code&gt;\s*&lt;/code&gt; matters: a real project's facts are usually nested — &lt;code&gt;who:&lt;/code&gt;&lt;br&gt;
sitting indented under a &lt;code&gt;human_context:&lt;/code&gt; block, not at column zero. Test&lt;br&gt;
this against a flat file only and it'll look like it works; test it against&lt;br&gt;
a realistically nested one and a too-strict regex reports fields "missing"&lt;br&gt;
that are right there, just indented.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3 — register it the way errors don't crash the caller
&lt;/h2&gt;

&lt;p&gt;This is the part that's easy to get wrong: a WebMCP tool that throws is a&lt;br&gt;
tool an agent can't recover from cleanly. Return errors as data, and validate&lt;br&gt;
input before it reaches your function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerScoreTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;score_facts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Score a structured facts document 0-100 by which required fields are present.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YAML text to score&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yaml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;readOnlyHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;yaml&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;yaml&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yaml is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;scoreFacts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;score_failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things doing real work here, none of them optional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;annotations: { readOnlyHint: true }&lt;/code&gt; — tells the caller up front this
can't change anything, before it ever calls the tool.&lt;/li&gt;
&lt;li&gt;The empty-input check runs &lt;em&gt;before&lt;/em&gt; &lt;code&gt;scoreFacts&lt;/code&gt; — a missing field is a
normal case, not an exception.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;try/catch&lt;/code&gt; around your own function means a bug in &lt;code&gt;scoreFacts&lt;/code&gt;
comes back as &lt;code&gt;{ error, message }&lt;/code&gt;, not a stack trace the caller has to
parse to understand what happened.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wire the two together and open the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getModelContext&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebMCP not available:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;registerScoreTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;score_facts registered via&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4 — verify it before you add anything else
&lt;/h2&gt;

&lt;p&gt;Install the &lt;a href="https://chromewebstore.google.com/detail/model-context-tool-inspec/gbpdfapgefenggkahomfgkhfehlcenpd" rel="noopener noreferrer"&gt;Model Context Tool&lt;br&gt;
Inspector&lt;/a&gt;,&lt;br&gt;
open it on your page (the localhost URL, not the file), and check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Inspector lists score_facts
[ ] Calling it with valid YAML (containing name:/goal:/who:/what:/why:) returns a score
[ ] Calling it with { yaml: "" } returns { error: "invalid_input", ... } — not a crash
[ ] The console logs which source registered it (native or polyfill)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any of those fail, stop here. Everything past this point assumes a&lt;br&gt;
working, verified tool.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 5 — the part most tutorials skip: don't trust a URL parameter
&lt;/h2&gt;

&lt;p&gt;The moment a tool accepts a URL instead of pasted text — "fetch the YAML&lt;br&gt;
from here" — it's fetching something the &lt;em&gt;caller&lt;/em&gt; named, not something you&lt;br&gt;
chose. That's a real risk, not a hypothetical one, and it needs the same&lt;br&gt;
kind of explicit check &lt;code&gt;execute&lt;/code&gt; got in Step 3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALLOWED_HOSTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;raw.githubusercontent.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// your trusted hosts only&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertAllowedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// throws on garbage input — let it&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url must use https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_HOSTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`host not allowlisted: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp endpoints are not allowed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three checks, each closing a different door:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact hostname match&lt;/strong&gt;, not a substring check — &lt;code&gt;evil-raw.githubusercontent.com.attacker.net&lt;/code&gt;
doesn't pass just because it contains the right string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS only&lt;/strong&gt; — a tool that fetches over plain HTTP hands a
man-in-the-middle the exact document it's about to trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;mcp&lt;/code&gt; segment in the path&lt;/strong&gt; — a page tool has no business talking to an MCP
server; naming the block explicitly makes it a decision, not an accident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Call it inside &lt;code&gt;execute&lt;/code&gt;'s &lt;code&gt;try&lt;/code&gt;, before the fetch, so a rejected URL comes&lt;br&gt;
back as &lt;code&gt;{ error, message }&lt;/code&gt;. Then fetch with &lt;code&gt;redirect: 'error'&lt;/code&gt;: a redirect&lt;br&gt;
is a new request to a host your check never saw, and the browser sends it&lt;br&gt;
before your code can read &lt;code&gt;response.url&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — the same shape, in production
&lt;/h2&gt;

&lt;p&gt;This whole pattern — detect the context, register with &lt;code&gt;readOnlyHint&lt;/code&gt;,&lt;br&gt;
validate before executing, return errors as data, allowlist any fetch — is&lt;br&gt;
the shape &lt;code&gt;faf.one/webmcp&lt;/code&gt; uses for &lt;code&gt;score_faf&lt;/code&gt; and &lt;code&gt;emit_agents_md&lt;/code&gt;, with a&lt;br&gt;
WASM scoring kernel where yours has a regex. Its third tool,&lt;br&gt;
&lt;code&gt;fill_6ws&lt;/code&gt;, takes the other route: a plain HTML form with &lt;code&gt;toolname&lt;/code&gt;&lt;br&gt;
attributes, registered by the browser. That page is one real,&lt;br&gt;
production instance of it. It's not the only way to build one — it's what&lt;br&gt;
happens when you take Steps 1 through 5 and keep going.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Companion to &lt;a href="https://dev.to/wolfejam/context-over-mcp-part-iv-no-working-directory-at-all-g0f"&gt;Context Over MCP, Part IV: No Working Directory At&lt;br&gt;
All&lt;/a&gt; — the explainer that describes an already-built WebMCP page. This&lt;br&gt;
is the version where you build one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>webmcp</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Context Over MCP, Part IV: No Working Directory At All</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Mon, 14 Sep 2026 16:26:34 +0000</pubDate>
      <link>https://dev.to/wolfejam/context-over-mcp-part-iv-no-working-directory-at-all-g0f</link>
      <guid>https://dev.to/wolfejam/context-over-mcp-part-iv-no-working-directory-at-all-g0f</guid>
      <description>&lt;p&gt;Part I: an MCP client has no working directory, so it never sees your &lt;code&gt;AGENTS.md&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Part II: publish the server so a client can find it. &lt;/p&gt;

&lt;p&gt;Part III: keep facts as data and instructions as prose, so what it finds is worth seeing.&lt;/p&gt;

&lt;p&gt;Now take the working directory away completely. WebMCP's client is a browser&lt;br&gt;
tab, and a tab doesn't fail to find a working directory. It doesn't have the&lt;br&gt;
concept.&lt;/p&gt;

&lt;p&gt;So what does "expose your context" mean when there's no repo, no process, and&lt;br&gt;
no server — just a page?&lt;/p&gt;
&lt;h2&gt;
  
  
  The API is not FAF's
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;document.modelContext.registerTool()&lt;/code&gt; is Chrome's own experimental API&lt;br&gt;
(&lt;code&gt;chrome://flags/#enable-webmcp-testing&lt;/code&gt;). A page registers a tool, an agent&lt;br&gt;
inspecting that page calls it, done — no separate MCP process, no transport,&lt;br&gt;
no working directory to have or lack. Where the native API isn't available,&lt;br&gt;
&lt;code&gt;@mcp-b/webmcp-polyfill&lt;/code&gt; — a real, independent package, not FAF's — provides&lt;br&gt;
the same &lt;code&gt;registerTool&lt;/code&gt; surface as a fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;native&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getDocumentContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;native&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;native&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registerTool&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;native&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initializeWebMCPPolyfill&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mcp-b/webmcp-polyfill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;initializeWebMCPPolyfill&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;polyfilled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getDocumentContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;polyfilled&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;polyfilled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registerTool&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;polyfilled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;polyfill&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WebMCP doesn't care what you register. This is the part that matters more&lt;br&gt;
than any specific tool: the platform capability is generic, open, and not&lt;br&gt;
owned by anyone's product. What follows is one demonstration of it, not the&lt;br&gt;
only shape it can take.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three tools, two ways to register them
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;faf.one/webmcp&lt;/code&gt; registers three:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;score_faf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ score, tier, populated, active, gaps }&lt;/code&gt; — 0–100, in-browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fill_6ws&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;human_context:&lt;/code&gt; YAML fragment from a form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;emit_agents_md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;{ markdown }&lt;/code&gt; — a minimal AGENTS.md render&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those are Part III's three moves, in a tab: fill in the six W's, score what's&lt;br&gt;
still blank, and write the &lt;code&gt;AGENTS.md&lt;/code&gt; from the facts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;score_faf&lt;/code&gt; and &lt;code&gt;emit_agents_md&lt;/code&gt; are registered in script with&lt;br&gt;
&lt;code&gt;registerTool()&lt;/code&gt;, and both carry &lt;code&gt;annotations: { readOnlyHint: true }&lt;/code&gt;. That's&lt;br&gt;
not a comment. It's part of the registration, and an agent can read it before&lt;br&gt;
calling anything: the same trust signal MCP tool annotations give a client, now&lt;br&gt;
on a web page instead of a server.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fill_6ws&lt;/code&gt; is a plain HTML form with &lt;code&gt;toolname&lt;/code&gt;, &lt;code&gt;tooldescription&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;toolautosubmit&lt;/code&gt; attributes: the browser turns the form into the tool. If the&lt;br&gt;
form isn't picked up, the page registers &lt;code&gt;fill_6ws&lt;/code&gt; in script instead.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;score_faf&lt;/code&gt; runs entirely client-side. The scoring kernel is WASM&lt;br&gt;
(&lt;code&gt;faf_wasm_sdk&lt;/code&gt;), compiled once from the same Rust source the CLI and the&lt;br&gt;
edge workers use — no server round-trip, no MCP process to spin up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scoreYaml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kernel_not_ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WASM scoring kernel is not initialized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;score_faf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the result mapper refuses to pad the picture. Ignored slots — fields that&lt;br&gt;
don't apply to this project — aren't counted as missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** Score is populated/active. Ignored slots are not missing — 13/13, not 13/21. */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gapsFromSlots&lt;/code&gt; only reports what's genuinely empty. A tool that's willing to&lt;br&gt;
inflate its own denominator to look better is exactly the kind of thing a&lt;br&gt;
"structured facts" argument falls apart without.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part that's honest about its own limits
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;emit_agents_md&lt;/code&gt; renders a real AGENTS.md — Setup &amp;amp; build, Run the tests,&lt;br&gt;
Stack, 6Ws — from the same &lt;code&gt;.faf&lt;/code&gt; fields the CLI reads. But the file's own&lt;br&gt;
comment says what it isn't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Minimal renderer — not the full faf-cli AGENTS.md compiler.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It doesn't do Guardrails tiers, branch-aware Commit &amp;amp; PR, or any of the&lt;br&gt;
richer sections the real compiler produces — because a browser tab is a&lt;br&gt;
different environment with a different budget than a CLI process, and a&lt;br&gt;
renderer that pretended otherwise would be lying about what ran. The output&lt;br&gt;
says so too — a plain, one-line stamp at the foot: &lt;code&gt;compiled from&lt;br&gt;
application/vnd.faf+yaml&lt;/code&gt;. Not "compiled with faf-cli." A different tool&lt;br&gt;
made this.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fetch surface is an allowlist, not a promise
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;score_faf&lt;/code&gt; and &lt;code&gt;emit_agents_md&lt;/code&gt; can take a URL instead of pasted YAML. That&lt;br&gt;
means the page fetches something the agent asked for — and a page that&lt;br&gt;
fetches whatever a caller names is a real risk, not a hypothetical one. The&lt;br&gt;
check is explicit, and it runs before anything is requested:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertAllowedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url must use https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_HOSTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`host not allowlisted: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp endpoints are not allowed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two hosts only — &lt;code&gt;faf.one&lt;/code&gt;, &lt;code&gt;raw.githubusercontent.com&lt;/code&gt; — exact match, not a&lt;br&gt;
suffix (their own comment: &lt;em&gt;"&lt;code&gt;ide.faf.one&lt;/code&gt; is not &lt;code&gt;faf.one&lt;/code&gt;"&lt;/em&gt;). HTTPS only,&lt;br&gt;
256KB cap. And a URL with an &lt;code&gt;mcp&lt;/code&gt; segment in its path is rejected outright, by&lt;br&gt;
name — the tool doesn't just happen to avoid talking to an MCP server, it&lt;br&gt;
refuses to. A &lt;code&gt;github.com/owner/repo&lt;/code&gt; link gets rewritten to the raw file&lt;br&gt;
host before any of this runs, and &lt;code&gt;fetchAllowedYaml&lt;/code&gt; refuses redirects&lt;br&gt;
(&lt;code&gt;redirect: 'error'&lt;/code&gt;), so the only URL the browser ever requests is the one&lt;br&gt;
this check approved.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Updated 15 Sep: the page now refuses redirects. It used to re-check the final&lt;br&gt;
URL after following them, which guards the response but not the request.&lt;br&gt;
Thanks to &lt;a class="mentioned-user" href="https://dev.to/joinwell52"&gt;@joinwell52&lt;/a&gt; for the catch.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;chrome://flags/#enable-webmcp-testing&lt;/code&gt; → enable → relaunch Chrome.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;https://faf.one/webmcp&lt;/code&gt; (production needs HTTPS; &lt;code&gt;localhost&lt;/code&gt; over
plain HTTP works fine for local testing).&lt;/li&gt;
&lt;li&gt;Install the &lt;a href="https://chromewebstore.google.com/detail/model-context-tool-inspec/gbpdfapgefenggkahomfgkhfehlcenpd" rel="noopener noreferrer"&gt;Model Context Tool
Inspector&lt;/a&gt;
and open it on that tab.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Five things should be true:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Inspector lists score_faf, fill_6ws, emit_agents_md
[ ] score_faf on the loaded fixture returns a numeric score
[ ] Bad YAML returns { error, message } — not a thrown exception
[ ] The 6Ws form returns YAML and does not navigate the page
[ ] Network tab shows no call to any MCP server or /mcp URL —
    the only network activity is the one allowlisted YAML fetch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one is the actual claim of this piece, made checkable: nothing here&lt;br&gt;
talks to a server. The tab did it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Part I asked, answered somewhere Part I didn't expect
&lt;/h2&gt;

&lt;p&gt;Part I's problem was a client that can't see your &lt;code&gt;AGENTS.md&lt;/code&gt; because it has&lt;br&gt;
no way to walk to it. WebMCP's problem is stricter: there's no walking to&lt;br&gt;
anything, ever — so the only way to expose context is to hand it over&lt;br&gt;
directly, as tool calls, from wherever the process already lives. &lt;code&gt;score_faf&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;fill_6ws&lt;/code&gt;, &lt;code&gt;emit_agents_md&lt;/code&gt; are one answer to that, running in the browser&lt;br&gt;
because the browser is where this particular client already is.&lt;/p&gt;

&lt;p&gt;The API that makes it possible isn't FAF's, and doesn't have to be. Register&lt;br&gt;
whatever a page's own good judgment says an agent visiting it should be able&lt;br&gt;
to call.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Series: &lt;a href="https://www.linkedin.com/pulse/invisible-agentsmd-meet-visible-mcp-server-card-james-wolfe-harrison-pojbe/" rel="noopener noreferrer"&gt;Part I — Invisible AGENTS.md?&lt;/a&gt; · &lt;a href="https://www.linkedin.com/pulse/publishing-mcp-server-official-registry-parts-nobody-wolfe-harrison-zfxve/" rel="noopener noreferrer"&gt;Part II — Publishing to the Registry&lt;/a&gt; · &lt;a href="https://dev.to/wolfejam/context-over-mcp-part-iii-horses-for-courses-4286"&gt;Part III — Horses for Courses&lt;/a&gt; · &lt;a href="https://github.com/Wolfe-Jam/mcp-context-card" rel="noopener noreferrer"&gt;mcp-context-card&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>webmcp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Context Over MCP, Part III: Horses for Courses</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Sat, 12 Sep 2026 20:41:33 +0000</pubDate>
      <link>https://dev.to/wolfejam/context-over-mcp-part-iii-horses-for-courses-4286</link>
      <guid>https://dev.to/wolfejam/context-over-mcp-part-iii-horses-for-courses-4286</guid>
      <description>&lt;p&gt;Part I: an MCP client has no working directory, so it never sees your&lt;br&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Part II: publish the server to the registry so a client can find&lt;br&gt;
it. Now it can — and this part is about what belongs &lt;em&gt;in&lt;/em&gt; the context it finds.&lt;/p&gt;

&lt;p&gt;Horses for courses. A flat race wants the fastest horse. A course with fences&lt;br&gt;
wants one that knows how to jump. Two different horses, and you would not swap&lt;br&gt;
them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The flat racer: Markdown, for instructions
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is a flat racer. Prose, fast to write, expressive, carries&lt;br&gt;
judgment: &lt;em&gt;use &lt;code&gt;SerializerV2&lt;/code&gt; for new features; V1 is back-compat only&lt;/em&gt;. Setup,&lt;br&gt;
Build, Test, Conventions, Guardrails — how you work in this repo. The context&lt;br&gt;
card renders exactly this: sections a client can navigate, the operational&lt;br&gt;
manual an agent acts on.&lt;/p&gt;

&lt;p&gt;For that course, prose is the right horse. Nothing beats a paragraph for "here&lt;br&gt;
is the nuance, here is the exception."&lt;/p&gt;
&lt;h2&gt;
  
  
  The course has fences: the facts
&lt;/h2&gt;

&lt;p&gt;Open the card as a human and the manual is not what you want first. First you&lt;br&gt;
want the portrait: what is this, who is it for, why does it exist, where does it&lt;br&gt;
run. The &lt;strong&gt;six W's&lt;/strong&gt;. The card does not show them, because &lt;code&gt;AGENTS.md&lt;/code&gt; sections&lt;br&gt;
are a checklist, not a description.&lt;/p&gt;

&lt;p&gt;You could add an &lt;code&gt;## About&lt;/code&gt; section. People do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## About&lt;/span&gt;

mcp-context-card is a TypeScript MCP server. It runs on Node 22+, builds with
tsc, has no database (memory is a file), and deploys as a stdio process or a
stateless Streamable HTTP service. CI runs on GitHub Actions across three OSes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every clause is a fact that already lives somewhere machine-readable —&lt;br&gt;
&lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;tsconfig.json&lt;/code&gt;, the Dockerfile, &lt;code&gt;ci.yml&lt;/code&gt;. It has been copied&lt;br&gt;
into prose, and prose is where it goes to rot.&lt;/p&gt;

&lt;p&gt;I found one of these while writing this article. &lt;code&gt;mcp-context-card&lt;/code&gt;'s own&lt;br&gt;
&lt;code&gt;AGENTS.md&lt;/code&gt; said &lt;strong&gt;"Node 22 or newer."&lt;/strong&gt; Release 1.0.1 had lowered the &lt;code&gt;engines&lt;/code&gt;&lt;br&gt;
floor to &lt;code&gt;&amp;gt;=20&lt;/code&gt; — &lt;code&gt;package.json&lt;/code&gt; moved, the README followed, the &lt;code&gt;AGENTS.md&lt;/code&gt;&lt;br&gt;
prose did not. An agent reads "Node 22+" at full confidence and tells a&lt;br&gt;
contributor on Node 20 they are unsupported. Wrong, and the file was &lt;em&gt;sure&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That is a fence. So are these: the facts feed more than one reader (the agent,&lt;br&gt;
the card, a catalog); you want to &lt;em&gt;know&lt;/em&gt; which of the six W's you have not&lt;br&gt;
answered; two people edit the repo and the prose diverges. A flat racer piles&lt;br&gt;
into the first fence.&lt;/p&gt;

&lt;p&gt;This is separation of concerns — the oldest idea in the trade. One place for&lt;br&gt;
each kind of thing. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The structured layer defines. &lt;code&gt;AGENTS.md&lt;/code&gt; instructs.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The jumper: schema, for definitions
&lt;/h2&gt;

&lt;p&gt;Facts want structure. Here is &lt;code&gt;project.faf&lt;/code&gt; — YAML, one file at the repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp-context-card&lt;/span&gt;
  &lt;span class="na"&gt;main_language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TypeScript&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp&lt;/span&gt;
&lt;span class="na"&gt;stack&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Node.js&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TypeScript (tsc)&lt;/span&gt;
  &lt;span class="na"&gt;api_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MCP (JSON-RPC 2.0 — stdio + stateless Streamable HTTP)&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;            &lt;span class="c1"&gt;# memory is a file&lt;/span&gt;
  &lt;span class="na"&gt;hosting&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;any Node host — stdio local, stateless HTTP remote&lt;/span&gt;
  &lt;span class="na"&gt;cicd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub Actions — typecheck + build + test on 3 OSes&lt;/span&gt;
&lt;span class="na"&gt;human_context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;who&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MCP host and server implementers who want a project's context over MCP&lt;/span&gt;
  &lt;span class="na"&gt;what&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;the essential context / memory / identity components for MCP&lt;/span&gt;
  &lt;span class="na"&gt;why&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AGENTS.md is the standard, but a client must know the file exists and read it whole&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.com/Wolfe-Jam/mcp-context-card&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Diffable. One source. Scoreable — a completeness check tells you which W's are&lt;br&gt;
still blank. And it is still YAML: a human reads it fine. You are not trading&lt;br&gt;
readability for structure — you get both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The example here uses &lt;code&gt;.faf&lt;/code&gt;.&lt;/strong&gt; It is IANA-registered&lt;br&gt;
(&lt;code&gt;application/vnd.faf+yaml&lt;/code&gt;), it ships with the scorer, and it is what&lt;br&gt;
&lt;code&gt;mcp-context-card&lt;/code&gt; reads — so it is the instance throughout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;— or use yours.&lt;/strong&gt; The pattern is format-agnostic: a &lt;code&gt;package.json&lt;/code&gt; plus a&lt;br&gt;
short &lt;code&gt;PROJECT.yaml&lt;/code&gt;, a custom schema, whatever your toolchain already emits.&lt;br&gt;
The horse for this course is &lt;em&gt;any&lt;/em&gt; structured source. The point is facts as&lt;br&gt;
data, kept apart from the instructions.&lt;/p&gt;
&lt;h2&gt;
  
  
  Both horses, one card
&lt;/h2&gt;

&lt;p&gt;The card only ever reads &lt;code&gt;AGENTS.md&lt;/code&gt; — never &lt;code&gt;project.faf&lt;/code&gt;, on purpose. That is&lt;br&gt;
what keeps it vendor-neutral. So the facts reach it the same way any good&lt;br&gt;
manual gets its spec sheet: you &lt;em&gt;derive&lt;/em&gt; that part.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx faf-cli &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--agents&lt;/span&gt;      &lt;span class="c"&gt;# or the equivalent over MCP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;human_context&lt;/code&gt; becomes a prose section at the top — the portrait. &lt;code&gt;stack&lt;/code&gt;&lt;br&gt;
becomes a facts block. Your hand-written conventions and guardrails stay exactly&lt;br&gt;
where they were, outside the managed markers. Re-run it after a code change and&lt;br&gt;
only the facts refresh.&lt;/p&gt;

&lt;p&gt;Now the card shows the portrait &lt;strong&gt;and&lt;/strong&gt; the manual, and neither drifts.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Same "— or use yours": any tool that writes a facts section into &lt;code&gt;AGENTS.md&lt;/code&gt;&lt;br&gt;
from a structured source does this. &lt;code&gt;agents-md-facts&lt;/code&gt; does the facts layer with&lt;br&gt;
no &lt;code&gt;project.faf&lt;/code&gt; at all.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;change the code
  → re-export         facts section refreshes · project.faf re-scores
  → the card updates  it just re-reads AGENTS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manual stays true because you wrote it once and it rarely changes. The&lt;br&gt;
portrait stays true because it is derived, not typed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this series is about
&lt;/h2&gt;

&lt;p&gt;Prose for instructions, schema for facts — run the right one on each, and the&lt;br&gt;
card shows both: the six W's a human reads first, the manual an agent acts on,&lt;br&gt;
in one page, honest on their own.&lt;/p&gt;

&lt;p&gt;Build the schema layer with &lt;code&gt;.faf&lt;/code&gt; — or use yours. The point was never the&lt;br&gt;
format. It's this: a client should be able to &lt;em&gt;see&lt;/em&gt; your context, and your context&lt;br&gt;
should be worth seeing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Series: &lt;a href="https://www.linkedin.com/pulse/invisible-agentsmd-meet-visible-mcp-server-card-james-wolfe-harrison-pojbe/" rel="noopener noreferrer"&gt;Part I — Invisible AGENTS.md?&lt;/a&gt; · &lt;a href="https://www.linkedin.com/pulse/publishing-mcp-server-official-registry-parts-nobody-wolfe-harrison-zfxve/" rel="noopener noreferrer"&gt;Part II — Publishing to the Registry&lt;/a&gt; · &lt;a href="https://dev.to/wolfejam/context-over-mcp-part-iv-no-working-directory-at-all-g0f"&gt;Part IV — No Working Directory At All&lt;/a&gt; · &lt;a href="https://github.com/Wolfe-Jam/mcp-context-card" rel="noopener noreferrer"&gt;mcp-context-card&lt;/a&gt; · &lt;a href="https://faf.one/spec" rel="noopener noreferrer"&gt;the &lt;code&gt;.faf&lt;/code&gt; format&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>tutorial</category>
      <category>opensource</category>
    </item>
    <item>
      <title>FastMCP 3 4 migration: the breaking changes that compile</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Mon, 07 Sep 2026 20:31:26 +0000</pubDate>
      <link>https://dev.to/wolfejam/fastmcp-3-4-migration-the-breaking-changes-that-compile-k6p</link>
      <guid>https://dev.to/wolfejam/fastmcp-3-4-migration-the-breaking-changes-that-compile-k6p</guid>
      <description>&lt;p&gt;&lt;em&gt;FastMCP 4 is GA. If you have an MCP server or client on &lt;code&gt;fastmcp&lt;/code&gt; 3.x, you'll&lt;br&gt;
upgrade soon. Most of it is painless — &lt;code&gt;FastMCP(...)&lt;/code&gt;, &lt;code&gt;@mcp.tool&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;mcp.run(transport=...)&lt;/code&gt; are all unchanged. The parts that aren't painless are the&lt;br&gt;
parts that don't announce themselves.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;These are field notes on top of the official&lt;br&gt;
&lt;a href="https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3" rel="noopener noreferrer"&gt;Upgrading from FastMCP 3&lt;/a&gt;&lt;br&gt;
guide — the items that bit hardest when I moved one MCP server and two clients,&lt;br&gt;
in the order they bit.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  1. &lt;code&gt;pip install -U fastmcp&lt;/code&gt; can leave you half-broken
&lt;/h2&gt;

&lt;p&gt;FastMCP 4 is split into extras. The &lt;code&gt;fastmcp&lt;/code&gt; package is now a thin meta-package&lt;br&gt;
that depends on &lt;code&gt;fastmcp-slim[client,server]&lt;/code&gt;; &lt;code&gt;fastmcp-slim&lt;/code&gt; carries the actual&lt;br&gt;
code, and its extras are &lt;code&gt;client&lt;/code&gt;, &lt;code&gt;server&lt;/code&gt;, &lt;code&gt;mcp&lt;/code&gt;, &lt;code&gt;anthropic&lt;/code&gt;, &lt;code&gt;apps&lt;/code&gt;, &lt;code&gt;azure&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;code-mode&lt;/code&gt;, &lt;code&gt;gemini&lt;/code&gt;, &lt;code&gt;openai&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On a &lt;strong&gt;fresh&lt;/strong&gt; install this is invisible — &lt;code&gt;pip install fastmcp&lt;/code&gt; pulls&lt;br&gt;
&lt;code&gt;fastmcp-slim[client,server]&lt;/code&gt; and everything works.&lt;/p&gt;

&lt;p&gt;I upgraded &lt;strong&gt;in place&lt;/strong&gt; with &lt;code&gt;pip install -U fastmcp&lt;/code&gt; over &lt;code&gt;fastmcp 3.2.x&lt;/code&gt;, and pip&lt;br&gt;
did not re-resolve those base extras. Result: an importable shell with nothing in&lt;br&gt;
it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fastmcp&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fastmcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fastmcp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
&lt;span class="nb"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cannot&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Client&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fastmcp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unknown&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks exactly like a broken release. It isn't — it's the 4.x extras split not&lt;br&gt;
getting re-resolved on an in-place upgrade. (FastMCP separately documents a&lt;br&gt;
different &lt;code&gt;pip&lt;/code&gt; file-manifest issue on the 3.2 → 3.3 hop and notes &lt;code&gt;uv&lt;/code&gt; is&lt;br&gt;
unaffected by &lt;em&gt;that&lt;/em&gt; one; this is a distinct problem, and I hit it with &lt;code&gt;pip -U&lt;/code&gt; —&lt;br&gt;
I didn't test &lt;code&gt;uv pip install -U&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;The fix, either way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip uninstall &lt;span class="nt"&gt;-y&lt;/span&gt; fastmcp fastmcp-slim
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;fastmcp   &lt;span class="c"&gt;# or fastmcp==4.0.x to pin the version you tested&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or just recreate the venv. It cost me a false-alarm debugging session — twice,&lt;br&gt;
because the symptom (&lt;code&gt;ModuleNotFoundError&lt;/code&gt; on a submodule that's genuinely in the&lt;br&gt;
wheel) is so convincing.&lt;/p&gt;

&lt;p&gt;Also: &lt;code&gt;fastmcp&lt;/code&gt; in 4.x &lt;strong&gt;no longer exposes &lt;code&gt;__version__&lt;/code&gt;&lt;/strong&gt;. If you assert on it&lt;br&gt;
anywhere, switch to &lt;code&gt;importlib.metadata.version("fastmcp")&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. &lt;code&gt;httpx&lt;/code&gt; → &lt;code&gt;httpx2&lt;/code&gt;: your &lt;code&gt;except&lt;/code&gt; clauses go quiet
&lt;/h2&gt;

&lt;p&gt;FastMCP 4 dropped &lt;code&gt;httpx&lt;/code&gt; for &lt;code&gt;httpx2&lt;/code&gt; (a next-gen fork) internally. So a FastMCP&lt;br&gt;
client call that used to raise &lt;code&gt;httpx.ConnectError&lt;/code&gt; now raises&lt;br&gt;
&lt;code&gt;httpx2.ConnectError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trap: &lt;code&gt;httpx&lt;/code&gt; is still transitively installed in most environments, so this&lt;br&gt;
keeps importing and type-checking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StreamableHttpTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;do_thing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# never matches on FastMCP 4
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It just silently stops catching. Grep for &lt;code&gt;except httpx.&lt;/code&gt; and check whether each&lt;br&gt;
one wraps a FastMCP &lt;code&gt;Client&lt;/code&gt; / transport call — if it does, migrate it to&lt;br&gt;
&lt;code&gt;httpx2&lt;/code&gt; (or catch FastMCP's own &lt;code&gt;fastmcp.exceptions.ToolError&lt;/code&gt;, which is usually&lt;br&gt;
what you actually want). Your own direct &lt;code&gt;httpx&lt;/code&gt; calls are unaffected as long as&lt;br&gt;
you keep &lt;code&gt;httpx&lt;/code&gt; as a dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same silent class, elsewhere:&lt;/strong&gt; anything you hand &lt;em&gt;into&lt;/em&gt; FastMCP that's built on&lt;br&gt;
&lt;code&gt;httpx&lt;/code&gt; — a custom &lt;code&gt;httpx_client_factory&lt;/code&gt;, an &lt;code&gt;httpx.AsyncClient&lt;/code&gt; passed to a&lt;br&gt;
transport, an &lt;code&gt;httpx.Auth&lt;/code&gt; — now needs to be &lt;code&gt;httpx2&lt;/code&gt;. The official guide lists&lt;br&gt;
this right next to the &lt;code&gt;except&lt;/code&gt; trap.&lt;/p&gt;

&lt;p&gt;One more downstream effect: TLS verification now uses the OS trust store via&lt;br&gt;
&lt;code&gt;truststore&lt;/code&gt; (honouring &lt;code&gt;SSL_CERT_FILE&lt;/code&gt; / &lt;code&gt;SSL_CERT_DIR&lt;/code&gt;) instead of bundled&lt;br&gt;
&lt;code&gt;certifi&lt;/code&gt; — corporate-CA setups may verify differently. HTTP log records also move&lt;br&gt;
from &lt;code&gt;httpx&lt;/code&gt; / &lt;code&gt;httpcore.*&lt;/code&gt; to &lt;code&gt;httpx2&lt;/code&gt; / &lt;code&gt;httpcore2.*&lt;/code&gt; — update logging filters.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. &lt;code&gt;Client&lt;/code&gt; now defaults to &lt;code&gt;mode="auto"&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In 4.x, &lt;code&gt;Client(...)&lt;/code&gt; defaults to &lt;code&gt;mode="auto"&lt;/code&gt; and negotiates the modern&lt;br&gt;
&lt;code&gt;2026-07-28&lt;/code&gt; protocol era. That era is sessionless, and it changes runtime&lt;br&gt;
behaviour even though your code compiles fine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;on_initialize&lt;/code&gt; handshake&lt;/strong&gt; — middleware / init hooks tied to it never run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ctx.set_state()&lt;/code&gt; doesn't persist&lt;/strong&gt; to the next call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ctx.elicit()&lt;/code&gt; raises&lt;/strong&gt; — the modern era has no server-initiated back-channel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your client only does plain reads and writes (&lt;code&gt;call_tool&lt;/code&gt;, &lt;code&gt;read_resource&lt;/code&gt;),&lt;br&gt;
you're fine — that's the common case and it needs no change. If it relies on&lt;br&gt;
session state, an init hook, or elicitation, pin it back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;legacy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;StreamableHttpTransport&lt;/code&gt; also dropped &lt;code&gt;sse_read_timeout=&lt;/code&gt; — pass &lt;code&gt;timeout=&lt;/code&gt; on the&lt;br&gt;
&lt;code&gt;Client&lt;/code&gt; instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Removed &lt;code&gt;ctx&lt;/code&gt; methods
&lt;/h2&gt;

&lt;p&gt;These are gone and raise &lt;code&gt;AttributeError&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ctx.sample()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ctx.sample_step()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ctx.list_roots()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your server's job was to borrow the caller's model via &lt;code&gt;ctx.sample()&lt;/code&gt; (or&lt;br&gt;
&lt;code&gt;FastMCP(sampling_handler=...)&lt;/code&gt;, also removed), you either call an LLM directly&lt;br&gt;
from the server now or stay on 3.x. &lt;code&gt;ctx.elicit()&lt;/code&gt; still exists but requires a&lt;br&gt;
&lt;code&gt;response_type&lt;/code&gt; argument and raises on modern connections — rewrite it as a guard&lt;br&gt;
tool that returns an "input required" result, or branch on&lt;br&gt;
&lt;code&gt;ctx.request_context.protocol_version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Background tasks moved to an extension.&lt;/strong&gt; &lt;code&gt;@mcp.tool(task=True)&lt;/code&gt; no longer runs&lt;br&gt;
anything by itself — install &lt;code&gt;fastmcp[tasks]&lt;/code&gt; and register&lt;br&gt;
&lt;code&gt;mcp.add_extension(TasksExtension())&lt;/code&gt;, or startup raises. Drop &lt;code&gt;task=&lt;/code&gt; from&lt;br&gt;
&lt;code&gt;@mcp.resource&lt;/code&gt; / &lt;code&gt;@mcp.prompt&lt;/code&gt; (tools only).&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Version floors
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# hard requirement — resolution fails without it&lt;/span&gt;
&lt;span class="py"&gt;pydantic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.12&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="c"&gt;# only if you use the server's FastAPI extra&lt;/span&gt;
&lt;span class="py"&gt;starlette&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;"    # → FastAPI &amp;gt;= 0.133.0 (first version admitting Starlette 1.x)&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin style unchanged: an &lt;strong&gt;app&lt;/strong&gt; pins the exact version it tested&lt;br&gt;
(&lt;code&gt;fastmcp==4.0.x&lt;/code&gt;); a &lt;strong&gt;library&lt;/strong&gt; floors at &lt;code&gt;fastmcp&amp;gt;=4.0.0&lt;/code&gt; in its own&lt;br&gt;
dependencies and tests against the current release.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Import moves (quick reference)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;3.x&lt;/th&gt;
&lt;th&gt;4.x&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;from fastmcp.tools.tool import Tool, ToolResult&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;from fastmcp.tools import Tool, ToolResult&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;from fastmcp.resources.resource import Resource&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;from fastmcp.resources import Resource&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;TextContent&lt;/code&gt;, &lt;code&gt;Tool&lt;/code&gt; protocol types from &lt;code&gt;fastmcp.types&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;from mcp.types import ...&lt;/code&gt; (&lt;code&gt;fastmcp.types&lt;/code&gt; now holds only FastMCP-defined types)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcp.as_proxy(sub)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;create_proxy(sub)&lt;/code&gt; from &lt;code&gt;fastmcp.server&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcp.import_server(sub)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mcp.mount(sub)&lt;/code&gt; (live composition, not a snapshot)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcp.add_tool_transformation(name, cfg)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcp.add_transform(ToolTransform({name: cfg}))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CachableToolResult&lt;/code&gt; (old typo)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CacheableToolResult&lt;/code&gt; — no compat alias&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;McpError(ErrorData(code=..., message=...))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;McpError(code=..., message=...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SDK v2 also renamed model fields camelCase → snake_case (&lt;code&gt;inputSchema&lt;/code&gt; →&lt;br&gt;
&lt;code&gt;input_schema&lt;/code&gt;, &lt;code&gt;isError&lt;/code&gt; → &lt;code&gt;is_error&lt;/code&gt;). Old reads are auto-bridged and emit a&lt;br&gt;
&lt;code&gt;FastMCPDeprecationWarning&lt;/code&gt;. The bridge is&lt;br&gt;
&lt;code&gt;fastmcp.settings.mcp_camelcase_compat&lt;/code&gt; (env &lt;code&gt;FASTMCP_MCP_CAMELCASE_COMPAT&lt;/code&gt;),&lt;br&gt;
&lt;code&gt;bool&lt;/code&gt;, default &lt;code&gt;true&lt;/code&gt;. Set it &lt;code&gt;false&lt;/code&gt; once — that turns every remaining camelCase&lt;br&gt;
read into a hard error, so you can find and clear them before the bridge is&lt;br&gt;
removed.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. New defaults from the settings page
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://gofastmcp.com/more/settings" rel="noopener noreferrer"&gt;&lt;code&gt;gofastmcp.com/more/settings&lt;/code&gt;&lt;/a&gt; lists every&lt;br&gt;
setting — each has a &lt;code&gt;fastmcp.settings.&amp;lt;name&amp;gt;&lt;/code&gt; attribute and a &lt;code&gt;FASTMCP_&amp;lt;NAME&amp;gt;&lt;/code&gt;&lt;br&gt;
environment variable. Three defaults changed behaviour in 4.x and don't get a&lt;br&gt;
line in the upgrade guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;telemetry_mode&lt;/code&gt;&lt;/strong&gt; defaults to &lt;code&gt;"native"&lt;/code&gt; — FastMCP 4 auto-instruments
OpenTelemetry spans for MCP calls. If you don't want that,
&lt;code&gt;FASTMCP_TELEMETRY_MODE=off&lt;/code&gt; (or &lt;code&gt;propagation_only&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;check_for_updates&lt;/code&gt;&lt;/strong&gt; defaults to &lt;code&gt;"stable"&lt;/code&gt; — the CLI checks PyPI for a newer
FastMCP on startup. Set &lt;code&gt;FASTMCP_CHECK_FOR_UPDATES=off&lt;/code&gt; in CI and containers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;client_raise_first_exceptiongroup_error&lt;/code&gt;&lt;/strong&gt; defaults to &lt;code&gt;true&lt;/code&gt; — a client
error surfaces as the first underlying exception, not the &lt;code&gt;ExceptionGroup&lt;/code&gt;.
That's why &lt;code&gt;except ToolError:&lt;/code&gt; still works; if you were catching with &lt;code&gt;except*&lt;/code&gt;,
revisit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also worth a look while you're there: &lt;code&gt;stateless_http&lt;/code&gt; (new-transport-per-request,&lt;br&gt;
the sessionless/Cloud-Run knob), &lt;code&gt;http_host_origin_protection&lt;/code&gt; (new, opt-in Host/&lt;br&gt;
Origin validation for Streamable HTTP), and &lt;code&gt;mask_error_details&lt;/code&gt; (default &lt;code&gt;false&lt;/code&gt;&lt;br&gt;
— error text is passed through unless you raise an explicit &lt;code&gt;ToolError&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;ResourceError&lt;/code&gt; / &lt;code&gt;PromptError&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;FASTMCP_DEPRECATION_WARNINGS=true&lt;/code&gt; (the default) for the whole migration —&lt;br&gt;
it's how you find the rest of this list in your own code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The good news: the minimal server barely changes
&lt;/h2&gt;

&lt;p&gt;If your server is only &lt;code&gt;@mcp.tool&lt;/code&gt;-decorated functions plus&lt;br&gt;
&lt;code&gt;mcp.run(transport="stdio")&lt;/code&gt; or &lt;code&gt;mcp.run(transport="streamable-http")&lt;/code&gt;, there is&lt;br&gt;
&lt;strong&gt;no code change&lt;/strong&gt;. The constructor, the decorator, and the transport call are all&lt;br&gt;
the same. You:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;bump &lt;code&gt;pydantic&lt;/code&gt; (and FastAPI, if you use it),&lt;/li&gt;
&lt;li&gt;grep for &lt;code&gt;except httpx.&lt;/code&gt; and migrate the ones around FastMCP calls,&lt;/li&gt;
&lt;li&gt;run your tests.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I actually changed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;An MCP server — &lt;code&gt;@mcp.tool&lt;/code&gt; + &lt;code&gt;mcp.run("stdio" / "streamable-http")&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;dependency floor only — &lt;strong&gt;zero code&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two MCP clients — &lt;code&gt;Client&lt;/code&gt; + &lt;code&gt;StreamableHttpTransport&lt;/code&gt; + &lt;code&gt;except ToolError&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;dependency floor only — verified &lt;code&gt;mode="auto"&lt;/code&gt; is fine for plain reads / writes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No API changes in either. The real cost was the &lt;code&gt;pip install -U&lt;/code&gt; false alarm&lt;br&gt;
(twice) and one test that hard-coded a version string in an assertion.&lt;/p&gt;




&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ ] Recreate the venv (or `pip3 uninstall fastmcp fastmcp-slim` first) — don't `-U` over 3.x
[ ] pydantic &amp;gt;= 2.12   (+ FastAPI &amp;gt;= 0.133.0 if you use the server's FastAPI extra)
[ ] grep `except httpx.` — migrate the ones wrapping FastMCP Client/transport calls to httpx2
[ ] grep `httpx_client_factory` / `httpx.AsyncClient` / `httpx.Auth` handed to FastMCP — same, → httpx2
[ ] grep `ctx.sample` / `ctx.sample_step` / `ctx.list_roots` — removed (and `FastMCP(sampling_handler=)`)
[ ] grep `ctx.elicit` — needs response_type + fails on modern connections
[ ] grep `@mcp.tool(task=True)` — now needs fastmcp[tasks] + TasksExtension()
[ ] grep `Client(` — needs mode="legacy" only if it relies on session state / on_initialize / elicit
[ ] grep `sse_read_timeout` — moved to Client(timeout=...)
[ ] grep imports: fastmcp.tools.tool, fastmcp.resources.resource, fastmcp.types, mcp.as_proxy, import_server
[ ] grep `fastmcp.__version__` — gone; use importlib.metadata.version("fastmcp")
[ ] set `fastmcp.settings.mcp_camelcase_compat = False` once — clear the camelCase deprecation warnings
[ ] CI: `FASTMCP_CHECK_FOR_UPDATES=off`; decide on `FASTMCP_TELEMETRY_MODE` (default is `native` = OTel on)
[ ] keep `FASTMCP_DEPRECATION_WARNINGS=true` (default) for the whole migration
[ ] run the test suite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're just &lt;code&gt;@mcp.tool&lt;/code&gt; + &lt;code&gt;mcp.run&lt;/code&gt;, the whole list is "bump two floors and&lt;br&gt;
check your &lt;code&gt;httpx&lt;/code&gt; catches." Everything else is for the code that does more.&lt;/p&gt;




&lt;h2&gt;
  
  
  More reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3" rel="noopener noreferrer"&gt;Upgrading from FastMCP 3&lt;/a&gt; — the official guide this checklist rides on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gofastmcp.com/more/settings" rel="noopener noreferrer"&gt;FastMCP settings&lt;/a&gt; — every &lt;code&gt;fastmcp.settings.*&lt;/code&gt; / &lt;code&gt;FASTMCP_*&lt;/code&gt; knob, including the §7 defaults.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://faf.one/mcp" rel="noopener noreferrer"&gt;The MCP landscape&lt;/a&gt; — how MCP servers are built (the official SDKs, FastMCP) and how they run (&lt;code&gt;stdio&lt;/code&gt;, Streamable HTTP), and where the &lt;code&gt;2026-07-28&lt;/code&gt; sessionless shift sits.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>fastapi</category>
      <category>mcp</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React, Next.js, Svelte, Zod: none of them can tell AI who they're actually for</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Wed, 02 Sep 2026 15:40:01 +0000</pubDate>
      <link>https://dev.to/wolfejam/react-nextjs-svelte-zod-none-of-them-can-tell-ai-who-theyre-actually-for-47eo</link>
      <guid>https://dev.to/wolfejam/react-nextjs-svelte-zod-none-of-them-can-tell-ai-who-theyre-actually-for-47eo</guid>
      <description>&lt;h2&gt;
  
  
  Your coding agent is good at reading code.
&lt;/h2&gt;

&lt;p&gt;Point Claude Code or Cursor at a repo&lt;br&gt;
and it will figure out the language, the framework, the build command — it just costs you tokens and a few tool calls every session to re-derive what it forgot.&lt;/p&gt;

&lt;p&gt;What it &lt;em&gt;can't&lt;/em&gt; read is the part that isn't in the code: who the project is for,and why it exists. So it guesses. Confidently, in the same tone it uses for the facts it actually verified.&lt;/p&gt;

&lt;p&gt;I wanted to see how big that gap is on real projects, so I ran a mechanical context extractor over eight of the most-loved repos in the JavaScript world.&lt;/p&gt;
&lt;h2&gt;
  
  
  The method
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;faf git &amp;lt;url&amp;gt;&lt;/code&gt; clones a repo and fills in a small typed context file (&lt;code&gt;project.faf&lt;/code&gt;) from what it can find — README, &lt;code&gt;package.json&lt;/code&gt;, project structure, config. No hand-authoring, no LLM writing prose. It fills what's there and leaves the rest blank. Nine-ish slots: the identity (name, goal, language) and the six W's — who, what, why, where, when, how.&lt;/p&gt;

&lt;p&gt;Run it yourself: &lt;code&gt;npx faf-cli git https://github.com/facebook/react&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;repo&lt;/th&gt;
&lt;th&gt;extracted&lt;/th&gt;
&lt;th&gt;&lt;code&gt;who&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;why&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;facebook/react&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vercel/next.js&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expressjs/express&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;colinhacks/zod&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sveltejs/svelte&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prettier/prettier&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;td&gt;— blank —&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eight repos in total (React, Next.js, Express, Zod, Hono, Svelte, Vue, Prettier). Every one of them: &lt;strong&gt;&lt;code&gt;who is this for&lt;/code&gt; and &lt;code&gt;why does this exist&lt;/code&gt; came back empty.&lt;/strong&gt; Not one has that written anywhere a machine — or an agent at task time — can read it.&lt;/p&gt;

&lt;p&gt;Svelte scored 88%. The &lt;code&gt;who&lt;/code&gt; and &lt;code&gt;why&lt;/code&gt; are still blank. This isn't a documentation-quality problem, and it's not a knock on any of these projects. The stack lives in the files. The intent lives in maintainers' heads, design docs, old RFC threads, and Discord history — none of which your agent has open when it's editing a file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the two halves behave differently
&lt;/h2&gt;

&lt;p&gt;The scores range from 44% to 88%, and that whole spread is one thing: &lt;strong&gt;how much stack the repo exposes in config files.&lt;/strong&gt; Svelte's monorepo setup fills seven stack slots (framework, runtime, build, CI…) and lands at 88%. React's root exposes one, and lands at 56%. More config → higher score.&lt;/p&gt;

&lt;p&gt;So the &lt;em&gt;recoverable&lt;/em&gt; half — language, framework, build — varies 2× across these repos. A cold agent can dig all of it out; you just pay for the dig, every session, in tokens and latency.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;unrecoverable&lt;/em&gt; half doesn't vary at all. &lt;code&gt;who&lt;/code&gt; and &lt;code&gt;why&lt;/code&gt; came back empty in &lt;strong&gt;every one of the eight&lt;/strong&gt;, regardless of repo size, fame, or documentation.&lt;br&gt;
No amount of spelunking through &lt;code&gt;src/&lt;/code&gt; tells you that a library was built to replace one specific painful pattern, or that it targets library authors and not app developers, or that an architectural choice you're about to "clean up" was deliberate. That information was never committed in a form the agent can consume.&lt;/p&gt;

&lt;p&gt;So the agent fills the blank with a plausible story. On React it'll probably land close. On your internal service, or a library with a subtle audience, it won't — and it'll refactor accordingly, with confidence.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix is about ten minutes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;faf go&lt;/code&gt; walks a maintainer through the six questions once, writes the answers into &lt;code&gt;project.faf&lt;/code&gt;, and you commit it. From then on every agent — Claude Code, Cursor, Codex — reads the same authored context instead of guessing, and it's one file that projects out to &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;.cursor/rules/&lt;/code&gt; so&lt;br&gt;
those can't drift apart.&lt;/p&gt;

&lt;p&gt;faf-cli's own &lt;code&gt;project.faf&lt;/code&gt; looked much like these until someone sat down and filled it in. React's team could close their gap in a single commit.&lt;/p&gt;

&lt;p&gt;(The first &lt;code&gt;project.faf&lt;/code&gt; I ever wrote was for a Svelte app — which makes it a little funny that &lt;code&gt;sveltejs/svelte&lt;/code&gt; topped this table at 88% and still can't tell an agent who Svelte is for.)&lt;/p&gt;
&lt;h2&gt;
  
  
  If you want the number
&lt;/h2&gt;

&lt;p&gt;Fill the gaps first with &lt;code&gt;faf go&lt;/code&gt;, then &lt;code&gt;faf bench&lt;/code&gt; measures the delta directly: it asks a model the questions about your repo cold, then again after reading &lt;code&gt;project.faf&lt;/code&gt;, and grades the answers mechanically (no LLM judge). Questions are derived from the &lt;em&gt;populated&lt;/em&gt; slots — the &lt;code&gt;.faf&lt;/code&gt; is the answer key — so a repo that still has &lt;code&gt;who&lt;/code&gt;/&lt;code&gt;why&lt;/code&gt; blank can't be graded on them; that's what &lt;code&gt;faf go&lt;/code&gt;is for. The delta is the accuracy your agent is leaving on the table, and where the cold misses cluster tells you which parts of your project only exist in your head.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx faf-cli go                     &lt;span class="c"&gt;# fill project.faf (the ten minutes)&lt;/span&gt;
npx faf-cli bench questions        &lt;span class="c"&gt;# the questions, derived from your repo&lt;/span&gt;
npx faf-cli bench grade answers.json &lt;span class="nt"&gt;--cold&lt;/span&gt;
npx faf-cli bench grade answers.json &lt;span class="nt"&gt;--faf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A low cold score isn't a verdict on the model. It's a diagnosis that the project is under-described for an agent to work in — and the output ends in &lt;code&gt;faf go&lt;/code&gt;, not a leaderboard.&lt;/p&gt;




&lt;p&gt;Notes: &lt;code&gt;faf git&lt;/code&gt;'s percentage reflects how much is &lt;em&gt;in the repo&lt;/em&gt;, so a high score means good docs, not a good tool — and the &lt;code&gt;who&lt;/code&gt;/&lt;code&gt;why&lt;/code&gt; blanks are honest, the information genuinely isn't there to extract. &lt;code&gt;project.faf&lt;/code&gt; is one typed source (the &lt;code&gt;.faf&lt;/code&gt; format is an IANA-registered media type,&lt;code&gt;application/vnd.faf+yaml&lt;/code&gt;) that the per-tool context files are generated from, rather than another markdown file to maintain by hand.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>claude</category>
      <category>cursor</category>
    </item>
    <item>
      <title>Your tools/list is stamped. That is not the same as cached.</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Thu, 20 Aug 2026 11:00:00 +0000</pubDate>
      <link>https://dev.to/wolfejam/your-toolslist-is-stamped-that-is-not-the-same-as-cached-2j0f</link>
      <guid>https://dev.to/wolfejam/your-toolslist-is-stamped-that-is-not-the-same-as-cached-2j0f</guid>
      <description>&lt;p&gt;A stamp on &lt;code&gt;tools/list&lt;/code&gt; is a claim about later reuse. Presence is not proof the next call is cacheable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — The 7/28 spec put &lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt; on &lt;code&gt;tools/list&lt;/code&gt;. Most write-ups treat that stamp as a cache. It is a &lt;strong&gt;claim about later reuse&lt;/strong&gt;. Presence means the server &lt;em&gt;said&lt;/em&gt; something. Truth needs an observation that can fail. We shipped a liar and a probe. The catalog grew. The probe still said OK. The stamp half was along for the ride. One mutant per clause. Name which negatives are still live.&lt;/p&gt;




&lt;h2&gt;
  
  
  The victory lap
&lt;/h2&gt;

&lt;p&gt;The 2026-07-28 spec added list-cache stamps (SEP-2549), modeled on HTTP &lt;code&gt;Cache-Control&lt;/code&gt;. The official sentence is some version of: &lt;em&gt;clients know exactly how long &lt;code&gt;tools/list&lt;/code&gt; is fresh.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That sentence is doing a lot of work.&lt;/p&gt;

&lt;p&gt;HTTP already taught this. &lt;code&gt;Cache-Control: max-age=60&lt;/code&gt; is a statement. It is not proof the next GET returns the same bytes. MCP imported the words. It did not import a test.&lt;/p&gt;

&lt;p&gt;A server can print &lt;code&gt;ttlMs: 60000, cacheScope: Public&lt;/code&gt; and change its catalog on the next call. The stamp is still well-formed. A probe that only checks &lt;em&gt;presence&lt;/em&gt; will still pass.&lt;/p&gt;




&lt;h2&gt;
  
  
  We built a liar. Then the lie got lazy.
&lt;/h2&gt;

&lt;p&gt;Last piece: a labeled companion, &lt;code&gt;mcp-worse&lt;/code&gt;, and one command — &lt;code&gt;contrast-smoke&lt;/code&gt; — that passes only if the good server meets the BETTER list contract &lt;strong&gt;and&lt;/strong&gt; the bad one fails it.&lt;/p&gt;

&lt;p&gt;The checker looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_lying_surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ListProbe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;unstamped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.ttl_ms&lt;/span&gt;&lt;span class="nf"&gt;.is_none&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.cache_scope&lt;/span&gt;&lt;span class="nf"&gt;.is_none&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;wrong_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.names&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;better_names&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;unstamped&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;wrong_order&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;OR&lt;/code&gt; is the hole.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mcp-better&lt;/code&gt; grew a third tool, &lt;code&gt;confirm_echo&lt;/code&gt;. &lt;code&gt;mcp-worse&lt;/code&gt; still lists two: &lt;code&gt;echo&lt;/code&gt;, &lt;code&gt;health&lt;/code&gt;. So &lt;code&gt;wrong_order&lt;/code&gt; (really: names ≠ the good catalog) is &lt;strong&gt;always true&lt;/strong&gt;. The stamp half stopped carrying observable weight.&lt;/p&gt;

&lt;p&gt;If worse grew &lt;code&gt;with_ttl_ms&lt;/code&gt; and &lt;code&gt;with_cache_scope&lt;/code&gt; tomorrow, the example would still print OK. It would fail for contents only. The negative case for &lt;code&gt;ttlMs&lt;/code&gt; would be gone, and nothing would say so.&lt;/p&gt;

&lt;p&gt;The “companion must stay a reliable liar” guard only fires when &lt;strong&gt;every&lt;/strong&gt; clause goes green at once. Partial decay is invisible.&lt;/p&gt;

&lt;p&gt;A comment on that post named it. They were right.&lt;/p&gt;

&lt;p&gt;A liar that fails for two reasons is weaker evidence than two liars that each fail for one. Steal-the-pattern already said: one smallest lie per claim. We had one binary that violated every clause, and an &lt;code&gt;OR&lt;/code&gt; that hid which ones were still live.&lt;/p&gt;




&lt;h2&gt;
  
  
  Presence is not truth
&lt;/h2&gt;

&lt;p&gt;Order is a property of the list the probe is &lt;strong&gt;holding&lt;/strong&gt;. You can see it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt; are statements about how that list &lt;strong&gt;may be reused later&lt;/strong&gt;. Confirming the fields exist and look well-formed proves the server made a claim. It does not prove a client would be right to skip the next &lt;code&gt;tools/list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;order-restart-smoke&lt;/code&gt; does the right thing for &lt;strong&gt;order&lt;/strong&gt;: two processes, same names. It applies that shape to TTL as &lt;code&gt;ttl_a == ttl_b&lt;/code&gt; — the stamp is restart-stable. That is a property of the &lt;strong&gt;number&lt;/strong&gt;, not of the caching behavior the number describes.&lt;/p&gt;

&lt;p&gt;Falsifying a TTL claim takes an observation pair that straddles a change. Our catalog is compiled in. The TTL claim &lt;strong&gt;cannot&lt;/strong&gt; be violated yet. Declared, not falsified. Once the catalog goes dynamic, &lt;code&gt;ttlMs&lt;/code&gt; is the first stamp with room to lie — and it is the clause with the least behind it.&lt;/p&gt;

&lt;p&gt;This lab reads &lt;strong&gt;this&lt;/strong&gt; list. Not the next call. Not a client cache.&lt;/p&gt;




&lt;h2&gt;
  
  
  Run the audit
&lt;/h2&gt;

&lt;p&gt;The probe now requires &lt;strong&gt;each&lt;/strong&gt; teaching clause on the companion. Stamp decay fails closed and names the clause.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// examples/contrast_smoke.rs — current tree&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_unstamped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ListProbe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.ttl_ms&lt;/span&gt;&lt;span class="nf"&gt;.is_none&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.cache_scope&lt;/span&gt;&lt;span class="nf"&gt;.is_none&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;wrong_names&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ListProbe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.names&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;better_names&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both must stay true on &lt;code&gt;mcp-worse&lt;/code&gt;. If either goes green, the example exits non-zero and says which.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Clone and build both binaries
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Wolfe-Jam/mcp-better.git
&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-better
cargo build &lt;span class="nt"&gt;--bins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;strong&gt;tree&lt;/strong&gt;, not &lt;code&gt;cargo install mcp-better&lt;/code&gt;. The published &lt;code&gt;v0.5.0&lt;/code&gt; tag still has the &lt;code&gt;OR&lt;/code&gt;. The named-clause OK line is the honesty cut on current &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Run contrast-smoke
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo run &lt;span class="nt"&gt;--example&lt;/span&gt; contrast-smoke
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect (captured 2026-08-19, current tree):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;better names=["health", "echo", "confirm_echo"] ttl=Some(60000) scope=Some(Public)
worse  names=["echo", "health"]                 ttl=None        scope=None
contrast-smoke: OK (better contract · worse unstamped · worse names≠health,echo,confirm_echo)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OK line is the point. It names which negatives are still live.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — What you just proved
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;th&gt;Not proved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Good list is ordered + stamped&lt;/td&gt;
&lt;td&gt;Wire: three names, &lt;code&gt;ttlMs &amp;gt; 0&lt;/code&gt;, &lt;code&gt;cacheScope == Public&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;That a client should cache it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Companion stays unstamped&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;unstamped&lt;/code&gt; asserted, not &lt;code&gt;OR&lt;/code&gt;-ed away&lt;/td&gt;
&lt;td&gt;Cache behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Companion catalog ≠ better&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;names ≠ health,echo,confirm_echo&lt;/code&gt; (today: two tools, reversed)&lt;/td&gt;
&lt;td&gt;That “wrong names” is &lt;em&gt;only&lt;/em&gt; order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial decay is visible&lt;/td&gt;
&lt;td&gt;Stamp clause going green fails the example&lt;/td&gt;
&lt;td&gt;A second mutant (stamped-but-reversed)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last empty cell is honest. One companion that fails for two reasons is still weaker than two mutants that each fail for one. This cut makes the live negatives &lt;strong&gt;named&lt;/strong&gt;. It does not ship a second dummy.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Not a cache test. No observation pair straddling a change. The catalog is compiled in.&lt;/li&gt;
&lt;li&gt;Not a rewrite of the last post. That one built the liar. This one asks what the probe actually falsified.&lt;/li&gt;
&lt;li&gt;Not a version diary. No new tool. No new crate.&lt;/li&gt;
&lt;li&gt;Not “every BETTER server must implement a mutant factory.”&lt;/li&gt;
&lt;li&gt;Not a security scanner.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Steal the pattern
&lt;/h2&gt;

&lt;p&gt;Same shape as last time — finish the last step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write down every claim the docs make about the &lt;strong&gt;wire&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;For each claim, the smallest change that would make it false.&lt;/li&gt;
&lt;li&gt;One mutant per claim — or, at minimum, &lt;strong&gt;assert each clause&lt;/strong&gt; so one leftover violation cannot hide another.&lt;/li&gt;
&lt;li&gt;Print which negatives are still live. An OK line that does not name them is an &lt;code&gt;OR&lt;/code&gt; in disguise.&lt;/li&gt;
&lt;li&gt;If the claim is about &lt;strong&gt;later reuse&lt;/strong&gt; (&lt;code&gt;ttlMs&lt;/code&gt;, &lt;code&gt;cacheScope&lt;/code&gt;), a presence check is “a claim was made.” Do not call it verified until something can fail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you cannot say which clause is still live, you do not have a negative case. You have a dummy that is wrong in a pile.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Last post: &lt;a href="https://dev.to/wolfejam/i-built-a-lying-mcp-server-on-purpose-heres-how-you-catch-it-102g"&gt;I built a lying MCP server on purpose&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Era map: &lt;a href="https://dev.to/wolfejam/not-all-mcp-servers-are-equal-what-728-just-made-official-2f29"&gt;Not all MCP servers are equal — what 7/28 just made official&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Spec: &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28" rel="noopener noreferrer"&gt;MCP 2026-07-28&lt;/a&gt; · list cache stamps (SEP-2549)&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/Wolfe-Jam/mcp-better" rel="noopener noreferrer"&gt;github.com/Wolfe-Jam/mcp-better&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Close
&lt;/h2&gt;

&lt;p&gt;A README cannot lie to a test that reads the wire. A stamp can still lie to a README.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ttlMs&lt;/code&gt; on the list is a reuse claim. &lt;code&gt;contrast-smoke&lt;/code&gt; now says which teaching clauses are still live. That is the list. Not the cache.&lt;/p&gt;

&lt;p&gt;Claim = wire. Ask what you actually falsified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which clause on your &lt;code&gt;tools/list&lt;/code&gt; is only present — not proven?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm an AAIF Ambassador. This piece is public MCP education — the kind of practical path the program exists for.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>rust</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>"Never guessed" isn't the same as "never misses."</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Thu, 20 Aug 2026 00:27:17 +0000</pubDate>
      <link>https://dev.to/wolfejam/never-guessed-isnt-the-same-as-never-misses-1okp</link>
      <guid>https://dev.to/wolfejam/never-guessed-isnt-the-same-as-never-misses-1okp</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — I built a tool that authors AGENTS.md from real repo facts, never invented ones. A month after it was scored as a real AAIF contribution: 29 downloads, 2 stars, 0 issues. Nobody stress-tested it, so I did — against two repos I'd already dug into by hand. Every line it wrote was true. It still missed the most important test in both of them, for the same structural reason, twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest starting number
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;agents-md-facts&lt;/code&gt; authors a minimal AGENTS.md from what your repo actually declares — real build commands, real test commands, real file layout. Nothing guessed. It was submitted to AAIF as a project contribution and scored accordingly.&lt;/p&gt;

&lt;p&gt;A month later: 29 npm downloads, 2 GitHub stars, 0 issues, 0 forks. Five commits since launch, all docs and housekeeping — nobody used it hard enough to find something to fix.&lt;/p&gt;

&lt;p&gt;That's not a failure post. It's the honest premise for this one: if nobody else was going to stress-test it, I would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dogfooding it for real
&lt;/h2&gt;

&lt;p&gt;I ran it, &lt;code&gt;--dry-run&lt;/code&gt;, against two repos I already understood deeply — not blind spots, ground truth I could check the output against. One Rust, one TypeScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-better &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx agents-md-facts &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Run the tests&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;cargo &lt;span class="nb"&gt;test
&lt;/span&gt;cargo clippy
&lt;span class="p"&gt;```&lt;/span&gt;

&lt;span class="gu"&gt;## Definition of Done&lt;/span&gt;

Done when: &lt;span class="sb"&gt;`cargo clippy`&lt;/span&gt; exits 0 · &lt;span class="sb"&gt;`cargo test`&lt;/span&gt; passes · committed with a clear message.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;claude-faf-mcp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx agents-md-facts &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Run the tests&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;bash
&lt;/span&gt;npm run &lt;span class="nb"&gt;test
&lt;/span&gt;npm run lint
&lt;span class="p"&gt;```&lt;/span&gt;

&lt;span class="gu"&gt;## Definition of Done&lt;/span&gt;

Done when: &lt;span class="sb"&gt;`npm run lint`&lt;/span&gt; exits 0 · &lt;span class="sb"&gt;`npm run test`&lt;/span&gt; passes · committed with a clear message.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it got right
&lt;/h2&gt;

&lt;p&gt;Every single line in both outputs traces to something real. &lt;code&gt;cargo test&lt;/code&gt; and &lt;code&gt;cargo clippy&lt;/code&gt; genuinely exist and genuinely run in &lt;code&gt;mcp-better&lt;/code&gt;. &lt;code&gt;npm run test&lt;/code&gt; and &lt;code&gt;npm run lint&lt;/code&gt; genuinely exist and genuinely run in &lt;code&gt;claude-faf-mcp&lt;/code&gt;. Zero invented commands, either time. The tool's actual promise — never guessed — held completely, both times, under real conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it missed — twice, same shape
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;mcp-better&lt;/code&gt; ships a tool called &lt;code&gt;confirm_echo&lt;/code&gt; with a real multi-round contract: a sealed, tamper-checked handshake. The unit tests for it live in &lt;code&gt;src/server.rs&lt;/code&gt; and run fine under &lt;code&gt;cargo test&lt;/code&gt;. But the &lt;em&gt;contract&lt;/em&gt; — does the tool still promise what it promised, over the wire, to a real client — is checked by a completely separate command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cargo &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--all-targets&lt;/span&gt;
&lt;span class="go"&gt;   Running unittests examples/mrtr_client.rs
running 0 tests
&lt;/span&gt;&lt;span class="gp"&gt;test result: ok. 0 passed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;0 failed&lt;span class="p"&gt;;&lt;/span&gt; 0 ignored
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cargo test&lt;/code&gt; compiles that file. It runs zero test functions in it. The actual check only happens if you separately run &lt;code&gt;cargo run --example mrtr-client&lt;/code&gt; — which is exactly the command the generated AGENTS.md never mentions, because "Definition of Done: &lt;code&gt;cargo test&lt;/code&gt; passes" is what the tool detected, and that line is &lt;em&gt;true&lt;/em&gt;. It's just not the whole truth.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;claude-faf-mcp&lt;/code&gt; has the same shape from a different angle. The generated file correctly points at &lt;code&gt;npm run test&lt;/code&gt;. Sitting in the same &lt;code&gt;tests/&lt;/code&gt; directory, untouched by the generated output: &lt;code&gt;WJTTC-FAFM-MEMORY.md&lt;/code&gt;, &lt;code&gt;WJTTC-MCP-CLI-CONTINUITY-v274.md&lt;/code&gt;, &lt;code&gt;WJTTC-REPORT-MCP-v120-STRESS-TEST.md&lt;/code&gt; — a separate certification layer the tool has no way to surface, for the same reason: it doesn't look like &lt;code&gt;npm run test&lt;/code&gt;, so there's nothing for a facts-detector to grab onto.&lt;/p&gt;

&lt;p&gt;Two different stacks. Same blind spot, in the same place: the deepest verification layer in each repo is precisely the one that doesn't look like every other repo's test command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why — and it isn't a bug
&lt;/h2&gt;

&lt;p&gt;"Never guessed" means the tool only writes down things it can detect. That's the entire safety property, and it's a real one — it's the whole reason this class of tool doesn't fall into the auto-generated-bloat trap the research on AI-written instruction files warns about. But detection has a shape. The tool knows what &lt;code&gt;cargo test&lt;/code&gt; and &lt;code&gt;npm run test&lt;/code&gt; look like. It has no way to know that &lt;code&gt;cargo run --example mrtr-client&lt;/code&gt; or a hand-written stress report &lt;em&gt;also&lt;/em&gt; answers "is this thing actually verified" — because those don't match any pattern it's built to recognize.&lt;/p&gt;

&lt;p&gt;"Real fact" and "fact my detector recognizes" are not the same set. The gap between them is exactly where the most important line in an AGENTS.md can go missing, silently, while every other line stays true.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means beyond this one tool
&lt;/h2&gt;

&lt;p&gt;Any facts-based generator — this one or the next one — inherits the same limit. A generated AGENTS.md is a floor, not a ceiling: provably not-wrong, because every line traces to something real, which is not the same claim as complete. And the layer most likely to be missing is usually the one that matters most, because a repo's &lt;em&gt;deepest&lt;/em&gt; verification is often exactly the thing that was built custom, in a shape nothing else in that codebase uses — which is precisely what a pattern-matcher is worst at finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm changing because of this
&lt;/h2&gt;

&lt;p&gt;The fact-sourcing discipline underneath all of this — every line traces to a fact, verify it stays true in the same PR it changed — is still the right foundation. It just isn't sufficient alone, and pretending otherwise would be exactly the kind of overclaim this series has spent five parts arguing against.&lt;/p&gt;

&lt;p&gt;The honest addition is one more pass, after the tool runs, human or agent: &lt;em&gt;does this repo have a second, differently-shaped verification layer the tool wouldn't have found?&lt;/em&gt; That's the question that surfaced &lt;code&gt;mrtr-client&lt;/code&gt; and the WJTTC reports here. It's not automatable yet. Naming it is the first step toward making it so.&lt;/p&gt;




&lt;p&gt;Help guide what we build — comments and suggestions welcome.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>devtools</category>
      <category>testing</category>
    </item>
    <item>
      <title>"I built a lying MCP server on purpose — here's how you catch it"</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Mon, 17 Aug 2026 05:06:18 +0000</pubDate>
      <link>https://dev.to/wolfejam/i-built-a-lying-mcp-server-on-purpose-heres-how-you-catch-it-102g</link>
      <guid>https://dev.to/wolfejam/i-built-a-lying-mcp-server-on-purpose-heres-how-you-catch-it-102g</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — A server's README can say anything. Its &lt;code&gt;tools/list&lt;/code&gt; response either backs that up or it doesn't.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;code&gt;mcp-worse&lt;/code&gt;&lt;/strong&gt; — a second binary, sharing two of &lt;code&gt;mcp-better&lt;/code&gt;'s tool names, that deliberately omits the list-cache stamps and serves tools in the wrong order — so a test could prove the difference. That test is &lt;strong&gt;&lt;code&gt;contrast-smoke&lt;/code&gt;&lt;/strong&gt;: one command, real MCP clients, real wire traffic. Exit code 0 only if the good server passes the contract &lt;em&gt;and&lt;/em&gt; the bad one fails it.&lt;/p&gt;

&lt;p&gt;This is what "claim = wire" looks like when you stop saying it and start shipping it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with trusting a README
&lt;/h2&gt;

&lt;p&gt;Every MCP server's docs make claims: &lt;em&gt;stateless&lt;/em&gt;, &lt;em&gt;cacheable list&lt;/em&gt;, &lt;em&gt;stable tool order&lt;/em&gt;. Nothing in the protocol stops a server from claiming all three and doing none of them. The client can't tell from the tool &lt;strong&gt;names&lt;/strong&gt; — &lt;code&gt;health&lt;/code&gt; and &lt;code&gt;echo&lt;/code&gt; look identical whether the server behind them is honest or not.&lt;/p&gt;

&lt;p&gt;So the question isn't "does this server have a &lt;code&gt;tools/list&lt;/code&gt; endpoint." It's: &lt;strong&gt;if the docs are wrong, what breaks, and when?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most servers never answer that, because nothing is &lt;em&gt;built to fail&lt;/em&gt; on purpose. You only find out a claim was false in production, from a client that behaved unpredictably against a server that "worked" in every manual check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the lie on purpose
&lt;/h2&gt;

&lt;p&gt;The cleanest way to test a contract-checker is to hand it something that violates the contract — not a hypothetical, a real binary.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mcp-worse&lt;/code&gt; is that binary. Same protocol version on the wire, same transport, and it mirrors two of &lt;code&gt;mcp-better&lt;/code&gt;'s tools by name (&lt;code&gt;health&lt;/code&gt;, &lt;code&gt;echo&lt;/code&gt;) — &lt;code&gt;mcp-better&lt;/code&gt; has since grown a third (&lt;code&gt;confirm_echo&lt;/code&gt;, an MRTR retry-flow demo) that the lying companion was never updated to match, so the tool &lt;em&gt;count&lt;/em&gt; alone is now part of the gap too, alongside two deliberate breaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/worse.rs&lt;/span&gt;
&lt;span class="cd"&gt;/// Intentional anti-order (BETTER is health → echo).&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;WORSE_TOOL_ORDER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"echo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"health"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="cd"&gt;/// Unstamped list with reversed order — the lie.&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;lying_list_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ListToolsResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.tool_router&lt;/span&gt;&lt;span class="nf"&gt;.list_all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="nf"&gt;.sort_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ...WORSE_TOOL_ORDER... */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Deliberately omit with_ttl_ms / with_cache_scope.&lt;/span&gt;
    &lt;span class="nn"&gt;ListToolsResult&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_all_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;ttlMs&lt;/code&gt;. No &lt;code&gt;cacheScope&lt;/code&gt;. Tools reversed. The &lt;code&gt;health&lt;/code&gt; tool result even says so out loud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-worse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.4.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-28"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LYING-DEMO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This binary deliberately fails the BETTER list contract for teaching."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not a trick client would fall for in the wild — it's labeled, it's teaching-only, it never ships to a registry. Its only job is to be &lt;strong&gt;wrong on purpose, reliably&lt;/strong&gt;, so something else can prove it catches a lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the audit
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;contrast-smoke&lt;/code&gt; spawns both binaries as actual child processes, talks real MCP over stdio, and checks the wire — not the source, not the docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// examples/contrast_smoke.rs&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_better_contract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ListProbe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.names&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;better_names&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nd"&gt;matches!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.ttl_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.cache_scope&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CacheScope&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Public&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_lying_surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ListProbe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;unstamped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.ttl_ms&lt;/span&gt;&lt;span class="nf"&gt;.is_none&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.cache_scope&lt;/span&gt;&lt;span class="nf"&gt;.is_none&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;wrong_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="py"&gt;.names&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;better_names&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;unstamped&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;wrong_order&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1 — Clone and build both binaries
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Wolfe-Jam/mcp-better.git
&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-better
cargo build &lt;span class="nt"&gt;--bins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This builds &lt;code&gt;mcp-better&lt;/code&gt; and &lt;code&gt;mcp-worse&lt;/code&gt; side by side — &lt;code&gt;contrast-smoke&lt;/code&gt; needs both on disk to probe them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Run contrast-smoke
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo run &lt;span class="nt"&gt;--example&lt;/span&gt; contrast-smoke
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect (real output, captured 2026-08-16 against v0.4.3):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;better names=["health", "echo", "confirm_echo"] ttl=Some(60000) scope=Some(Public)
worse  names=["echo", "health"]                 ttl=None        scope=None
contrast-smoke: OK (mcp-better passes BETTER list contract · mcp-worse fails it)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read those two lines side by side — that's the whole post in two rows of text. Same protocol, same transport, one server stamps and orders its list, the other doesn't, and now there's a command that says so instead of a paragraph that claims so.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;mcp-better&lt;/code&gt; ever regresses — someone drops the &lt;code&gt;ttlMs&lt;/code&gt; stamp in a refactor, tool order stops being deterministic — this fails loudly, on the &lt;em&gt;good&lt;/em&gt; server, using the exact same probe that already knows what "bad" looks like. And if &lt;code&gt;mcp-worse&lt;/code&gt; ever accidentally started passing the contract, that fails too (the companion has to stay a reliable liar or the test is worthless).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — What you just proved
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mcp-better&lt;/code&gt;'s list is cache-stamped&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ttlMs &amp;gt; 0&lt;/code&gt;, &lt;code&gt;cacheScope == Public&lt;/code&gt;, read off the wire&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool order is a real contract, not incidental&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mcp-worse&lt;/code&gt; reversing it is what makes the test fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The checker isn't fooled by names&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mcp-worse&lt;/code&gt; shares two tool names with &lt;code&gt;mcp-better&lt;/code&gt; (&lt;code&gt;health&lt;/code&gt;, &lt;code&gt;echo&lt;/code&gt;); wrong order and missing stamps fail it regardless — no name-matching heuristic to fool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The contract has a negative case&lt;/td&gt;
&lt;td&gt;Not just "good passes" — "bad provably fails," same probe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the actual point. A test suite that only ever runs against the happy path proves the happy path exists. It doesn't prove the checker &lt;em&gt;works&lt;/em&gt; — that it would catch a violation if one showed up. &lt;code&gt;mcp-worse&lt;/code&gt; exists so &lt;code&gt;contrast-smoke&lt;/code&gt; has something real to fail against, once, in CI, forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Not a security scanner — it doesn't check auth, injection, or prompt-level trust. It checks one specific, common claim: does the list response match what the docs say about caching and order.&lt;/li&gt;
&lt;li&gt;Not a general-purpose MCP fuzzer. Two tools, one contract, on purpose — small enough to read in five minutes.&lt;/li&gt;
&lt;li&gt;Not a product. &lt;code&gt;mcp-worse&lt;/code&gt; never ships to the MCP Registry. It exists in the same repo as &lt;code&gt;mcp-better&lt;/code&gt;, for the same reason a crash-test dummy exists next to the car.&lt;/li&gt;
&lt;li&gt;Not "MCP servers are untrustworthy." Most aren't audited this way &lt;em&gt;yet&lt;/em&gt; — that's the gap this pattern closes, not an indictment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Steal the pattern
&lt;/h2&gt;

&lt;p&gt;You don't need &lt;code&gt;mcp-worse&lt;/code&gt; specifically. You need the shape:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write down every claim your server's docs make about its wire behavior (cache hints, ordering, transport headers — whatever you promise).&lt;/li&gt;
&lt;li&gt;For each claim, ask: what's the smallest change that would make it false?&lt;/li&gt;
&lt;li&gt;Build &lt;em&gt;that&lt;/em&gt; — deliberately, once, labeled as a teaching/test fixture, never shipped as a product.&lt;/li&gt;
&lt;li&gt;Write one probe that checks both your real server and the broken companion, and asserts they land on opposite sides of every claim.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can't build the broken version, you don't know what your claim depends on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repo (full source referenced above): &lt;a href="https://github.com/Wolfe-Jam/mcp-better" rel="noopener noreferrer"&gt;github.com/Wolfe-Jam/mcp-better&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Prior in this thread: &lt;a href="https://dev.to/wolfejam/not-all-mcp-servers-are-equal-what-728-just-made-official-2f29"&gt;"Not all MCP servers are equal — what 7/28 just made official"&lt;/a&gt; — the claim=wire checklist this post makes concrete&lt;/li&gt;
&lt;li&gt;MCP spec: &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28" rel="noopener noreferrer"&gt;modelcontextprotocol.io/specification/2026-07-28&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Close
&lt;/h2&gt;

&lt;p&gt;A README can't lie to a test that spawns the real process and reads the real wire. &lt;code&gt;mcp-worse&lt;/code&gt; isn't clever — two constants and a missing function call are enough. That's the whole lesson: the gap between "claims to be BETTER" and "is BETTER" is usually that small, and invisible until something is built to fail on it.&lt;/p&gt;

&lt;p&gt;Claim = wire. Build the broken version. Ship the probe that fails on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the smallest claim your own server makes that you've never tested?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm an AAIF Ambassador. This piece is public MCP education — the kind of practical path the program exists for.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>rust</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Not all MCP servers are equal — what 7/28 just made official</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Fri, 31 Jul 2026 06:34:03 +0000</pubDate>
      <link>https://dev.to/wolfejam/not-all-mcp-servers-are-equal-what-728-just-made-official-2f29</link>
      <guid>https://dev.to/wolfejam/not-all-mcp-servers-are-equal-what-728-just-made-official-2f29</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;strong&gt;7/28&lt;/strong&gt; (wire: &lt;strong&gt;&lt;code&gt;2026-07-28&lt;/code&gt;&lt;/strong&gt;) is the modern MCP release. The big deal is &lt;strong&gt;STATELESS&lt;/strong&gt;: no protocol session, request/response core, Discover, stamped lists, Streamable HTTP routing headers. A server can expose &lt;code&gt;health&lt;/code&gt; on three hosts and still be &lt;strong&gt;three different machines operationally&lt;/strong&gt;. This post is the GOOD → BETTER map, plus a &lt;strong&gt;runnable textbook&lt;/strong&gt; you can install in minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;MCP lives under the &lt;a href="https://aaif.io" rel="noopener noreferrer"&gt;Agentic AI Foundation&lt;/a&gt; / Linux Foundation. Spec: &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28" rel="noopener noreferrer"&gt;modelcontextprotocol.io/specification/2026-07-28&lt;/a&gt;. Claude-side rollout note: &lt;a href="https://claude.com/blog/bringing-mcp-2026-07-28-to-claude" rel="noopener noreferrer"&gt;Bringing MCP 2026-07-28 to Claude&lt;/a&gt; (2026-07-28). Ship note: &lt;a href="https://faf.one/blog/mcp-better" rel="noopener noreferrer"&gt;faf.one/blog/mcp-better&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Same name, different machine
&lt;/h2&gt;

&lt;p&gt;InterOp is not “does &lt;code&gt;tools/list&lt;/code&gt; return a string called &lt;code&gt;echo&lt;/code&gt;?”&lt;/p&gt;

&lt;p&gt;InterOp is: &lt;strong&gt;does this server behave like a 7/28 peer under a modern client?&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Why it bites&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Does the client use &lt;strong&gt;Discover&lt;/strong&gt;, or only legacy &lt;code&gt;initialize&lt;/code&gt;?&lt;/td&gt;
&lt;td&gt;Handshake-as-identity is &lt;strong&gt;GOOD-era&lt;/strong&gt;. 7/28 is request/response + &lt;code&gt;server/discover&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does &lt;code&gt;tools/list&lt;/code&gt; return &lt;strong&gt;&lt;code&gt;ttlMs&lt;/code&gt; + &lt;code&gt;cacheScope&lt;/code&gt;&lt;/strong&gt;?&lt;/td&gt;
&lt;td&gt;List cache is part of the modern contract — not optional polish for BETTER claims.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is tool order &lt;strong&gt;stable&lt;/strong&gt;?&lt;/td&gt;
&lt;td&gt;Prompt-cache and client caching assume determinism.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On HTTP: are &lt;strong&gt;&lt;code&gt;Mcp-Method&lt;/code&gt; / &lt;code&gt;Mcp-Name&lt;/code&gt;&lt;/strong&gt; present?&lt;/td&gt;
&lt;td&gt;Routing without body parse — required on Streamable HTTP POSTs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does the server invent &lt;strong&gt;session stickiness&lt;/strong&gt; as identity?&lt;/td&gt;
&lt;td&gt;Protocol sessions are gone. App state = explicit handles if anything.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If two servers both advertise &lt;code&gt;echo&lt;/code&gt; but only one answers those questions honestly, they are &lt;strong&gt;not&lt;/strong&gt; interchangeable. Host A “works.” Host B “flakes.” The tool name stayed the same. The &lt;strong&gt;operational surface&lt;/strong&gt; did not.&lt;/p&gt;




&lt;h2&gt;
  
  
  GOOD habits (pre-7/28 muscle memory)
&lt;/h2&gt;

&lt;p&gt;None of this is “you’re bad.” It was the world the old samples taught.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Habit&lt;/th&gt;
&lt;th&gt;What it looked like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Session as identity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sticky &lt;code&gt;Mcp-Session-Id&lt;/code&gt;, “connected” means long-lived peer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Handshake as the event&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;initialize&lt;/code&gt; / &lt;code&gt;initialized&lt;/code&gt; as the main lifecycle story&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unstamped lists&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tools/list&lt;/code&gt; works, but no cache hints — clients re-poll forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Host-lucky InterOp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Works in one Desktop config; mystery failure in another&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Banner claims&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;README says “modern MCP”; wire is still 2024-shaped&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Call that &lt;strong&gt;GOOD&lt;/strong&gt;: real MCP, real tools, real value — with &lt;strong&gt;session-era operational assumptions&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  BETTER after 7/28 (checklist you can audit)
&lt;/h2&gt;

&lt;p&gt;Humans say &lt;strong&gt;7/28&lt;/strong&gt;. Machines negotiate &lt;strong&gt;&lt;code&gt;2026-07-28&lt;/code&gt;&lt;/strong&gt;. The big deal is &lt;strong&gt;STATELESS&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;BETTER check&lt;/th&gt;
&lt;th&gt;Spec-shaped meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stateless core&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No protocol session; any request can hit any healthy instance (HTTP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Discover&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clients prefer Discover / Auto → 7/28; servers answer discover correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Self-describing traffic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Version + capabilities travel with the request (&lt;code&gt;_meta&lt;/code&gt; / headers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stamped lists&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Positive &lt;code&gt;ttlMs&lt;/code&gt;, intentional &lt;code&gt;cacheScope&lt;/code&gt; (&lt;code&gt;public&lt;/code&gt; for static catalogs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stable tool order&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same process, same order across N list calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP road (if claimed)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Streamable HTTP + &lt;code&gt;Mcp-Method&lt;/code&gt; / &lt;code&gt;Mcp-Name&lt;/code&gt;; no fake “we’re remote-prod” without auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Honest claim surface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docs + CI prove what you claim; no deprecated Roots/Sampling/protocol Logging as greenfield features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MRTR / Tasks / OAuth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real, but &lt;strong&gt;not&lt;/strong&gt; required to be a BETTER &lt;em&gt;tools&lt;/em&gt; textbook on day one — document later if you go deep&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;BETTER is not “more tools.”&lt;/strong&gt; BETTER is &lt;strong&gt;claim = wire&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  If MCP had an AGENTS.md
&lt;/h2&gt;

&lt;p&gt;One box. Steal it for every server README you touch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Operational contract (7/28)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Protocol: 2026-07-28
&lt;span class="p"&gt;-&lt;/span&gt; Lifecycle: Discover-compatible (not initialize-only)
&lt;span class="p"&gt;-&lt;/span&gt; tools/list: ttlMs + cacheScope + stable order
&lt;span class="p"&gt;-&lt;/span&gt; Transports: stdio (default) · Streamable HTTP (if enabled — document bind + auth posture)
&lt;span class="p"&gt;-&lt;/span&gt; Non-goals: (resources / OAuth / Tasks — say so if out of scope)
&lt;span class="p"&gt;-&lt;/span&gt; Prove it: (link CI / smoke that fails when you lie)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Short. Specific. Actionable. Same discipline as a good AGENTS.md — &lt;strong&gt;facts the peer can verify&lt;/strong&gt;, not vibes.&lt;/p&gt;




&lt;h2&gt;
  
  
  10-minute textbook: &lt;code&gt;mcp-better&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Wolfe-Jam/mcp-better" rel="noopener noreferrer"&gt;&lt;code&gt;mcp-better&lt;/code&gt;&lt;/a&gt; is a &lt;strong&gt;small&lt;/strong&gt; Rust server built for this era — not a product platform. Two tools: &lt;code&gt;health&lt;/code&gt;, &lt;code&gt;echo&lt;/code&gt;. Official &lt;code&gt;rmcp&lt;/code&gt; 3. Discover-compatible. Stamped list. v0.1 = 7/28 over &lt;strong&gt;stdio&lt;/strong&gt;. v0.2 = same era + opt-in &lt;strong&gt;Streamable HTTP&lt;/strong&gt; (local demo).&lt;/p&gt;

&lt;p&gt;On crates.io and the MCP Registry as &lt;code&gt;io.github.Wolfe-Jam/mcp-better&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;mcp-better &lt;span class="nt"&gt;--version&lt;/span&gt; 0.2.0
mcp-better &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First install compiles Rust deps once — one-time wait; then you’re done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Run stdio (default)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mcp-better
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point a Discover-capable client / smoke at it. Or from the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Wolfe-Jam/mcp-better.git
&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-better
cargo build &lt;span class="nt"&gt;--bins&lt;/span&gt;
cargo run &lt;span class="nt"&gt;--example&lt;/span&gt; stdio-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect: negotiated &lt;strong&gt;&lt;code&gt;2026-07-28&lt;/code&gt;&lt;/strong&gt;, list stamps (&lt;code&gt;ttlMs&lt;/code&gt; / &lt;code&gt;cacheScope&lt;/code&gt;), tools in stable order: &lt;code&gt;health&lt;/code&gt;, then &lt;code&gt;echo&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Optional HTTP road (same era)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mcp-better &lt;span class="nt"&gt;--http&lt;/span&gt;
&lt;span class="c"&gt;# http://127.0.0.1:8787/mcp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Local demo only — no auth/TLS (see the repo SECURITY.md). From source, &lt;code&gt;http-smoke&lt;/code&gt; exercises list + health + echo with &lt;strong&gt;&lt;code&gt;Mcp-Method&lt;/code&gt; / &lt;code&gt;Mcp-Name&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — What you just proved
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Built for 7/28&lt;/td&gt;
&lt;td&gt;Discover path + era string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List cache&lt;/td&gt;
&lt;td&gt;Positive &lt;code&gt;ttlMs&lt;/code&gt;, &lt;code&gt;public&lt;/code&gt; scope for a static catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dual transport&lt;/td&gt;
&lt;td&gt;stdio default · HTTP opt-in — &lt;strong&gt;not&lt;/strong&gt; a second protocol “version”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small surface&lt;/td&gt;
&lt;td&gt;Two tools — enough to teach the contract&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That’s the point of a textbook: &lt;strong&gt;copy the operational contract&lt;/strong&gt;, not a 40-tool megaserver.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Not a tour of every SEP.
&lt;/li&gt;
&lt;li&gt;Not “everyone has migrated.” Hosts and SDKs roll out on their clocks (Claude products are rolling 7/28 support — see Anthropic’s post).
&lt;/li&gt;
&lt;li&gt;Not a connector-directory pitch. Open Registry + honest wire still matter when app-store shelves optimize for other shapes.
&lt;/li&gt;
&lt;li&gt;Not a product install funnel. One runnable example. One checklist.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Further reading (one hop)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Spec: &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28" rel="noopener noreferrer"&gt;MCP 2026-07-28&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Changelog: &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/changelog" rel="noopener noreferrer"&gt;Key changes&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Textbook: &lt;a href="https://github.com/Wolfe-Jam/mcp-better" rel="noopener noreferrer"&gt;github.com/Wolfe-Jam/mcp-better&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Ship note: &lt;a href="https://faf.one/blog/mcp-better" rel="noopener noreferrer"&gt;faf.one/blog/mcp-better&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Close
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;7/28 is hot for a reason.&lt;/strong&gt; The big deal is &lt;strong&gt;STATELESS&lt;/strong&gt; — Discover, stamped lists, and honest HTTP headers are how the wire enforces that.  &lt;/p&gt;

&lt;p&gt;If you only remember one line: &lt;strong&gt;same tool name ≠ same operational server.&lt;/strong&gt; Audit the contract. Ship the smoke. Prefer BETTER over banner.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I’m an AAIF Ambassador. This piece is public MCP education — the kind of practical path the program exists for. Questions welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>architecture</category>
      <category>backend</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
