<?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>Beyond Valid: What an Agent Needs From a SKILL.md</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Mon, 26 Oct 2026 17:35:16 +0000</pubDate>
      <link>https://dev.to/wolfejam/beyond-valid-what-an-agent-needs-from-a-skillmd-107e</link>
      <guid>https://dev.to/wolfejam/beyond-valid-what-an-agent-needs-from-a-skillmd-107e</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; The Agent Skills validator tells you a &lt;code&gt;SKILL.md&lt;/code&gt; is well formed. It can't tell you whether an agent will pick the skill at the right moment, run it safely, and know when to stop. This tutorial takes one skill from &lt;em&gt;valid&lt;/em&gt; to &lt;em&gt;useful&lt;/em&gt; in six steps, by hand, using only the &lt;a href="https://agentskills.io/specification" rel="noopener noreferrer"&gt;Agent Skills specification&lt;/a&gt; and its reference validator.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;New here?&lt;/em&gt; &lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;Agent Skills&lt;/a&gt; is an open format for giving AI agents new abilities: a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; file (YAML frontmatter, then Markdown instructions), plus any scripts or references it needs. It's an AAIF project, and many agent products read it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The skill we'll fix
&lt;/h2&gt;

&lt;p&gt;Here's a complete, valid skill:&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="nn"&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;release-notes&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Helps with releases.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

Write release notes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it with &lt;code&gt;skills-ref&lt;/code&gt;, the specification's reference validator. With &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; installed, no setup is needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx &lt;span class="nt"&gt;--from&lt;/span&gt; skills-ref agentskills validate release-notes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Valid skill: release-notes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It passes. The specification's own description guidance calls &lt;code&gt;Helps with PDFs.&lt;/code&gt; a poor example, and ours has the same problem. &lt;strong&gt;Valid is the floor, not the goal.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How an agent meets your skill
&lt;/h2&gt;

&lt;p&gt;Two moments matter, and the specification describes both under &lt;em&gt;progressive disclosure&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Choosing.&lt;/strong&gt; At startup the agent loads only each skill's &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; (about 100 tokens). That's all it has when deciding whether your skill fits the task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doing.&lt;/strong&gt; Once it picks your skill, it loads the whole body (under 5,000 tokens recommended) and follows it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can see the choosing view yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx &lt;span class="nt"&gt;--from&lt;/span&gt; skills-ref agentskills to-prompt release-notes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;available_skills&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;skill&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;name&amp;gt;&lt;/span&gt;
release-notes
&lt;span class="nt"&gt;&amp;lt;/name&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;description&amp;gt;&lt;/span&gt;
Helps with releases.
&lt;span class="nt"&gt;&amp;lt;/description&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the agent's whole view while it chooses. "Helps with releases" could mean tagging, publishing, versioning or notes. Steps 2 and 3 fix the choosing; steps 4 to 6 fix the doing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Make it valid
&lt;/h2&gt;

&lt;p&gt;Run the validator on every skill, and fix every error before anything else. Here's what it catches:&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="nn"&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;Release_Notes&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Helps with releases.&lt;/span&gt;
&lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;me&lt;/span&gt;
&lt;span class="nn"&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 plaintext"&gt;&lt;code&gt;Validation failed for Release_Notes:
  - Unexpected fields in frontmatter: author. Only ['allowed-tools', 'compatibility', 'description', 'license', 'metadata', 'name'] are allowed.
  - Skill name 'Release_Notes' must be lowercase
  - Skill name 'Release_Notes' contains invalid characters. Only letters, digits, and hyphens are allowed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The usual fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;name&lt;/code&gt;&lt;/strong&gt;: lowercase letters, digits and single hyphens, 64 characters at most, and the same as the folder's name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;description&lt;/code&gt;&lt;/strong&gt;: 1 to 1,024 characters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extra fields&lt;/strong&gt; (&lt;code&gt;author&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, a client's own settings): move them under &lt;code&gt;metadata&lt;/code&gt;, which takes any string keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Say what it does, then when to use it
&lt;/h2&gt;

