<?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>Checked Yours? MCP Server Cards, counted twice</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Wed, 21 Oct 2026 00:34:21 +0000</pubDate>
      <link>https://dev.to/wolfejam/checked-yours-mcp-server-cards-counted-twice-3b66</link>
      <guid>https://dev.to/wolfejam/checked-yours-mcp-server-cards-counted-twice-3b66</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; MCP Server Cards went Final on October 6. We counted every remote MCP server in the registry on October 12, and again eight days later with the same method. The share of hosts with a valid card rose from &lt;strong&gt;1.9% to 2.7%&lt;/strong&gt;. The registry added 18% more hosts in eight days. Hosts with a valid card? 66% more. Nearly four times faster. We checked twice. Most cards still leave browsers out, but the number that don't nearly doubled.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In Plain English&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;What a Server Card is.&lt;/strong&gt; A small public file that tells an AI app who an MCP server is and how to connect, before it connects. Think of it as the server's business card, pinned to the door.&lt;br&gt;
&lt;strong&gt;What two counts found.&lt;/strong&gt; Still very few servers have one, but more each week. Of the cards that exist, most are missing something browsers need to read them.&lt;br&gt;
&lt;strong&gt;What you can do.&lt;/strong&gt; Paste your server's address into &lt;a href="https://servercard.wolfejam.dev" rel="noopener noreferrer"&gt;Server Card Check&lt;/a&gt;. It tells you, rule by rule, what passes and what to fix.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;The facts follow below.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The count, twice
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127" rel="noopener noreferrer"&gt;SEP-2127&lt;/a&gt; merged as Final on October 6. It gives every MCP server a card: a JSON document with the server's name, version, description and connection details. Clients find it at &lt;code&gt;&amp;lt;server-url&amp;gt;/server-card&lt;/code&gt;, or through an AI Catalog at &lt;code&gt;/.well-known/ai-catalog.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We read the whole MCP Registry and kept the latest version of every server with a remote endpoint. Then we checked each address the way a client would, read-only: GET and OPTIONS requests, nothing that changes state. Twice, with the same scripts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What we counted&lt;/th&gt;
&lt;th&gt;Oct 12&lt;/th&gt;
&lt;th&gt;Oct 20&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Registry entries read&lt;/td&gt;
&lt;td&gt;145,157&lt;/td&gt;
&lt;td&gt;162,532&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoints checked&lt;/td&gt;
&lt;td&gt;26,105&lt;/td&gt;
&lt;td&gt;29,838&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Something answered at a card location&lt;/td&gt;
&lt;td&gt;3,609&lt;/td&gt;
&lt;td&gt;4,184&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;…a web page, not a card&lt;/td&gt;
&lt;td&gt;637&lt;/td&gt;
&lt;td&gt;733&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema-valid cards&lt;/td&gt;
&lt;td&gt;2,194 (8.4%)&lt;/td&gt;
&lt;td&gt;2,539 (8.5%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Valid, by distinct host&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;352 of 18,268 (1.9%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;586 of 21,526 (2.7%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Valid, by registry namespace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;344 of 15,926 (2.2%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;528 of 18,856 (2.8%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Entries are not servers.&lt;/strong&gt; 8.5% looks healthy until you see where it comes from. One gateway publishes a card for each of its registry entries: 1,791 on October 12, 1,872 now. Its cards are real, but they are one deployment. Count by host, by publisher, or leave that gateway out, and the answer lands in the same place: under 3%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What moved.&lt;/strong&gt; In eight days the registry added 12% more entries and 18% more hosts. Hosts with a valid card went from 352 to 586, up 66%. 246 hosts gained one, 197 of them on servers that weren't in the registry a week earlier. 12 lost theirs. Leaving the large gateway out, valid cards went from 403 to 667.&lt;/p&gt;

&lt;h2&gt;
  
  
  Valid is not the same as working
&lt;/h2&gt;

&lt;p&gt;A card can pass the schema and still let its users down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web pages where the card should be.&lt;/strong&gt; 733 endpoints answer the card's address with &lt;code&gt;200 OK&lt;/code&gt; and an HTML page, usually a catch-all route. A client asked for JSON and got a website. 37 of last week's are gone; 133 new ones appeared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browsers left out.&lt;/strong&gt; The spec says hosted cards MUST send CORS headers and lists four. By the method we used on October 12, the number of JSON cards sending all four rose from &lt;strong&gt;179 to 324&lt;/strong&gt; (6.0% to 9.4%). We checked that the yardstick hadn't moved: both versions of the checker, run on the same 200 endpoints in the same minute, agreed on every card. Under the stricter rule the checker uses now, the figure is 298.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeat visits.&lt;/strong&gt; 553 cards answer a repeat request with &lt;code&gt;304 Not Modified&lt;/code&gt;, up from 433. The rest send the whole card every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We checked ours too, on October 12. Our card at &lt;code&gt;context.faf.one&lt;/code&gt; passed the schema and failed two CORS rules. It's fixed, and the fix is the worked example below.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to make yours pass
&lt;/h2&gt;

&lt;p&gt;The most common failures on October 20, across the 1,579 JSON cards outside the large gateway:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Cards&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ETag&lt;/code&gt; not exposed (MUST)&lt;/td&gt;
&lt;td&gt;1,174&lt;/td&gt;
&lt;td&gt;Send &lt;code&gt;Access-Control-Expose-Headers: ETag&lt;/code&gt; on the GET&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preflight doesn't allow &lt;code&gt;If-None-Match&lt;/code&gt; (MUST)&lt;/td&gt;
&lt;td&gt;1,172&lt;/td&gt;
&lt;td&gt;Answer &lt;code&gt;OPTIONS&lt;/code&gt; with &lt;code&gt;Access-Control-Allow-Headers: Content-Type, If-None-Match&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No &lt;code&gt;ETag&lt;/code&gt;, no &lt;code&gt;304&lt;/code&gt; (SHOULD)&lt;/td&gt;
&lt;td&gt;1,005&lt;/td&gt;
&lt;td&gt;Send an &lt;code&gt;ETag&lt;/code&gt;; answer a matching &lt;code&gt;If-None-Match&lt;/code&gt; with &lt;code&gt;304&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not schema-valid (MUST)&lt;/td&gt;
&lt;td&gt;912&lt;/td&gt;
&lt;td&gt;Run the checker; it names the field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong content type (SHOULD)&lt;/td&gt;
&lt;td&gt;888&lt;/td&gt;
&lt;td&gt;Serve it as &lt;code&gt;application/mcp-server-card+json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;$schema&lt;/code&gt; missing or wrong (MUST)&lt;/td&gt;
&lt;td&gt;791&lt;/td&gt;
&lt;td&gt;Use the v1 Server Card schema URL exactly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;description&lt;/code&gt; too long or empty (MUST)&lt;/td&gt;
&lt;td&gt;722&lt;/td&gt;
&lt;td&gt;1 to 100 characters&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the web pages: serve the JSON at &lt;code&gt;/server-card&lt;/code&gt;, or return 404 there. Not a 200 with your homepage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A worked example: ours.&lt;/strong&gt; Our card carries a &lt;code&gt;generated_at&lt;/code&gt; time that changes on every request, by design, so a strong ETag would never repeat. The card now sends a &lt;em&gt;weak&lt;/em&gt; ETag, hashed without that one field. Weak validators mean "the same meaning", and that's what a client needs to get a &lt;code&gt;304&lt;/code&gt;. One field left out of a hash; two new tests.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx mcp-server-card-check https://your-server.example/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/Wolfe-Jam/mcp-server-card-check" rel="noopener noreferrer"&gt;Server Card Check&lt;/a&gt; is the census's checker, made into a tool: MIT-licensed, vendor-neutral, 39 checks, each marked MUST or SHOULD and linked to the spec section it comes from. Read-only by default. It runs in the browser, from the command line, or as a library, and exits 1 on any MUST failure, so it fits in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  A shared test
&lt;/h2&gt;

&lt;p&gt;MCP's official conformance suite doesn't test Server Cards yet. There's an open proposal for that scenario (&lt;a href="https://github.com/modelcontextprotocol/conformance/issues/550" rel="noopener noreferrer"&gt;conformance #550&lt;/a&gt;), and the offer to help write it stands. A shared test is better than anyone's private one, including ours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;Registry read at 12:02 UTC on October 20; checks ran until 15:14 UTC, with no errors. 304 addresses were templates (such as &lt;code&gt;{tenant}&lt;/code&gt;) and were skipped. The checker was v0.4.1. Its rules changed between the two counts (it was reviewed independently twice in between), so every figure compared here was counted by the October 12 method, and that method was re-checked against both versions. Both summaries are in the &lt;a href="https://github.com/Wolfe-Jam/mcp-server-card-check/blob/main/docs/reference.md" rel="noopener noreferrer"&gt;repository&lt;/a&gt;, with the scripts, so every number on this page can be reproduced.&lt;/p&gt;

&lt;p&gt;We'll count again while it's moving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checked yours?
&lt;/h2&gt;

&lt;p&gt;If you run a remote MCP server, paste its address into &lt;a href="https://servercard.wolfejam.dev" rel="noopener noreferrer"&gt;the checker&lt;/a&gt;. No card yet? The report tells you what a valid one needs. Have one? It takes ten seconds to see if browsers can read it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://faf.one/blog/checked-yours" rel="noopener noreferrer"&gt;faf.one&lt;/a&gt; on October 13, 2026, and updated on October 20 with the second count.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>api</category>
    </item>
    <item>
      <title>I argued with AI (Codex) about AGENTS.md. Then I realized what it actually was.</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Thu, 15 Oct 2026 14:35:40 +0000</pubDate>
      <link>https://dev.to/wolfejam/i-argued-with-ai-codex-about-agentsmd-then-i-realized-what-it-actually-was-5ee</link>
      <guid>https://dev.to/wolfejam/i-argued-with-ai-codex-about-agentsmd-then-i-realized-what-it-actually-was-5ee</guid>
      <description>&lt;p&gt;When I was building FAF, I built it with Claude, starting on Sonnet 3.5 (yes, I thought 3.7 had come down from heaven!). I needed to test other AIs too. The Big 3 were Anthropic, Google's Gemini, and OpenAI, who had just released Codex.&lt;/p&gt;

&lt;p&gt;All three got FAF from the get-go. It had a schema and it was YAML, and they love structured data. It's like YAML and noodles to AI 🍜. Claude had &lt;code&gt;CLAUDE.md&lt;/code&gt;, and Gemini reinforced that (copied it, lol) with &lt;code&gt;GEMINI.md&lt;/code&gt;. But not OpenAI. There was no &lt;code&gt;CODEX.md&lt;/code&gt;. They had &lt;code&gt;AGENTS.md&lt;/code&gt;, and Codex kept telling me it "had context." I was fighting with an AI.&lt;/p&gt;

&lt;p&gt;So I stopped using Codex for a while.&lt;/p&gt;

&lt;p&gt;Then the industry chose. &lt;code&gt;AGENTS.md&lt;/code&gt; became the file every coding agent reads: Codex, Cursor, Copilot, Windsurf, Amp, Devin. OpenAI gave it to the Linux Foundation's Agentic AI Foundation in December 2025, and it became an open standard.&lt;/p&gt;

&lt;p&gt;Codex said it had context. I said it didn't. Then a developer from the Svelte community asked me, "Isn't this what &lt;code&gt;AGENTS.md&lt;/code&gt; does?"&lt;/p&gt;

&lt;p&gt;I had to sit back and look. And the penny dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's instructions
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is a guide &lt;em&gt;for agents&lt;/em&gt;: how to build, how to test, the conventions, what not to touch. Instructions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;That's all it was ever meant to be, and it's good at it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The argument I'd been having was never about which file wins. It was about something markdown doesn't do: say &lt;strong&gt;what the project is&lt;/strong&gt;. What it's for, who it's for, what it's built on, what doesn't apply. Instructions need something to be &lt;em&gt;about&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is markdown, not YAML: prose, for instructions. &lt;code&gt;project.faf&lt;/code&gt; sits upstream, next to &lt;code&gt;package.json&lt;/code&gt;, and gives &lt;code&gt;AGENTS.md&lt;/code&gt; the falsifiable facts about the project, on demand. &lt;code&gt;AGENTS.md&lt;/code&gt; instructs the agents.&lt;/p&gt;

&lt;p&gt;That's where the line came from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FAF defines. MD instructs. AI codes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;becomes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FAF defines. AGENTS.md instructs. AI codes.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "defines" means
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;project.faf&lt;/code&gt; is a small YAML file at the root of a repo. It is to context what &lt;code&gt;package.json&lt;/code&gt; is to dependencies, and it sits right next to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── AGENTS.md        ← agents follow this
├── package.json     ← npm reads this
├── project.faf      ← AI reads this
├── README.md        ← humans read this
└── src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fa6rcy9cs0o06lkqw7amt.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.amazonaws.com%2Fuploads%2Farticles%2Fa6rcy9cs0o06lkqw7amt.png" alt="project.faf sitting alphabetically between package.json and README.md in a file browser"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It holds the facts: the goal, the stack, the who, what and why. Each one is a &lt;strong&gt;validated fact&lt;/strong&gt;, from one of two places only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;the repo&lt;/strong&gt;, which &lt;code&gt;faf auto&lt;/code&gt; reads (runtime, build, CI, hosting);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;a person&lt;/strong&gt;, who writes or approves what code can't show (the goal and the who, what and why), through &lt;code&gt;faf go&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing is guessed. A placeholder like &lt;code&gt;tbd&lt;/code&gt; counts as empty, because it has no source.&lt;/p&gt;

&lt;p&gt;It feeds the facts to AI, so tokens are best spent on following instructions and coding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FAF defines. Context, compiled.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Facts, not more instructions
&lt;/h2&gt;

&lt;p&gt;This matters more than it sounds. A 2026 study, &lt;em&gt;Evaluating AGENTS.md: Are Repository-Level Context Files Helpful for Coding Agents?&lt;/em&gt; (Gloaguen et al., &lt;a href="https://arxiv.org/abs/2602.11988" rel="noopener noreferrer"&gt;arXiv 2602.11988&lt;/a&gt;), found that "providing context files does not generally improve task success rates, while increasing inference cost by over 20% on average", for LLM-generated and developer-committed files alike. Their advice: use them for what's non-standard, and evaluate anything else before you rely on it.&lt;/p&gt;

&lt;p&gt;Their latest version adds a finding that matters here: instructions in context files are well followed, but repository overviews, "although popular and recommended by model providers, are not helpful." That's the split this post is about. The paper doesn't say why. My read: an overview is, simply put, more prose. And prose can be stale, non-specific or contradictory, and an agent can't tell which.&lt;/p&gt;

&lt;p&gt;So FAF doesn't write one. The definition stays in &lt;code&gt;project.faf&lt;/code&gt;, where every fact has a source, and what lands in &lt;code&gt;AGENTS.md&lt;/code&gt; is commands, guardrails and the stack: things an agent can run or check. The study tested markdown context files, so it shows the limits of prose. Structured data, a schema in YAML, wasn't in it. That's the right next test, and their advice says so too.&lt;/p&gt;

&lt;p&gt;So: keep the file lean, and give the agent &lt;strong&gt;facts with a source&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's how FAF writes it:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It writes the standard file, plain conformant markdown, never a FAF dialect. Inside it, FAF manages one block of facts: setup and build commands, how to run the tests, where things live, guardrails, the stack. Anything you wrote by hand outside that block is kept. When the facts change, run the same command and the block is rewritten from the source, so it doesn't drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ladder
&lt;/h2&gt;

&lt;p&gt;Here's where any repo stands. With the ladder and the managed block, FAF and &lt;code&gt;AGENTS.md&lt;/code&gt; work together, streamlined:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;NONE&lt;/th&gt;
&lt;th&gt;GOOD&lt;/th&gt;
&lt;th&gt;BETTER&lt;/th&gt;
&lt;th&gt;BEST&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;AGENTS.md&lt;/code&gt;; agents guess&lt;/td&gt;
&lt;td&gt;Any &lt;code&gt;AGENTS.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;An &lt;code&gt;AGENTS.md&lt;/code&gt; with the managed block: lean, current, checked&lt;/td&gt;
&lt;td&gt;BETTER, plus &lt;code&gt;project.faf&lt;/code&gt; in the repo: the source behind the block, and far more, with the app type and the human context (goal, who, why)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What happens&lt;/td&gt;
&lt;td&gt;Every session starts blank&lt;/td&gt;
&lt;td&gt;Goes stale, quietly&lt;/td&gt;
&lt;td&gt;The block's facts have no source in the repo&lt;/td&gt;
&lt;td&gt;Facts and instructions stay in step&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most repos are in the first column. Getting to GOOD is one file. BEST is where the facts stop drifting, because they're defined in one place and every instruction file points back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The camps met
&lt;/h2&gt;

&lt;p&gt;Here's the part I enjoy most. In September, Claude Code started reading &lt;code&gt;AGENTS.md&lt;/code&gt; too: in a project with no &lt;code&gt;CLAUDE.md&lt;/code&gt;, it uses &lt;code&gt;AGENTS.md&lt;/code&gt; instead. The two camps I was choosing between have met.&lt;/p&gt;

&lt;p&gt;Which makes the point for me. The file that instructs will keep changing names, vendors and rules. The facts underneath will not. Define them once, and let every instruction file, &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;GEMINI.md&lt;/code&gt;, &lt;code&gt;.cursorrules&lt;/code&gt;, point back to the same source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Paste either into your terminal or AI 👇&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 auto
&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;npx faf-cli &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--agents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;★ bookmark repo: github.com/Wolfe-Jam/faf-cli&lt;/p&gt;

&lt;p&gt;Then ask your agent what the project is. It won't have to guess.&lt;/p&gt;




&lt;p&gt;Help guide what we build —&lt;br&gt;
Comments · suggestions welcome.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>opensource</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>It sounds like a House of Cards. Is it?</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Mon, 12 Oct 2026 16:15:11 +0000</pubDate>
      <link>https://dev.to/wolfejam/it-sounds-like-a-house-of-cards-is-it-k1c</link>
      <guid>https://dev.to/wolfejam/it-sounds-like-a-house-of-cards-is-it-k1c</guid>
      <description>&lt;p&gt;N cards, N models, N governing bodies. What could possibly go wrong?&lt;/p&gt;

&lt;p&gt;That was my first thought, too. So let's look at it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The business card you hand over before the meeting
&lt;/h2&gt;

&lt;p&gt;Before a meeting, you hand someone a business card. It doesn't do the work. It tells them who you are, what you do, and how to reach you, so the conversation can start in the right place.&lt;/p&gt;

&lt;p&gt;AI agents have the same thing now. The A2A project describes its Agent Card in exactly those words: "It acts as a digital business card."&lt;/p&gt;

&lt;p&gt;An agent's card is a small public file. It says who the agent is, what it can do, which version it runs, and where to reach it. Crucially, another program can read it &lt;strong&gt;before&lt;/strong&gt; connecting, so it can decide whether to connect at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents have them. And, since 6 October, so do MCP servers.
&lt;/h2&gt;

&lt;p&gt;A2A has had Agent Cards for a while. On 6 October 2026 MCP caught up: Server Cards (&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127" rel="noopener noreferrer"&gt;SEP-2127&lt;/a&gt;) went &lt;strong&gt;Final&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An MCP Server Card carries a server's name, version, description and the address of its endpoint. It deliberately leaves out the tools themselves, because those can change from user to user and from minute to minute. The card is the introduction; the connection is the conversation.&lt;/p&gt;

&lt;p&gt;And then there are the others: the MCP Registry's own server listing, AI Catalog entries that point at cards, and identity files that projects keep about themselves. That's the "N cards".&lt;/p&gt;

&lt;h2&gt;
  
  
  The case for "yes, it's a house of cards"
&lt;/h2&gt;

&lt;p&gt;I wanted to know how many servers actually carry a card, so I checked.&lt;/p&gt;

&lt;p&gt;I took a sample of the official MCP Registry: about 50 servers with a public address. I asked each one for its card at the address the spec reserves for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One had one.&lt;/strong&gt; And it was a good one: small and tidy, listed in the site's own AI Catalog, with an AGENTS.md for agents to read.&lt;/p&gt;

&lt;p&gt;That's not a scandal; the spec had only been Final for a few days. But one in fifty shows the gap.&lt;/p&gt;

&lt;p&gt;It gets more interesting. A card is &lt;strong&gt;advisory&lt;/strong&gt;. The spec says so plainly: clients shouldn't treat a card as the truth, but check it against the live connection. A card can drift out of date, overstate what a server supports, or quietly break the small technical rules that let a browser read it at all.&lt;/p&gt;

&lt;p&gt;So I pointed a checker at one of my own live servers, one I'd built and was rather pleased with. It failed &lt;strong&gt;two&lt;/strong&gt; of the spec's required checks. Two headers a browser needs, to read the card and to ask politely whether it had changed, were both missing.&lt;/p&gt;

&lt;p&gt;Then I checked the one good card I'd found. It failed one of the same checks: a browser still couldn't ask politely whether it had changed.&lt;/p&gt;

&lt;p&gt;N cards, N models, N governing bodies, and cards that looked fine but weren't. That's the house of cards, right there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case for "no"
&lt;/h2&gt;

&lt;p&gt;Here's what changes the answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;N governing bodies is shrinking.&lt;/strong&gt; MCP and A2A, the two big agent protocols, now live under one roof: the Agentic AI Foundation, at the Linux Foundation, alongside goose and AGENTS.md. And the roof keeps growing: agentgateway joined in June, A2A in August, and Agent Router (formerly Envoy AI Gateway) in September. The protocols, and the gateways that route traffic between them, now answer to the same neutral home.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cards are being linked, by both camps.&lt;/strong&gt; AI Catalog, currently maintained by the Linux Foundation, is one format for listing cards across protocols. A catalog can point at an MCP Server Card, an A2A Agent Card, or anything else, by type. Its technical steering committee has four seats: two nominated by MCP's core maintainers, two by A2A's steering committee. The bridge is literally built from both sides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rules are written down, and they're checkable.&lt;/strong&gt; SEP-2127 says where a card lives, what it must contain, which headers it must send, and what it must never include (credentials, private addresses). Rules you can check are rules you can fix.&lt;/p&gt;

&lt;p&gt;And that's exactly what happened. I found the two failures &lt;em&gt;because&lt;/em&gt; the check exists. I fixed them and redeployed the same day. Checked again: no required failures.&lt;/p&gt;

&lt;p&gt;A house of cards falls over when you touch it. This one got stronger.&lt;/p&gt;

&lt;h2&gt;
  
  
  A business card you can actually see
&lt;/h2&gt;

&lt;p&gt;There's one more piece. Agent Cards and Server Cards are written for machines, in JSON. But a business card is meant to be handed to a person.&lt;/p&gt;

&lt;p&gt;So I print one as a business card. On the front: the name, a title, and a one-liner, the way you'd read a real one. Flip it over and the back holds the detail: the full identity, the project's instructions for agents, what it remembers, and how machines find it.&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%2Fb6wnk2x9ab6jkvlpupfu.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%2Fb6wnk2x9ab6jkvlpupfu.png" alt="The front of a business card for an MCP server: logo, name, " width="800" height="474"&gt;&lt;/a&gt;&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%2Fqcghkg1nlu4rpr4vkv3f.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%2Fqcghkg1nlu4rpr4vkv3f.png" alt="The back: the About tab with the full identity" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same facts, two readers: one for machines, one for humans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's heading
&lt;/h2&gt;

&lt;p&gt;Three things are worth watching:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verified cards.&lt;/strong&gt; If a card is advisory, the next step is a card that someone has checked, with the check visible. AI Catalog is already working on how independent assessors can sign what they've checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cards that don't drift.&lt;/strong&gt; The spec asks that a card never contradict the server it describes. Tooling that keeps the two in step makes that the default rather than a hope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business cards for people, too.&lt;/strong&gt; A business card works for a person as well as an agent. The title tells you which.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So: is it a house of cards?
&lt;/h2&gt;

&lt;p&gt;It sounds like one: N cards, N models, N governing bodies. But the bodies are converging, the cards are being linked by the people who write the protocols, and the rules are specific enough to check.&lt;/p&gt;

&lt;p&gt;The real risk isn't the number of cards. It's cards that nobody checks.&lt;/p&gt;

&lt;p&gt;Check yours.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;See a project's business card:&lt;/strong&gt; &lt;code&gt;npx -y mcp-context-card card&lt;/code&gt;, run in any project folder. &lt;a href="https://github.com/Wolfe-Jam/mcp-context-card" rel="noopener noreferrer"&gt;mcp-context-card&lt;/a&gt; is open source (MIT), an MCP server for a project's context, memory and identity. It serves a Server Card that passes SEP-2127.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The specs:&lt;/strong&gt; &lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127" rel="noopener noreferrer"&gt;MCP Server Cards (SEP-2127)&lt;/a&gt; · &lt;a href="https://a2a-protocol.org/" rel="noopener noreferrer"&gt;A2A Agent Card&lt;/a&gt; · &lt;a href="https://github.com/Agent-Card/ai-catalog" rel="noopener noreferrer"&gt;AI Catalog&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>opensource</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Context Over MCP, Series Wrap: Six Lessons, FAQs, and What I'd Do Differently</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:05:59 +0000</pubDate>
      <link>https://dev.to/wolfejam/context-over-mcp-series-wrap-six-lessons-faqs-and-what-id-do-differently-2f11</link>
      <guid>https://dev.to/wolfejam/context-over-mcp-series-wrap-six-lessons-faqs-and-what-id-do-differently-2f11</guid>
      <description>&lt;p&gt;Seven pieces, one question: &lt;strong&gt;how does an agent get your project's context when it has no working directory?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; works because a coding agent opens your repo and reads a file. Connect that same agent to a project over MCP, or put a tool in a browser tab, and there's no folder to read. The instructions are still true. The agent just can't see them.&lt;/p&gt;

&lt;p&gt;This wrap is the map, the six lessons, a few FAQs, and what I'd do differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map
&lt;/h2&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%2Fud9a67ungnfwbv3pvy5o.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%2Fud9a67ungnfwbv3pvy5o.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Read it if…&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.linkedin.com/pulse/invisible-agentsmd-meet-visible-mcp-server-card-james-wolfe-harrison-pojbe/" rel="noopener noreferrer"&gt;Invisible AGENTS.md? Meet the visible MCP context card&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;you want the problem in one page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;II&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.linkedin.com/pulse/publishing-mcp-server-official-registry-parts-nobody-wolfe-harrison-zfxve/" rel="noopener noreferrer"&gt;Publishing an MCP server to the official Registry (and the parts nobody writes down)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;you're about to publish a server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;III&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/wolfejam/context-over-mcp-part-iii-horses-for-courses-4286"&gt;Horses for Courses&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;you're deciding what goes in prose and what goes in data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IV&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/wolfejam/context-over-mcp-part-iv-no-working-directory-at-all-g0f"&gt;No Working Directory At All&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;your client is a browser tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Companion&lt;/td&gt;
&lt;td&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;/td&gt;
&lt;td&gt;you'd rather build one than read about it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/wolfejam/context-over-mcp-part-v-the-form-is-the-tool-1cad"&gt;The Form Is the Tool&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;you have HTML forms and no time for script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VI&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/wolfejam/context-over-mcp-part-vi-context-you-can-see-1daa"&gt;Context You Can See&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;you want to see what your agent sees&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Six lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. A file convention doesn't travel over a protocol.&lt;/strong&gt; &lt;code&gt;AGENTS.md&lt;/code&gt; is a filesystem convention. An MCP client has no working directory, so it never finds the file. The fix isn't a new file. It's serving the same context through MCP: tools and resources the client can call, discoverable from the server's metadata. (Part I)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Publishing is where the undocumented failures live.&lt;/strong&gt; The official Registry is straightforward until it isn't: a case-sensitive login match, a 100-character &lt;code&gt;server.json&lt;/code&gt; description cap, and versions drifting between the package and the registry entry. Automating the publish with GitHub Actions OIDC removes most of it. (Part II)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Prose for instructions, data for facts.&lt;/strong&gt; Instructions read best as prose. Facts (versions, commands, paths) drift unless they're structured and checked. Keep them apart and derive one from the other, and a drift bug becomes a failed check instead of a confident wrong answer. (Part III)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The browser has no working directory either.&lt;/strong&gt; With WebMCP, a page registers tools for an in-browser agent: &lt;code&gt;document.modelContext.registerTool()&lt;/code&gt; natively, or a polyfill. The rules that kept it safe were small and boring: mark read-only tools &lt;code&gt;readOnlyHint&lt;/code&gt;, return errors as data, and allowlist every fetch (exact host, HTTPS, no redirects). (Part IV and the companion)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. A form can be the tool.&lt;/strong&gt; Declarative WebMCP turns an existing HTML form into a tool with a few attributes, no script, and the human still confirms the submit. If your site already has forms, you're most of the way there. (Part V)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Context should be visible.&lt;/strong&gt; If an agent works from context, you should be able to see that context. Part VI shows it as a card: inline as an MCP App where the host supports it, as text plus a browser view where it doesn't, and with no config by reading the project from MCP roots. Shown in goose and in the MCP Apps reference host. (Part VI)&lt;/p&gt;

&lt;h2&gt;
  
  
  One rename: "server card" → "context card"
&lt;/h2&gt;

&lt;p&gt;Part I called it a "visible MCP server card". That name was a mistake I'll own. &lt;strong&gt;MCP Server Cards&lt;/strong&gt; are a separate, spec-level idea: metadata that describes a server. What Part I meant, and what Part VI shows, is the &lt;strong&gt;context card&lt;/strong&gt;: the project context an agent actually sees. From here on it's "context card" everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Works with Skills over MCP
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://aaif.io/blog/skills-over-mcp" rel="noopener noreferrer"&gt;Skills over MCP&lt;/a&gt; (SEP-2640) lets an MCP server serve its skills right alongside its tools, as &lt;code&gt;skill://&lt;/code&gt; resources the agent loads when they're relevant. Angie Jones put the idea in one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Essentially, ship the manual with the product."&lt;br&gt;
Angie Jones, &lt;a href="https://aaif.io/blog/skills-over-mcp" rel="noopener noreferrer"&gt;Skills Over MCP&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the agent has the tools, and now it has the manual. One question is still open: &lt;strong&gt;what are we building, for whom, and why?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's project context, and it's what Context over MCP serves: the &lt;code&gt;AGENTS.md&lt;/code&gt; rules, memory and identity the agent would have read from disk if it had a working directory. Tools say what the agent &lt;em&gt;can&lt;/em&gt; do. Skills say &lt;em&gt;how&lt;/em&gt;. Context says &lt;em&gt;what&lt;/em&gt;, &lt;em&gt;for whom&lt;/em&gt; and &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills over MCP ships the manual for the tools. Context over MCP ships the facts about your project. An agent needs both to do the right thing in the right place.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's timely, too. Agent Skills, the &lt;code&gt;SKILL.md&lt;/code&gt; format, is on its way to becoming an AAIF project, and AAIF has already called MCP and Skills &lt;a href="https://aaif.io/blog/closing-the-context-gap-why-mcp-skills-works" rel="noopener noreferrer"&gt;"the two halves of a working agent"&lt;/a&gt;. Project context is what tells those two halves where they are.&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%2Flmbyjr5el8pvppfgsxu8.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%2Flmbyjr5el8pvppfgsxu8.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Skills + Context over MCP = an agent informed and ready.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does this replace AGENTS.md?&lt;/strong&gt; No. &lt;code&gt;AGENTS.md&lt;/code&gt; stays the source. Everything here is about getting the same context to agents that can't open the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My agent reads AGENTS.md fine. Do I need any of this?&lt;/strong&gt; Not for a local coding agent in your repo. It matters when the agent reaches your project over MCP, from another machine, or from a browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which clients show the card inline?&lt;/strong&gt; Hosts that support MCP Apps render it inline; Part VI shows it in the MCP Apps reference host. Elsewhere you get the text card and a browser view, which is how Part VI shows it in goose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is WebMCP in every browser?&lt;/strong&gt; No. Chrome has native support behind a flag (Parts IV and V ran in Chrome 153 with it on). Elsewhere, the polyfill covers it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stateless or stateful?&lt;/strong&gt; The context card server runs over stdio and stateless Streamable HTTP, which keeps it simple to host. The WebMCP pieces run in the browser tab itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name things once.&lt;/strong&gt; The "server card" slip cost a rename. Check a name against the spec vocabulary before it goes in a title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead with the picture.&lt;/strong&gt; Part VI's card made the whole series click for readers. I'd show it in Part I.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship the tutorials first.&lt;/strong&gt; The how-to pieces travelled further than the explanations. A run-it-yourself version of every idea, from the start.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;A goose docs contribution for the context card is next, so the extension setup lives where goose users already look.&lt;/p&gt;

&lt;p&gt;Thanks for reading along. Questions and corrections are welcome in the comments.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>opensource</category>
      <category>goose</category>
      <category>webdev</category>
    </item>
    <item>
      <title>One engine, one number: how six scoring engines came to agree</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Sun, 04 Oct 2026 19:31:04 +0000</pubDate>
      <link>https://dev.to/wolfejam/one-engine-one-number-how-six-scoring-engines-came-to-agree-393b</link>
      <guid>https://dev.to/wolfejam/one-engine-one-number-how-six-scoring-engines-came-to-agree-393b</guid>
      <description>&lt;p&gt;The same &lt;code&gt;project.faf&lt;/code&gt; used to score 64 in one FAF tool and 100 in another. Not because either was broken: each tool had learned to score on its own, and they mostly agreed.&lt;/p&gt;

&lt;p&gt;Mostly isn't good enough for a number people rely on. So we rebuilt it: one engine under every tool, and a test matrix that proves they agree. This is how it works, and the patterns are worth stealing for any product that has to give the same answer in several places.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's being scored
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;project.faf&lt;/code&gt; is a small YAML file at a repo's root that tells any AI what the project is: what it does, who it's for, the stack, how to build it. FAF scores it from 0 to 100% for AI-readiness: how much of what an AI needs is actually written down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isn't that what &lt;code&gt;AGENTS.md&lt;/code&gt; is for?&lt;/strong&gt; We hear this a lot. They work together. &lt;code&gt;AGENTS.md&lt;/code&gt;, the open standard, instructs: how to build, test and work in the repo. &lt;code&gt;project.faf&lt;/code&gt; defines: what the project is, as validated facts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FAF defines. &lt;code&gt;AGENTS.md&lt;/code&gt; instructs. AI codes.&lt;/strong&gt; If you already keep an &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;project.faf&lt;/code&gt; doesn't replace it: it's the source of facts behind it, the same way it sits behind &lt;code&gt;CLAUDE.md&lt;/code&gt;. One command writes them in: &lt;code&gt;faf export --agents&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The score has to be trustworthy, because people gate on it. A CI check fails below a threshold. A team compares repos. An agent decides whether it has enough context to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the scores drifted
&lt;/h2&gt;

&lt;p&gt;FAF grew one tool at a time: a CLI, then MCP servers for Claude, the IDEs, Gemini and Grok, then a Python SDK, then a hosted edge. Each one learned to score.&lt;/p&gt;

&lt;p&gt;Two things drifted:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The slot model.&lt;/strong&gt; The CLI counted 21 slots, while the newer scorers counted 33, with 12 enterprise slots (infrastructure, app, operations). The same file could read 21 of 21 in one place and 21 of 33 in another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The implementations.&lt;/strong&gt; Small differences crept in: placeholder values, rounding, slot names. Before the fix, the hosted scorer matched the reference on only 47 of 80 FAF repos.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The fix: always 33, and one engine
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The model first.&lt;/strong&gt; Every &lt;code&gt;.faf&lt;/code&gt; is scored against the same 33 slots. A slot is in one of three states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;empty:&lt;/strong&gt; every slot starts empty until it's filled. A placeholder such as &lt;code&gt;tbd&lt;/code&gt; or &lt;code&gt;todo&lt;/code&gt; still counts as empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;populated:&lt;/strong&gt; it holds a real value. A validated fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;slotignored:&lt;/strong&gt; the project's app type defines which slots it needs. The rest are marked not applicable, as &lt;code&gt;slotignored&lt;/code&gt;, so there's no doubt, and they drop out of the count.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How FAF defines.&lt;/strong&gt; A validated fact comes from only two places: the repo, which &lt;code&gt;faf auto&lt;/code&gt; reads (runtime, build, CI, hosting), or a person, who writes or approves what code can't show (the goal and the who, what and why) through &lt;code&gt;faf go&lt;/code&gt;. Nothing is guessed. That's why a placeholder counts as empty: it has no source.&lt;/p&gt;

&lt;p&gt;The score is populated slots divided by active slots. A typical app fills the 21 base slots and marks the 12 enterprise slots &lt;code&gt;slotignored&lt;/code&gt;: 21 of 21, so 100%. At 100%, every definition slot for the app type is filled: AI is optimized to code. The same file &lt;em&gt;without&lt;/em&gt; those markers is 21 of 33, so 64%. That difference was the whole bug, made explicit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then the engine.&lt;/strong&gt; The scoring logic lives in one place, and that place is Rust.&lt;/p&gt;

&lt;p&gt;Why Rust: one engine has to run everywhere FAF does. Rust compiles to WebAssembly, so the same compiled code runs inside a Node CLI, behind the MCP servers and on Cloudflare's edge. There's one source and no ports to keep in step. A JavaScript copy and a Workers copy would be two more places to drift.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://crates.io/crates/faf-kernel" rel="noopener noreferrer"&gt;faf-kernel&lt;/a&gt;&lt;/strong&gt;, a Rust crate (v1.1.1), is the engine.&lt;/li&gt;
&lt;li&gt;It compiles to &lt;strong&gt;WebAssembly&lt;/strong&gt; and ships on npm as &lt;code&gt;faf-scoring-kernel&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;faf-cli v8&lt;/strong&gt; carries that WASM build. &lt;code&gt;faf score&lt;/code&gt; is the kernel.&lt;/li&gt;
&lt;li&gt;The MCP servers (&lt;strong&gt;claude-faf-mcp v7&lt;/strong&gt;, &lt;strong&gt;faf-mcp v4&lt;/strong&gt;, &lt;strong&gt;grok-faf-mcp v2&lt;/strong&gt;) don't score at all. They compose faf-cli in-process, so they return faf-cli's answer. They never shell out to a &lt;code&gt;faf&lt;/code&gt; on your PATH, so the answer doesn't depend on what's installed on the machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mcpaas.live v1.8&lt;/strong&gt;, the hosted edge on Cloudflare, runs the kernel itself on every hosted route, plus the README badges and repo cards.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That covers every JavaScript and edge surface with one copy of one engine. Python is the exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where you can't compose, prove parity
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;faf-python-sdk v2&lt;/strong&gt; is a pure-Python implementation of the same rules. A second implementation is exactly how drift starts, so it comes with a parity test.&lt;/p&gt;

&lt;p&gt;The test scores 845 files with both the kernel and the SDK, and compares more than the final number:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the score;&lt;/li&gt;
&lt;li&gt;the tier;&lt;/li&gt;
&lt;li&gt;the slot counts;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;every slot's state&lt;/strong&gt;, slot by slot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The files range from real &lt;code&gt;project.faf&lt;/code&gt; files to deliberately awkward YAML. Result: 845 of 845 match. The SDK also never raises: unreadable YAML scores 0 with every slot empty, for any &lt;code&gt;str&lt;/code&gt; or &lt;code&gt;bytes&lt;/code&gt; input.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;faf_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;score_faf&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score_faf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project.faf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;gemini-faf-mcp v3&lt;/strong&gt; scores through that SDK, so Gemini inherits the parity for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proof: 54 of 54
&lt;/h2&gt;

&lt;p&gt;A claim like "every tool agrees" is only worth what you can check. So on September 30 we scored nine public &lt;code&gt;project.faf&lt;/code&gt; files through six engines: the kernel, faf-cli, the Python SDK, and the three hosted MCP routes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;kernel&lt;/th&gt;
&lt;th&gt;faf-cli&lt;/th&gt;
&lt;th&gt;Python&lt;/th&gt;
&lt;th&gt;/claude&lt;/th&gt;
&lt;th&gt;/grok&lt;/th&gt;
&lt;th&gt;/faf&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;agents-md-facts&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mcp-context-card&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude-faf-mcp&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;faf-cli&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;faf-mcp&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;faf-python-sdk&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemini-faf-mcp&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;grok-faf-mcp&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;faf-cli, no markers&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nine files, six engines, 54 scores, all equal. claude-faf-mcp, faf-mcp and grok-faf-mcp score through faf-cli, and gemini-faf-mcp through the Python SDK, so the table covers them too.&lt;/p&gt;

&lt;p&gt;The non-100 rows matter as much as the 100s. Agreeing on 52 and 56 shows the engines agree on &lt;em&gt;partial&lt;/em&gt; credit, which is where implementations usually drift. The last row is the faf-cli file with its &lt;code&gt;slotignored&lt;/code&gt; markers stripped: 36 everywhere, until you add them back.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The table is the September 30 run. Both partial files have since reached 100%: agents-md-facts with a single &lt;code&gt;faf auto&lt;/code&gt;, the command in the next section.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading is one command
&lt;/h2&gt;

&lt;p&gt;This was a breaking change on purpose. A file written before always-33 has no enterprise markers, so its 12 enterprise slots now count as empty, and a 21-slot file drops to 64%.&lt;/p&gt;

&lt;p&gt;The fix is to run this once in the project:&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 auto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It writes the 12 &lt;code&gt;slotignored&lt;/code&gt; markers (unless your app type actually uses those slots), and the score returns. On four real v7-era files: 56 → 100, 56 → 100, 58 → 100, 52 → 100.&lt;/p&gt;

&lt;p&gt;If anything gates on the score, such as a CI threshold, re-check the number after upgrading. A new file from &lt;code&gt;faf init&lt;/code&gt;, &lt;code&gt;faf auto&lt;/code&gt; or &lt;code&gt;faf git&lt;/code&gt; already carries the markers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns worth stealing
&lt;/h2&gt;

&lt;p&gt;None of this is specific to FAF. If your product computes the same answer in several places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Make the model explicit before the code.&lt;/strong&gt; "Not applicable" is not the same as "empty". Once that's a named state, half the disagreements disappear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose, don't reimplement.&lt;/strong&gt; Every surface that &lt;em&gt;can&lt;/em&gt; call the one engine should. Our MCP servers went from "scoring" to "asking faf-cli", and a whole class of bugs went with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where you must reimplement, test parity on every field.&lt;/strong&gt; Comparing the final number hides compensating errors. Compare every intermediate state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish the matrix.&lt;/strong&gt; Real files, every engine, the partial scores included. It's the cheapest credibility there is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never resolve the engine from PATH.&lt;/strong&gt; In-process composition means the answer is a property of the package version, not the machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Paste either into your terminal or AI 👇&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx faf-cli auto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; faf-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;★ bookmark repo: github.com/Wolfe-Jam/faf-cli&lt;/p&gt;

&lt;p&gt;Then ask Claude, your IDE, Gemini or Grok for the score. It will be the same number.&lt;/p&gt;

&lt;p&gt;The full release series, one post per product: &lt;a href="https://faf.one/blog/always33" rel="noopener noreferrer"&gt;The Always33 Suite&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Help guide what we build —&lt;br&gt;
Comments · suggestions welcome.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>testing</category>
      <category>mcp</category>
      <category>rust</category>
    </item>
    <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>
  </channel>
</rss>