&lt;p&gt;The specification says the description &lt;em&gt;should&lt;/em&gt; cover both what the skill does and when to use it, with keywords that match real tasks. Put what it does first, in the third person (as the specification's good example does), then the trigger:&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;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Drafts release notes for a new version from the git log and the pull requests merged since the previous tag. Use when the user asks for release notes, a changelog entry, or "what changed since v1.2".&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trigger words are the ones a user would actually type: &lt;em&gt;release notes&lt;/em&gt;, &lt;em&gt;changelog&lt;/em&gt;, &lt;em&gt;what changed&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Say what it's not for (when a neighbour could trigger it)
&lt;/h2&gt;

&lt;p&gt;If a nearby task could pick your skill by mistake, say so in the description. Release notes sit close to commit messages and pull request descriptions:&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="nn"&gt;...&lt;/span&gt; &lt;span class="s"&gt;Not for commit messages or pull request descriptions.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip this when nothing nearby is easily confused. It's a guard, not a requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Say what it needs and what it touches
&lt;/h2&gt;

&lt;p&gt;These are the facts an agent should have &lt;strong&gt;before step 1&lt;/strong&gt;, not partway through:&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="gs"&gt;**Needs:**&lt;/span&gt; git, and the GitHub CLI (&lt;span class="sb"&gt;`gh`&lt;/span&gt;) signed in to the repo.

&lt;span class="gs"&gt;**Touches:**&lt;/span&gt; reads tags, the git log and merged pull requests. Writes &lt;span class="sb"&gt;`RELEASE_NOTES.md`&lt;/span&gt; only. Publishes nothing: the user creates the release.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Needs&lt;/strong&gt; tells the agent to check its tools first, rather than failing at step 3. &lt;strong&gt;Touches&lt;/strong&gt; tells it (and the person approving its actions) what will change: which files it writes, and whether anything leaves the machine. "Publishes nothing" matters as much as "publishes": it tells the agent where to stop and hand back.&lt;/p&gt;

&lt;p&gt;The specification has an optional &lt;code&gt;compatibility&lt;/code&gt; field for environment requirements, such as &lt;code&gt;Requires git, docker, jq, and access to the internet&lt;/code&gt;. Use it if you like. It's one free-text line, so the reads, writes and publishing still belong in the body.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Give the steps, in order
&lt;/h2&gt;

&lt;p&gt;The specification recommends step-by-step instructions. Number them, and put the exact command in each one where there is one:&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;## Steps&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Find the previous tag: &lt;span class="sb"&gt;`git describe --tags --abbrev=0`&lt;/span&gt;.
&lt;span class="p"&gt;2.&lt;/span&gt; List the commits since it: &lt;span class="sb"&gt;`git log &amp;lt;tag&amp;gt;..HEAD --oneline`&lt;/span&gt;.
&lt;span class="p"&gt;3.&lt;/span&gt; List the pull requests merged since it: &lt;span class="sb"&gt;`gh pr list --state merged --search "merged:&amp;gt;=&amp;lt;tag date&amp;gt;"`&lt;/span&gt;.
&lt;span class="p"&gt;4.&lt;/span&gt; Group them under Added, Changed and Fixed, one line each, in words a user would use.
&lt;span class="p"&gt;5.&lt;/span&gt; Write &lt;span class="sb"&gt;`RELEASE_NOTES.md`&lt;/span&gt; and show it to the user.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Say when it's done
&lt;/h2&gt;

&lt;p&gt;An agent without a finish line keeps polishing, or stops early. Name the finished state of the &lt;em&gt;work&lt;/em&gt; (a file, a result), not a step in the conversation:&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="gs"&gt;**Done when:**&lt;/span&gt; &lt;span class="sb"&gt;`RELEASE_NOTES.md`&lt;/span&gt; lists every pull request merged since the previous tag, and the user has seen it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's also a check the agent can run on itself before it reports back.&lt;/p&gt;




&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&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;release-notes&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Drafts release notes for a new version from the git log and the pull requests merged since the previous tag. Use when the user asks for release notes, a changelog entry, or "what changed since v1.2". Not for commit messages or pull request descriptions.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Release notes&lt;/span&gt;

&lt;span class="gs"&gt;**Needs:**&lt;/span&gt; git, and the GitHub CLI (&lt;span class="sb"&gt;`gh`&lt;/span&gt;) signed in to the repo.

&lt;span class="gs"&gt;**Touches:**&lt;/span&gt; reads tags, the git log and merged pull requests. Writes &lt;span class="sb"&gt;`RELEASE_NOTES.md`&lt;/span&gt; only. Publishes nothing: the user creates the release.

&lt;span class="gu"&gt;## Steps&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Find the previous tag: &lt;span class="sb"&gt;`git describe --tags --abbrev=0`&lt;/span&gt;.
&lt;span class="p"&gt;2.&lt;/span&gt; List the commits since it: &lt;span class="sb"&gt;`git log &amp;lt;tag&amp;gt;..HEAD --oneline`&lt;/span&gt;.
&lt;span class="p"&gt;3.&lt;/span&gt; List the pull requests merged since it: &lt;span class="sb"&gt;`gh pr list --state merged --search "merged:&amp;gt;=&amp;lt;tag date&amp;gt;"`&lt;/span&gt;.
&lt;span class="p"&gt;4.&lt;/span&gt; Group them under Added, Changed and Fixed, one line each, in words a user would use.
&lt;span class="p"&gt;5.&lt;/span&gt; Write &lt;span class="sb"&gt;`RELEASE_NOTES.md`&lt;/span&gt; and show it to the user.

&lt;span class="gs"&gt;**Done when:**&lt;/span&gt; &lt;span class="sb"&gt;`RELEASE_NOTES.md`&lt;/span&gt; lists every pull request merged since the previous tag, and the user has seen it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx &lt;span class="nt"&gt;--from&lt;/span&gt; skills-ref agentskills validate release-notes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Valid skill: release-notes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twenty lines. The specification asks for under 500 lines and about 5,000 tokens; if yours grows past that, move reference material into &lt;code&gt;references/&lt;/code&gt; and link it, one level deep, as the specification recommends. Then check those links point at files that exist.&lt;/p&gt;

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

&lt;p&gt;Run down this for any &lt;code&gt;SKILL.md&lt;/code&gt;, yours or one you're about to install:&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;Check&lt;/th&gt;
&lt;th&gt;Why the agent cares&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;skills-ref validate&lt;/code&gt; passes&lt;/td&gt;
&lt;td&gt;Clients may skip or reject an invalid skill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Description: what it does, then "Use when…"&lt;/td&gt;
&lt;td&gt;It's all the agent sees while choosing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;"Not for…", if a neighbour could trigger it&lt;/td&gt;
&lt;td&gt;Stops the wrong skill being picked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Needs and Touches, before the steps&lt;/td&gt;
&lt;td&gt;Tools checked first; side effects known before they happen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Numbered steps&lt;/td&gt;
&lt;td&gt;The order is part of the instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Done when&lt;/td&gt;
&lt;td&gt;The agent knows when to stop and report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Under 500 lines; local links resolve&lt;/td&gt;
&lt;td&gt;The whole body loads at once; a missing file is a dead end&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check 1 is the specification's hard rules. Checks 2 to 7 come from its &lt;em&gt;should&lt;/em&gt; guidance and recommendations, plus three facts an agent needs that the specification leaves to you: needs, touches and done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: the same facts as data
&lt;/h2&gt;

&lt;p&gt;Steps 4 to 6 are prose, which suits a model reading the body. If you also want tools to read those facts, some authors put them in a small YAML block at the top of the body, called a &lt;strong&gt;FAF Block&lt;/strong&gt; (also called a Context Block):&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="c"&gt;&amp;lt;!-- faf:start --&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;yaml
&lt;/span&gt;&lt;span class="na"&gt;v&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;git&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gh,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;signed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;reads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;log"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merged&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pull&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;requests"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;writes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;RELEASE_NOTES.md&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;github.com&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;publishes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;done_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RELEASE_NOTES.md&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lists&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;every&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pull&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;merged&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;since&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;previous&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tag,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;has&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;seen&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;it"&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- faf:end --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's valid in any skill today: the specification puts no restrictions on the body, and the body loads only after the skill is chosen, so the block costs nothing while the agent is choosing. Further reading on the format it comes from: &lt;a href="https://faf.one/links" rel="noopener noreferrer"&gt;faf.one/links&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to go deeper
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Specification&lt;/strong&gt;: &lt;a href="https://agentskills.io/specification" rel="noopener noreferrer"&gt;agentskills.io/specification&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference validator&lt;/strong&gt;: &lt;a href="https://github.com/agentskills/agentskills/tree/main/skills-ref" rel="noopener noreferrer"&gt;skills-ref&lt;/a&gt; (&lt;code&gt;uvx --from skills-ref agentskills --help&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What Agent Skills are&lt;/strong&gt;: &lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;agentskills.io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Valid gets your skill loaded. The rest gets it chosen at the right moment, run safely, and finished.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>agentskills</category>
      <category>tutorial</category>
      <category>devtools</category>
    </item>
    <item>
      <title>How to Shuffle the Pack: MCP Server Card census reproduction guidance</title>
      <dc:creator>wolfejam.dev</dc:creator>
      <pubDate>Fri, 23 Oct 2026 14:50:01 +0000</pubDate>
      <link>https://dev.to/wolfejam/how-to-shuffle-the-pack-mcp-server-card-census-reproduction-guidance-e7o</link>
      <guid>https://dev.to/wolfejam/how-to-shuffle-the-pack-mcp-server-card-census-reproduction-guidance-e7o</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; The numbers in &lt;a href="https://faf.one/blog/checked-yours" rel="noopener noreferrer"&gt;Checked Yours?&lt;/a&gt; come from three small scripts, 166 lines in all, that only ever read. You can run them yourself in an afternoon. This is how, step by step, and the four choices that move the answer from 8.5% to under 3%.&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 the census is.&lt;/strong&gt; A count of how many MCP servers in the public registry publish a Server Card, the small file that tells an AI app who a server is and how to connect.&lt;br&gt;
&lt;strong&gt;What this post does.&lt;/strong&gt; Walks through the count in three steps: read the registry, check each server, add it up. Then a fourth, to compare two runs.&lt;br&gt;
&lt;strong&gt;Why you'd run it.&lt;/strong&gt; To check our numbers, to count again next week, or to point the same scripts at your own list of servers.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 22 or later, and git.&lt;/li&gt;
&lt;li&gt;About three and a half hours, mostly waiting. Step 2 is resumable: stop it and start it again, and it picks up where it left off.&lt;/li&gt;
&lt;li&gt;Nothing else. No keys, no accounts. Everything it reads is public.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Wolfe-Jam/mcp-server-card-check
&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-server-card-check
npm ci &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CENSUS_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./census-data &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CENSUS_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: read the deck
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node scripts/census/registry.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://registry.modelcontextprotocol.io/v0.1/servers" rel="noopener noreferrer"&gt;MCP Registry&lt;/a&gt; lists every published version of every server, 100 to a page. &lt;code&gt;registry.mjs&lt;/code&gt; reads every page (about 20 minutes) and keeps one card per server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;the latest version only&lt;/strong&gt;, not every release;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;only servers with a remote endpoint&lt;/strong&gt; (&lt;code&gt;streamable-http&lt;/code&gt;). A server you install and run locally has no URL to check;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;each URL once.&lt;/strong&gt; Many entries point at the same address.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On October 20 that took &lt;strong&gt;162,532 entries&lt;/strong&gt; down to &lt;strong&gt;30,142 unique addresses&lt;/strong&gt;. 304 of those were templates, like &lt;code&gt;https://{tenant}.example.com/mcp&lt;/code&gt;. They can't be requested as written, so they're set aside, not guessed. That leaves &lt;strong&gt;29,838&lt;/strong&gt; to check.&lt;/p&gt;

&lt;p&gt;If a page fails, the script waits and tries again, up to five times. It saves a checkpoint as it goes, so a dropped connection costs a page, not the whole read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: check every card, read-only
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CONCURRENCY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;16 node scripts/census/probe.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each address, &lt;code&gt;probe.mjs&lt;/code&gt; runs the same discovery checks as the &lt;a href="https://servercard.wolfejam.dev" rel="noopener noreferrer"&gt;Server Card Check&lt;/a&gt; web page: it looks for a card at &lt;code&gt;&amp;lt;url&amp;gt;/server-card&lt;/code&gt;, then in the AI Catalog at &lt;code&gt;/.well-known/ai-catalog.json&lt;/code&gt;, and checks what it finds against the v1 schema and the hosting rules (CORS, caching).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can't change anything, by construction.&lt;/strong&gt; Every request goes through this guard:&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;readOnlyFetch&lt;/span&gt; &lt;span class="o"&gt;=&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;init&lt;/span&gt; &lt;span class="o"&gt;=&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;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&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;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OPTIONS&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`census is read-only: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; refused`&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;fetch&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="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;follow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&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="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="s2"&gt;User-Agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UA&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;GET and OPTIONS only. The checker's transport checks send a POST, so for the census they're recorded as skipped, and nothing is ever sent to a server's MCP endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be a good guest.&lt;/strong&gt; At most 16 requests at a time, a 10-second timeout each, and a User-Agent that names the project and links to it, so anyone who sees the traffic knows what it is.&lt;/p&gt;

&lt;p&gt;Results go to &lt;code&gt;census.jsonl&lt;/code&gt;, one line per address. Stop it whenever you like; addresses already in the file are skipped next time. On October 20 the checks ran from 12:02 to 15:14 UTC, with no errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: count the hand
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node scripts/census/summarize.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No network. &lt;code&gt;summarize.mjs&lt;/code&gt; is arithmetic over the two files from steps 1 and 2, and writes &lt;code&gt;summary.json&lt;/code&gt;. A &lt;strong&gt;card&lt;/strong&gt; is a &lt;code&gt;200&lt;/code&gt; at a card location that parses as JSON. A &lt;strong&gt;valid card&lt;/strong&gt; also passes the v1 schema. A &lt;code&gt;200&lt;/code&gt; that isn't JSON (usually a website's catch-all page) is counted separately.&lt;/p&gt;

&lt;p&gt;That gives four outcomes for every address, and they are different problems with different fixes. A reader asked for this split after the October 20 post. Here it is for the 27,966 endpoints outside the largest publisher:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Endpoints&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;th&gt;The fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No card&lt;/td&gt;
&lt;td&gt;25,654&lt;/td&gt;
&lt;td&gt;91.7%&lt;/td&gt;
&lt;td&gt;Publish one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A web page where the card should be&lt;/td&gt;
&lt;td&gt;733&lt;/td&gt;
&lt;td&gt;2.6%&lt;/td&gt;
&lt;td&gt;Serve JSON there, or a 404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A card that fails validation&lt;/td&gt;
&lt;td&gt;912&lt;/td&gt;
&lt;td&gt;3.3%&lt;/td&gt;
&lt;td&gt;The checker names the field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A valid card&lt;/td&gt;
&lt;td&gt;667&lt;/td&gt;
&lt;td&gt;2.4%&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;More cards fail than pass. Every row comes from &lt;code&gt;summary.json&lt;/code&gt;: no card is checked minus answered; failing is JSON cards minus valid ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shuffle the pack: four choices that move the answer
&lt;/h2&gt;

&lt;p&gt;The same rows give very different headlines, depending on what you divide by. October 20:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Count by&lt;/th&gt;
&lt;th&gt;Valid&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;2,539 of 29,838&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.51%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint, leaving out the largest publisher&lt;/td&gt;
&lt;td&gt;667 of 27,966&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.39%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distinct host&lt;/td&gt;
&lt;td&gt;586 of 21,526&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.72%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry namespace&lt;/td&gt;
&lt;td&gt;528 of 18,856&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.80%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Namespaces with a single endpoint&lt;/td&gt;
&lt;td&gt;471 of 17,519&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.69%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Entries are not servers.&lt;/strong&gt; One publisher put 1,872 valid cards behind one gateway. Count endpoints and they're 74% of the valid cards. Count hosts, namespaces, or leave that publisher out, and every answer lands under 3%. The script reports the largest publisher separately, so you can see the effect, but it doesn't name it in the headline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latest version only.&lt;/strong&gt; Counting every release would count the same server many times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Templates are skipped, not filled in.&lt;/strong&gt; Guessing a tenant name would be checking a server that may not exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A web page isn't a card.&lt;/strong&gt; 733 addresses answered the card's URL with &lt;code&gt;200 OK&lt;/code&gt; and HTML. Count every &lt;code&gt;200&lt;/code&gt; as "has a card" and the figure goes up by about a fifth (4,184 instead of 3,451).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these is hidden. Each one is a line in &lt;code&gt;summarize.mjs&lt;/code&gt;, and &lt;code&gt;summary.json&lt;/code&gt; carries every denominator, so you can pick another one and say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: compare two runs, or two checkers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CENSUS_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./census-data &lt;span class="nv"&gt;A&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.1.1 &lt;span class="nv"&gt;B&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.4.1 &lt;span class="nv"&gt;SAMPLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;200 npm run census:diff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the checker's rules change between two counts, is a change in the numbers the servers or the checker? &lt;code&gt;census:diff&lt;/code&gt; answers that. It installs both versions from npm and checks each sampled endpoint with A and then B, &lt;strong&gt;in the same minute&lt;/strong&gt;, so a changed verdict comes from the code, not from the server changing in between. The sample is seeded (&lt;code&gt;SEED&lt;/code&gt;), so the same command picks the same endpoints again.&lt;/p&gt;

&lt;p&gt;Before the October 20 post we ran it on 200 endpoints. The October 12 way of counting CORS gave the same verdict on every JSON card in the sample under both versions. So 179 → 324 cards sending all four CORS headers was the servers, not the yardstick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run yours
&lt;/h2&gt;

&lt;p&gt;Point the same scripts at your own servers, or count the registry again next week and compare. If your numbers differ from ours, open an issue on the &lt;a href="https://github.com/Wolfe-Jam/mcp-server-card-check/issues" rel="noopener noreferrer"&gt;repository&lt;/a&gt;. A count anyone can repeat is worth more than one only we can.&lt;/p&gt;

&lt;p&gt;Run a remote MCP server? Paste its address into &lt;a href="https://servercard.wolfejam.dev" rel="noopener noreferrer"&gt;Server Card Check&lt;/a&gt;. Checked yours?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://faf.one/blog/shuffle-the-pack" rel="noopener noreferrer"&gt;faf.one&lt;/a&gt; on October 21, 2026.&lt;/em&gt;&lt;/p&gt;

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