<?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: Yonyon</title>
    <description>The latest articles on DEV Community by Yonyon (@yonyonai).</description>
    <link>https://dev.to/yonyonai</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%2F3819756%2F48d4dde4-41e4-44a5-b714-9ced7c8ce432.jpg</url>
      <title>DEV Community: Yonyon</title>
      <link>https://dev.to/yonyonai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yonyonai"/>
    <language>en</language>
    <item>
      <title>Elicitation didn't die in the MCP stateless rewrite. It's the only one that survived.</title>
      <dc:creator>Yonyon</dc:creator>
      <pubDate>Thu, 06 Aug 2026 18:07:59 +0000</pubDate>
      <link>https://dev.to/yonyonai/elicitation-didnt-die-in-the-mcp-stateless-rewrite-its-the-only-one-that-survived-3o8</link>
      <guid>https://dev.to/yonyonai/elicitation-didnt-die-in-the-mcp-stateless-rewrite-its-the-only-one-that-survived-3o8</guid>
      <description>&lt;p&gt;Most people I've talked to since the 2026-07-28 MCP specification landed believe&lt;br&gt;
elicitation was killed along with the rest of the bidirectional surface. It wasn't. It's&lt;br&gt;
the one that got invested in, and the asymmetry tells you something real about how to&lt;br&gt;
think about safety primitives in a protocol.&lt;/p&gt;
&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;The 2026-07-28 revision turned MCP from a stateful, bidirectional protocol into plain&lt;br&gt;
request/response. Every request is self-describing, so any request can land on any&lt;br&gt;
instance behind a round-robin load balancer.&lt;/p&gt;

&lt;p&gt;That rewrite had to do something about the features that assumed an open connection:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;feature&lt;/th&gt;
&lt;th&gt;fate&lt;/th&gt;
&lt;th&gt;SEP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Roots&lt;/td&gt;
&lt;td&gt;deprecated&lt;/td&gt;
&lt;td&gt;&lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;SEP-2577&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sampling&lt;/td&gt;
&lt;td&gt;deprecated&lt;/td&gt;
&lt;td&gt;&lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;SEP-2577&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logging&lt;/td&gt;
&lt;td&gt;deprecated&lt;/td&gt;
&lt;td&gt;&lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;SEP-2577&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Elicitation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;redesigned onto MRTR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;SEP-2322&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Deprecated does not mean gone. A formal deprecation policy (SEP-2596) guarantees a&lt;br&gt;
twelve-month minimum window, so the first three still work.&lt;/p&gt;

&lt;p&gt;But only one of the four was rebuilt.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why elicitation got the different treatment
&lt;/h2&gt;

&lt;p&gt;Roots and sampling are &lt;em&gt;capabilities&lt;/em&gt;. They let a server do more.&lt;/p&gt;

&lt;p&gt;Elicitation is a &lt;em&gt;control&lt;/em&gt;. It lets a server do less, on purpose, until a human says&lt;br&gt;
otherwise.&lt;/p&gt;

&lt;p&gt;A protocol can drop a capability and leave users with a smaller feature set. If it drops&lt;br&gt;
the control, every tool that guards an irreversible operation loses its guard, and the&lt;br&gt;
failure is silent, because the tool just starts succeeding. That asymmetry is why elicitation&lt;br&gt;
was worth the cost of a redesign.&lt;/p&gt;
&lt;h2&gt;
  
  
  What elicitation is
&lt;/h2&gt;

&lt;p&gt;A server, mid tool call, pauses and asks the user a question.&lt;/p&gt;

&lt;p&gt;Without it, a tool either runs or refuses. With it, a tool can stop halfway and say&lt;br&gt;
"this will delete 40 records, confirm?" and block until answered.&lt;/p&gt;

&lt;p&gt;In a production MCP deployment this typically guards the operations a retry cannot&lt;br&gt;
undo: outbound messaging, content publishing, record deletion.&lt;/p&gt;
&lt;h2&gt;
  
  
  Old mechanism vs MRTR
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before.&lt;/strong&gt; The server pushes a request down an open bidirectional stream to the client&lt;br&gt;
and blocks waiting for the answer. This requires a persistent connection, precisely what&lt;br&gt;
the stateless core removes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After (MRTR, &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;SEP-2322&lt;/a&gt;).&lt;/strong&gt; The server returns an ordinary response:&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;"resultType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"input_required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requests"&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="err"&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;The client collects answers from the user, then &lt;strong&gt;retries the original tool call&lt;/strong&gt; with&lt;br&gt;
them attached in &lt;code&gt;inputResponses&lt;/code&gt;. Each leg is a complete request/response cycle.&lt;/p&gt;

&lt;p&gt;The 4.x implementation states the philosophy directly in its own docstring:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The protocol is stateless: each MRTR leg is a complete request→response cycle. When a&lt;br&gt;
guard tool returns an &lt;code&gt;InputRequiredResult&lt;/code&gt; from its body to ask the client for input,&lt;br&gt;
that ask is the &lt;em&gt;legitimate result&lt;/em&gt; of this tool call — not a pause, not an error, not&lt;br&gt;
a third control-flow outcome.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That framing matters for implementers. The ask flows through the middleware chain as an&lt;br&gt;
ordinary return value. Middleware completes normally. You identify one with a plain&lt;br&gt;
&lt;code&gt;isinstance&lt;/code&gt; check rather than a special control path.&lt;/p&gt;
&lt;h2&gt;
  
  
  The trap: an empty schema is not a confirmation
&lt;/h2&gt;

&lt;p&gt;Here is the part worth acting on today, regardless of which protocol version you're on.&lt;/p&gt;

&lt;p&gt;FastMCP's &lt;code&gt;Context.elicit&lt;/code&gt; takes a &lt;code&gt;response_type&lt;/code&gt;. Passing &lt;code&gt;None&lt;/code&gt; produces this schema:&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="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;The library documents it: &lt;em&gt;"When response_type is None, the accepted elicitation will&lt;br&gt;
contain an empty dict."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An empty dict means the accept payload carries &lt;strong&gt;no data&lt;/strong&gt;. And that means a client that&lt;br&gt;
auto-accepts produces bytes indistinguishable, on the wire, from a human clicking&lt;br&gt;
approve.&lt;/p&gt;

&lt;p&gt;I confirmed this against a running system. A delete tool executed its full delete path&lt;br&gt;
with &lt;code&gt;confirm=False&lt;/code&gt;. The gate existed in the code, was reached at runtime, and stopped&lt;br&gt;
nothing.&lt;/p&gt;

&lt;p&gt;The fix is not "add a confirmation step." It is to stop treating &lt;em&gt;acceptance&lt;/em&gt; as the&lt;br&gt;
signal and require an affirmative &lt;em&gt;value&lt;/em&gt;:&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="n"&gt;CONFIRM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Final&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elicit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CONFIRM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now approval carries information. An empty or absent response is not approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part where two bugs turned out to be one bug
&lt;/h2&gt;

&lt;p&gt;That corrected call needs the &lt;code&gt;list[str]&lt;/code&gt; overload of &lt;code&gt;elicit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Under mypy, that overload did not exist.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Context.elicit&lt;/code&gt; declares six &lt;code&gt;@overload&lt;/code&gt; stubs. Each stub's explanatory string sat&lt;br&gt;
&lt;em&gt;after&lt;/em&gt; the stub body rather than inside it, making it a bare expression statement rather&lt;br&gt;
than a docstring. A statement between overloads terminates the overload series for mypy,&lt;br&gt;
so mypy registered stub one and ignored the rest.&lt;/p&gt;

&lt;p&gt;pyright and ty both tolerate it and see all six. Fixed upstream in&lt;br&gt;
&lt;a href="https://github.com/PrefectHQ/fastmcp/pull/4774" rel="noopener noreferrer"&gt;PrefectHQ/fastmcp#4774&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So the safest available call shape was the one the type checker reported as&lt;br&gt;
nonexistent. The type bug and the security bug were the same bug, one layer apart.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two things I verified that you may want to check yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;response_type: None&lt;/code&gt; is removed in 4.0.0b1.&lt;/strong&gt; In 3.x it was overload one and marked&lt;br&gt;
deprecated. In the 4.x beta the first overload is &lt;code&gt;response_type: type[T]&lt;/code&gt; and the &lt;code&gt;None&lt;/code&gt;&lt;br&gt;
variant is gone. The unsafe default deleted itself. If you already hardened to an&lt;br&gt;
explicit value list, you are on the 4.x-correct shape ahead of the migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The overload fix is not in any release.&lt;/strong&gt; Read from the PyPI JSON API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;latest stable      3.4.6     uploaded 2026-08-05    still has all six
newest 4.x         4.0.0b1   uploaded 2026-07-28    predates the fix, has five
4.0.0 stable       does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is on &lt;code&gt;main&lt;/code&gt;. There is no 3.x maintenance branch. If you pin below the major, you&lt;br&gt;
do not have it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grep for &lt;code&gt;response_type=None&lt;/code&gt;&lt;/strong&gt; in anything that gates an irreversible operation.
That is the empty-schema path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a second type checker as a non-blocking advisory job.&lt;/strong&gt; Not to gate on. Just so
something in your pipeline is capable of seeing what your primary checker cannot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write your dual-protocol contract tests now.&lt;/strong&gt; The confirm gate has to hold on both
an old-protocol client and a 2026-07-28 client through the twelve-month runway. The
failure mode to test explicitly is a silent fall-through that auto-approves. Writing
these before the SDK ships means the migration has a green target instead of being
validated afterward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove the gate is reached, not merely present.&lt;/strong&gt; Revert the wiring, watch the new
tests fail, restore. Green tests over unreachable code is the failure this catches.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A note on the scan
&lt;/h2&gt;

&lt;p&gt;I checked whether the overload defect was widespread by AST-scanning a production&lt;br&gt;
dependency tree for the construct: a bare string expression sitting between two&lt;br&gt;
&lt;code&gt;@overload&lt;/code&gt; definitions.&lt;/p&gt;

&lt;p&gt;13,920 &lt;code&gt;.py&lt;/code&gt; files. Six occurrences. All six in one file, all unintentional, all now&lt;br&gt;
fixed upstream.&lt;/p&gt;

&lt;p&gt;Worth stating because it cuts against the obvious conclusion. This is not a common&lt;br&gt;
Python footgun you should go hunting for. It is a single localized mistake that survived&lt;br&gt;
into a release because the checker that catches it was not the checker that project ran.&lt;/p&gt;

&lt;p&gt;The typing specification, incidentally, does not cover this case at all. It requires only&lt;br&gt;
that overload definitions "be followed by an overload implementation, which does not&lt;br&gt;
include an &lt;code&gt;@overload&lt;/code&gt; decorator", and says nothing about intervening statements. So&lt;br&gt;
mypy is not right and ty is not wrong. Each picked a behavior in a gap, and a bug lived&lt;br&gt;
in the gap between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A green gate proves that the checker you ran agrees with you. It does not prove the code&lt;br&gt;
is right.&lt;/p&gt;

&lt;p&gt;Three type checkers looked at that file. One saw the bug. The project shipped it running&lt;br&gt;
one of the two that did not.&lt;/p&gt;




&lt;p&gt;Upstream issue: &lt;a href="https://github.com/PrefectHQ/fastmcp/issues/4773" rel="noopener noreferrer"&gt;PrefectHQ/fastmcp#4773&lt;/a&gt;&lt;br&gt;
Upstream fix: &lt;a href="https://github.com/PrefectHQ/fastmcp/pull/4774" rel="noopener noreferrer"&gt;PrefectHQ/fastmcp#4774&lt;/a&gt;&lt;br&gt;
Spec: the 2026-07-28 MCP specification announcement, &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;SEP-2322&lt;/a&gt; / &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;SEP-2577&lt;/a&gt; / &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;SEP-2596&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;The 2026-07-28 MCP specification announcement&lt;/a&gt; — source for SEP-2322 (MRTR), SEP-2577 (deprecations) and SEP-2596 (the 12-month policy)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/modelcontextprotocol/modelcontextprotocol" rel="noopener noreferrer"&gt;modelcontextprotocol/modelcontextprotocol&lt;/a&gt; — the spec repo, where the SEPs live&lt;/li&gt;
&lt;li&gt;The overload bug referenced above: issue &lt;a href="https://github.com/PrefectHQ/fastmcp/issues/4773" rel="noopener noreferrer"&gt;PrefectHQ/fastmcp#4773&lt;/a&gt;, fix &lt;a href="https://github.com/PrefectHQ/fastmcp/pull/4774" rel="noopener noreferrer"&gt;PrefectHQ/fastmcp#4774&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/fastmcp/" rel="noopener noreferrer"&gt;fastmcp on PyPI&lt;/a&gt; — confirm which version you are on before assuming you have the fix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Previously in this series:&lt;/strong&gt; &lt;a href="https://dev.to/yonyonai/the-bug-fastmcps-own-ci-could-not-see-gh6"&gt;The bug FastMCP's own CI could not see&lt;/a&gt; — three type checkers, one saw it, and the project ran one of the two that did not.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>python</category>
      <category>security</category>
      <category>typing</category>
    </item>
    <item>
      <title>The bug FastMCP's own CI could not see</title>
      <dc:creator>Yonyon</dc:creator>
      <pubDate>Thu, 06 Aug 2026 18:07:57 +0000</pubDate>
      <link>https://dev.to/yonyonai/the-bug-fastmcps-own-ci-could-not-see-gh6</link>
      <guid>https://dev.to/yonyonai/the-bug-fastmcps-own-ci-could-not-see-gh6</guid>
      <description>&lt;p&gt;I opened a pull request to &lt;a href="https://github.com/PrefectHQ/fastmcp" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt; (27k stars) at 23:12:32 UTC. A bot closed it at 23:12:47. Fifteen seconds.&lt;/p&gt;

&lt;p&gt;A second bot then labeled it &lt;code&gt;too-long&lt;/code&gt;, with the comment: &lt;em&gt;"condense this issue. We'll triage it once it's trimmed down."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It was merged the next day, unedited. Nobody condensed anything. The label is still on the merged PR.&lt;/p&gt;

&lt;p&gt;That is the funny part. The bug underneath it is the useful part, because it is a failure mode that any project with a type-checking gate can be sitting on right now without knowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Context.elicit&lt;/code&gt; is FastMCP's "ask the human a question" primitive. It declares six alternative call signatures as &lt;code&gt;@overload&lt;/code&gt; stubs, one per supported &lt;code&gt;response_type&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each stub's body was &lt;code&gt;...&lt;/code&gt;, and the explanatory prose sat &lt;em&gt;after&lt;/em&gt; the body. Verbatim from &lt;code&gt;fastmcp/server/context.py&lt;/code&gt; at 3.4.5, lines 1022 to 1061:&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="nd"&gt;@overload&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;elicit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;AcceptedElicitation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;DeclinedElicitation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;CancelledElicitation&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;When response_type is None, the accepted elicitation will contain an
    empty dict&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;                                          &lt;span class="c1"&gt;# &amp;lt;-- not a docstring
&lt;/span&gt;
    &lt;span class="nd"&gt;@overload&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;elicit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AcceptedElicitation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;DeclinedElicitation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;CancelledElicitation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;When response_type is not None, the accepted elicitation will contain the
    response data&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="nd"&gt;@overload&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;elicit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AcceptedElicitation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;DeclinedElicitation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;CancelledElicitation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;When response_type is a list of strings, the accepted elicitation will
    contain the selected string response&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three more follow the same pattern, six in total.&lt;/p&gt;

&lt;p&gt;That string is not a docstring. A docstring has to be the &lt;em&gt;first&lt;/em&gt; statement inside a function or class body. Here the body already ended at &lt;code&gt;...&lt;/code&gt;, so the string is a bare expression statement sitting in the class body, between two overloads.&lt;/p&gt;

&lt;p&gt;And a statement between overloads terminates the overload chain for mypy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why only mypy
&lt;/h2&gt;

&lt;p&gt;Here is the reduced shape. No FastMCP install needed, paste it into a file and run two checkers:&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;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;overload&lt;/span&gt;

&lt;span class="nd"&gt;@overload&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Doc for the None case.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="nd"&gt;@overload&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Doc for the list case.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;

&lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# mypy: error.  pyright: fine.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;checker&lt;/th&gt;
&lt;th&gt;overloads registered&lt;/th&gt;
&lt;th&gt;verdict on the &lt;code&gt;list[str]&lt;/code&gt; call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mypy&lt;/td&gt;
&lt;td&gt;1 of 6&lt;/td&gt;
&lt;td&gt;error, no call-site workaround&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pyright&lt;/td&gt;
&lt;td&gt;6 of 6&lt;/td&gt;
&lt;td&gt;fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ty&lt;/td&gt;
&lt;td&gt;6 of 6&lt;/td&gt;
&lt;td&gt;fine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;pyright and ty tolerate the interleaved statement and keep collecting overloads. mypy stops.&lt;/p&gt;

&lt;p&gt;FastMCP's own gate is &lt;code&gt;ty&lt;/code&gt;. So the project's CI was green on a file that was broken for a large share of its users, and it stayed that way through a release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it actually bit
&lt;/h2&gt;

&lt;p&gt;Two details turn this from a style nit into a live problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First: the one stub mypy could still see is the deprecated one.&lt;/strong&gt; In the 3.x line the first overload is &lt;code&gt;response_type: None&lt;/code&gt;, which the library's own docstring marks as deprecated. So mypy users were being funneled toward exactly the call shape the library is retiring. There is no way out at the call site either. I tried five formulations (explicit annotation, &lt;code&gt;cast&lt;/code&gt;, a typed local, a &lt;code&gt;Sequence[str]&lt;/code&gt; alias, direct literal). All five fail under mypy. All five pass under pyright.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second: I needed the invisible signature for a security control.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;response_type=None&lt;/code&gt; compiles to an empty JSON schema:&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library documents this itself, in the very docstring that broke the overload chain: &lt;em&gt;"When response_type is None, the accepted elicitation will contain an empty dict."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An empty dict means the accept payload carries no data. A client that auto-accepts produces bytes that are indistinguishable, on the wire, from a human clicking approve. I proved that with a live probe a week earlier, when a delete tool ran its full delete path with &lt;code&gt;confirm=False&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The hardening is to stop treating "accepted" as a signal and require an affirmative &lt;em&gt;value&lt;/em&gt;:&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="n"&gt;CONFIRM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Final&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cancel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elicit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CONFIRM&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;list[str]&lt;/code&gt;. One of the five overloads mypy could not see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Move each literal inside its stub body, where it becomes an actual docstring:&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="nd"&gt;@overload&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;elicit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AcceptedElicitation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;DeclinedElicitation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;CancelledElicitation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The accepted elicitation will contain no data&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;+15 / -24&lt;/code&gt;, one file. No runtime change, no API change. The implementation function is byte-identical before and after (I hashed it). The deletion count is larger than the addition count only because dropping the trailing &lt;code&gt;...&lt;/code&gt; let &lt;code&gt;ruff format&lt;/code&gt; collapse two return annotations onto fewer lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timeline
&lt;/h2&gt;

&lt;p&gt;Every timestamp below is from the GitHub timeline API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;08-05 22:36:16Z  issue opened
08-05 23:12:32Z  PR opened
08-05 23:12:47Z  auto-CLOSED, 15 seconds later
                 (missing-issue-link: external PRs must reference an
                  issue ASSIGNED to their author)
08-05 23:14:09Z  bot labels it `too-long`
                 "Excessively verbose or unedited LLM output.
                  Condense before triage."
                 ... ~14 hours of silence ...
08-06 13:29:05Z  maintainer assigns the issue
08-06 13:29:17Z  label removed, PR auto-REOPENS
08-06 13:29:30Z  APPROVED, 13 seconds later
08-06 13:34:12Z  a second PR merges, see below
08-06 13:35:50Z  MERGED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The part I did not expect
&lt;/h2&gt;

&lt;p&gt;Twenty-eight seconds after approving my PR, the project lead opened a branch named &lt;code&gt;codex/review-closed-contributor-prs&lt;/code&gt; and merged it four minutes later. It adds a line to FastMCP's own &lt;code&gt;CLAUDE.md&lt;/code&gt;, the file that instructs their review agents:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Review closed contributor PRs. External PRs may be closed as part of the issue-link workflow, so closure alone is not a negative signal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then my PR merged.&lt;/p&gt;

&lt;p&gt;I want to be precise about what I am claiming here: there is no explicit cross-reference between those two pull requests. I am reporting the order of events and the twenty-eight second gap. Draw your own conclusion.&lt;/p&gt;

&lt;p&gt;It is a good line to add either way. When a repo automates triage, "this PR is closed" stops meaning "a human rejected this" and starts meaning "a bot ran." Anything downstream that reads closure as a signal, human or agent, is now reading a stale convention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status, honestly
&lt;/h2&gt;

&lt;p&gt;Merged to &lt;code&gt;main&lt;/code&gt;, which is the 4.x line, 289 commits ahead of the latest release. It is &lt;strong&gt;not&lt;/strong&gt; in any release.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v3.4.6   released 08-05   still has all six stray literals   &amp;lt;- PyPI serves this
main     merged   08-06   fixed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no &lt;code&gt;3.x&lt;/code&gt; maintenance branch on the repo. If you pin &lt;code&gt;fastmcp&amp;gt;=3.4.5,&amp;lt;4&lt;/code&gt;, as I do across eleven packages, you consume the broken version until someone backports it or you move to 4.x.&lt;/p&gt;

&lt;p&gt;Also worth saying plainly: this never broke my own CI, because my gate is &lt;code&gt;ty&lt;/code&gt;, not mypy. It breaks downstream consumers of my package who run mypy. I found it by running a checker my project does not run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A green gate proves that the checker you ran agrees with you. It does not prove the code is right.&lt;/p&gt;

&lt;p&gt;Three type checkers looked at this file. One saw the bug. FastMCP happened to run one of the two that did not, and shipped it. I happened to run the third, for unrelated reasons, on code I needed for a security fix.&lt;/p&gt;

&lt;p&gt;If you maintain a typed Python library, the cheap version of this lesson is: run a second checker in a non-blocking job. You do not have to fix what it finds. You just have to be able to see it.&lt;/p&gt;




&lt;p&gt;Issue: &lt;a href="https://github.com/PrefectHQ/fastmcp/issues/4773" rel="noopener noreferrer"&gt;PrefectHQ/fastmcp#4773&lt;/a&gt;&lt;br&gt;
PR: &lt;a href="https://github.com/PrefectHQ/fastmcp/pull/4774" rel="noopener noreferrer"&gt;PrefectHQ/fastmcp#4774&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Issue: &lt;a href="https://github.com/PrefectHQ/fastmcp/issues/4773" rel="noopener noreferrer"&gt;PrefectHQ/fastmcp#4773&lt;/a&gt; — includes the 11-line repro, no install needed&lt;/li&gt;
&lt;li&gt;Fix: &lt;a href="https://github.com/PrefectHQ/fastmcp/pull/4774" rel="noopener noreferrer"&gt;PrefectHQ/fastmcp#4774&lt;/a&gt; — merged, &lt;code&gt;+15 / -24&lt;/code&gt;, one file&lt;/li&gt;
&lt;li&gt;Diagnostic proposal I filed with the &lt;code&gt;ty&lt;/code&gt; team: &lt;a href="https://github.com/astral-sh/ty/issues/4204" rel="noopener noreferrer"&gt;astral-sh/ty#4204&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/PrefectHQ/fastmcp" rel="noopener noreferrer"&gt;fastmcp on GitHub&lt;/a&gt; · &lt;a href="https://pypi.org/project/fastmcp/" rel="noopener noreferrer"&gt;fastmcp on PyPI&lt;/a&gt; — check the version you actually have&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Next in this series:&lt;/strong&gt; &lt;a href="https://dev.to/yonyonai/elicitation-didnt-die-in-the-mcp-stateless-rewrite-its-the-only-one-that-survived-3o8"&gt;Elicitation didn't die in the MCP stateless rewrite. It's the only one that survived.&lt;/a&gt; — why the empty-schema trap and this type bug are the same bug, one layer apart.&lt;/p&gt;

</description>
      <category>python</category>
      <category>typing</category>
      <category>opensource</category>
      <category>mcp</category>
    </item>
    <item>
      <title>One operator. An army of minds.</title>
      <dc:creator>Yonyon</dc:creator>
      <pubDate>Sat, 18 Jul 2026 06:47:34 +0000</pubDate>
      <link>https://dev.to/yonyonai/one-operator-an-army-of-minds-3jm9</link>
      <guid>https://dev.to/yonyonai/one-operator-an-army-of-minds-3jm9</guid>
      <description>&lt;p&gt;One operator. An army of minds.&lt;/p&gt;

&lt;p&gt;63 sessions · 5,173 agents · 12,658 tool events — my actual Claude Code telemetry rendered as one comic splash.&lt;/p&gt;

&lt;p&gt;The numbers are real. The cape is aspirational.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>AI agents shouldn't grade their own homework</title>
      <dc:creator>Yonyon</dc:creator>
      <pubDate>Sun, 12 Jul 2026 09:18:52 +0000</pubDate>
      <link>https://dev.to/yonyonai/ai-agents-shouldnt-grade-their-own-homework-7go</link>
      <guid>https://dev.to/yonyonai/ai-agents-shouldnt-grade-their-own-homework-7go</guid>
      <description>&lt;p&gt;Your AI coding agent just said &lt;strong&gt;"Done ✅"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Who checked? The same agent that wrote the bug.&lt;/p&gt;

&lt;p&gt;This is the structural blind spot of agentic coding, and no amount of "please double-check your work" in the prompt fixes it. An LLM that just wrote the code is the &lt;em&gt;worst-positioned&lt;/em&gt; reviewer of it — it shares the exact context, assumptions, and blind spots that produced any bug. A self-graded "done" is a &lt;strong&gt;claim&lt;/strong&gt;, not &lt;strong&gt;evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So I stopped letting my agent grade its own homework. Here's the gate I built into &lt;a href="https://docs.claude.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;, and the portable pattern underneath it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claim → independent re-derivation → human ratification → automated trigger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four parts, each closing a specific failure mode. Nothing here is Claude-specific — it's a template for any agent that reports its own success.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Independence beats "check harder"
&lt;/h3&gt;

&lt;p&gt;The fix isn't a better prompt telling the agent to re-read its diff. It's an &lt;strong&gt;independence boundary&lt;/strong&gt;: a &lt;em&gt;fresh, adversarial&lt;/em&gt; subagent — explicitly told to &lt;em&gt;disprove&lt;/em&gt; the claim — re-verifies from scratch against &lt;strong&gt;live running state&lt;/strong&gt;. Not the diff. Not the plan the author already believed. The actual output of the actual system, right now.&lt;/p&gt;

&lt;p&gt;I ran this on a real refactor. The verifier re-ran the tests itself, re-checked the commits were pushed, grepped for dead code the main agent claimed to have removed, and returned a structured verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VERDICT: 6/6 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different context catches what the author is constitutionally blind to. A second &lt;em&gt;cheap&lt;/em&gt; pass beats the original agent trying harder — because "harder" still runs through the same blind spot.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A verdict is not a decision
&lt;/h3&gt;

&lt;p&gt;The machine verifies; the human ratifies. The verdict surfaces through a sign-off gate with three explicit paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;accept&lt;/strong&gt; — close it out&lt;/li&gt;
&lt;li&gt;🔍 &lt;strong&gt;show-evidence&lt;/strong&gt; — re-run a specific check live, in front of me, &lt;em&gt;first&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;reject&lt;/strong&gt; — treat as a bug report&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Show-evidence" is the one that matters. It turns the operator from a rubber-stamp on a green checkmark into someone the agent has to &lt;em&gt;defend the claim to&lt;/em&gt; with artifacts. In Claude Code this is the &lt;code&gt;AskUserQuestion&lt;/code&gt; tool; in your stack it's whatever forces a human to say yes on the record.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Automate the trigger, or it never fires
&lt;/h3&gt;

&lt;p&gt;A verification step you have to &lt;em&gt;remember&lt;/em&gt; to run is a verification step you skip under deadline. So the whole gate is triggered by a &lt;strong&gt;Stop hook&lt;/strong&gt; that fires after the agent edits files:&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="c1"&gt;# verify-done-nudge.py — a Claude Code Stop hook
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop_hook_active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="c1"&gt;# LOOP GUARD: this Stop follows our own block — let it through
&lt;/span&gt;
&lt;span class="c1"&gt;# ... only fires if the turn used Edit/Write/MultiEdit
# ... and stays silent if recent text already shows verify/tests/PASS/pushed
&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You edited files but haven&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t verified. Run /verify-done &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(independent re-check + sign-off) before declaring complete.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automate the &lt;strong&gt;trigger&lt;/strong&gt;, not the judgment. That's the difference between a gate and a good intention.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The engineering that makes it safe
&lt;/h3&gt;

&lt;p&gt;The credibility (and the interesting part) is in the guards. This hook runs on &lt;em&gt;every&lt;/em&gt; Stop event, so it has to be bulletproof:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Loop guard.&lt;/strong&gt; A Stop hook that re-invokes the agent could recurse forever. Claude Code sets &lt;code&gt;stop_hook_active = true&lt;/code&gt; on the Stop that follows your own block — check it and bail, so the gate fires &lt;em&gt;at most once&lt;/em&gt; per work-cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-open.&lt;/strong&gt; Any error — missing transcript, malformed JSON, anything — returns cleanly and lets the session end. A broken &lt;em&gt;verifier&lt;/em&gt; should never block you from finishing work. It degrades to the old behavior; it doesn't brick your session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No LLM in the hook.&lt;/strong&gt; Pure shell, no network, no model call — just a bounded tail-read of the transcript. A hook that calls an LLM bills silently on every turn and can time out. The hook decides &lt;em&gt;whether to nudge&lt;/em&gt;; the agent does the thinking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it fits in a workflow
&lt;/h2&gt;

&lt;p&gt;One subtlety if you orchestrate multi-agent workflows: the &lt;strong&gt;verify&lt;/strong&gt; phase can run inside a background workflow (fan out several adversarial verifiers in parallel, return a verdict), but the &lt;strong&gt;confirm&lt;/strong&gt; phase can't — an interactive sign-off has to happen on the main thread. So: the workflow &lt;em&gt;verifies&lt;/em&gt;, the main thread &lt;em&gt;confirms&lt;/em&gt; with the returned verdict. Keep the human ratification out of the background.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claim → independent re-derivation → human ratify → automated trigger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your agent can say "done," it can say it too soon. Give it a hostile second opinion, make a human ratify, and wire the trigger so the check can't be skipped when it's inconvenient.&lt;/p&gt;

&lt;p&gt;Separation of powers — applied to a single agent's workflow.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>claudecode</category>
      <category>devtools</category>
    </item>
    <item>
      <title>OrchestKit scored 83/100 on ORA — what "agent-readiness" actually measures</title>
      <dc:creator>Yonyon</dc:creator>
      <pubDate>Wed, 17 Jun 2026 06:43:34 +0000</pubDate>
      <link>https://dev.to/yonyonai/orchestkit-scored-83100-on-ora-what-agent-readiness-actually-measures-3pkj</link>
      <guid>https://dev.to/yonyonai/orchestkit-scored-83100-on-ora-what-agent-readiness-actually-measures-3pkj</guid>
      <description>&lt;p&gt;I ran &lt;a href="https://orchestkit.yonyon.ai" rel="noopener noreferrer"&gt;OrchestKit&lt;/a&gt; — my open-source Claude Code toolkit — through &lt;strong&gt;ORA&lt;/strong&gt;, an independent audit that scores how &lt;em&gt;agent-ready&lt;/em&gt; a product is: how easily an AI agent can discover you, identify itself, authenticate, and actually get work done against your surface.&lt;/p&gt;

&lt;p&gt;Result: &lt;strong&gt;83/100, Grade B ("Competitive"), ranked #49 of 12,200.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The breakdown
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth &amp;amp; Access&lt;/td&gt;
&lt;td&gt;28/30&lt;/td&gt;
&lt;td&gt;🟢 Strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent Integration&lt;/td&gt;
&lt;td&gt;18/20&lt;/td&gt;
&lt;td&gt;🟢 Strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;18/20&lt;/td&gt;
&lt;td&gt;🟢 Strong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Experience&lt;/td&gt;
&lt;td&gt;10/10&lt;/td&gt;
&lt;td&gt;🟢 Perfect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discovery&lt;/td&gt;
&lt;td&gt;9/20&lt;/td&gt;
&lt;td&gt;🟠 Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;p&gt;The foundation is strong: an agent can identify, connect, and use OrchestKit with near-zero friction — exactly what an agent-native toolkit should be.&lt;/p&gt;

&lt;p&gt;The one hole is &lt;strong&gt;Discovery&lt;/strong&gt; — presence, not product. The thing is ready; it just needs more people (and agents) to know it exists. Honestly the most fun problem to have, and it's the next focus.&lt;/p&gt;

&lt;p&gt;Solo-built. Open-source. Independently verified.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://orchestkit.yonyon.ai" rel="noopener noreferrer"&gt;https://orchestkit.yonyon.ai&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>mcp</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Making your docs site agent-readable: llms.txt, MCP, and the .well-known files that actually matter</title>
      <dc:creator>Yonyon</dc:creator>
      <pubDate>Sun, 14 Jun 2026 17:13:20 +0000</pubDate>
      <link>https://dev.to/yonyonai/making-your-docs-site-agent-readable-llmstxt-mcp-and-the-well-known-files-that-actually-matter-33c6</link>
      <guid>https://dev.to/yonyonai/making-your-docs-site-agent-readable-llmstxt-mcp-and-the-well-known-files-that-actually-matter-33c6</guid>
      <description>&lt;p&gt;AI agents increasingly read your docs &lt;em&gt;instead of&lt;/em&gt; a human. If your documentation site only emits HTML for a browser, an agent has to scrape and guess. There's a better surface — and most of it is a handful of small, standard files. Here's the full stack we ship on the OrchestKit docs site, why each piece exists, and how to verify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;llms.txt&lt;/code&gt; — the agent's table of contents
&lt;/h2&gt;

&lt;p&gt;A plain-text index at &lt;code&gt;/llms.txt&lt;/code&gt;: what the product is, its constraints, and a link map to every machine-readable resource. Keep it under ~30k chars; put the exhaustive page list in &lt;code&gt;/docs/llms.txt&lt;/code&gt; and the full corpus in &lt;code&gt;/llms-full.txt&lt;/code&gt;. The win: an agent gets oriented in one fetch instead of crawling.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Markdown content negotiation
&lt;/h2&gt;

&lt;p&gt;Append &lt;code&gt;.md&lt;/code&gt; to any page URL (or send &lt;code&gt;Accept: text/markdown&lt;/code&gt;) and return the raw Markdown. Agents get clean tokens; humans still get the rendered page.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. An OpenAPI spec for your read APIs
&lt;/h2&gt;

&lt;p&gt;Even a docs site has an API surface (search, page fetch). Publish an OpenAPI document at a predictable path so an agent can call it without reverse-engineering. Pair it with RFC 9727 — a &lt;code&gt;/.well-known/api-catalog&lt;/code&gt; linkset that enumerates every API entry point.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. An MCP server
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol lets agents call your tools natively. We expose a read-only MCP server over Streamable HTTP at &lt;code&gt;/api/mcp&lt;/code&gt; plus a discovery &lt;code&gt;server-card.json&lt;/code&gt;. Two tools — search docs, get a doc by id — are enough to be useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The &lt;code&gt;.well-known&lt;/code&gt; identity files
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;agent-card.json&lt;/code&gt; (A2A): declares your agent skills.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agent-skills/index.json&lt;/code&gt;: the Agent Skills Discovery RFC, with a SHA-256 digest per skill so a consumer can verify it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;oauth-protected-resource&lt;/code&gt; (RFC 9728): if your API is anonymous, &lt;em&gt;say so&lt;/em&gt; — an empty &lt;code&gt;authorization_servers&lt;/code&gt; is a positive signal, not an omission.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. JSON-LD that an entity graph can reconcile
&lt;/h2&gt;

&lt;p&gt;Emit a &lt;code&gt;schema.org&lt;/code&gt; graph (&lt;code&gt;Organization&lt;/code&gt;, &lt;code&gt;SoftwareApplication&lt;/code&gt;, &lt;code&gt;WebSite&lt;/code&gt;) linked by &lt;code&gt;@id&lt;/code&gt;, with &lt;code&gt;sameAs&lt;/code&gt; pointing at the registries that already verify you (GitHub, your package registry, Wikidata). One canonical Organization block, reused everywhere, so the graph never sees conflicting identifiers. Never fabricate an &lt;code&gt;aggregateRating&lt;/code&gt; — surface real signals (e.g. GitHub stars as an &lt;code&gt;InteractionCounter&lt;/code&gt;) instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Tell crawlers the truth in &lt;code&gt;robots.txt&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Explicitly allow the named AI crawlers you want (GPTBot, ClaudeBot, OAI-SearchBot, Google-Extended…), and emit a &lt;code&gt;Content-Signal&lt;/code&gt; directive. Link your sitemap and a schema-map.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;curl -s https://yoursite/llms.txt&lt;/code&gt;, fetch each &lt;code&gt;.well-known&lt;/code&gt; path, and run your JSON-LD through a structured-data validator. If you build on Claude Code, the open-source &lt;strong&gt;OrchestKit&lt;/strong&gt; docs site implements every item above — the source is on GitHub, MIT-licensed, and you can read the route handlers directly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I maintain OrchestKit (a free, MIT plugin for Claude Code, 111 skills/37 agents/210 hooks). The agent-discovery surface described here is what its docs site ships today.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I made my portfolio readable by AI agents. A scanner found four things that didn't exist</title>
      <dc:creator>Yonyon</dc:creator>
      <pubDate>Thu, 11 Jun 2026 16:42:20 +0000</pubDate>
      <link>https://dev.to/yonyonai/i-made-my-portfolio-readable-by-ai-agents-a-scanner-found-four-things-that-didnt-exist-456o</link>
      <guid>https://dev.to/yonyonai/i-made-my-portfolio-readable-by-ai-agents-a-scanner-found-four-things-that-didnt-exist-456o</guid>
      <description>&lt;p&gt;Last week I wrote about &lt;a href="https://dev.to/yonyonai/how-i-made-my-website-fully-agent-readable-an-mcp-server-nlweb-ask-in-nextjs-50m0"&gt;making my site fully agent-readable&lt;/a&gt; — an MCP server, an NLWeb &lt;code&gt;/ask&lt;/code&gt; endpoint, &lt;code&gt;llms.txt&lt;/code&gt;, a &lt;code&gt;.well-known/&lt;/code&gt; discovery tree. Shipping those was the easy half.&lt;/p&gt;

&lt;p&gt;Then I started scoring the site with an agent-readiness scanner — a prober that visits like a real agent: no JavaScript execution, spec-shaped requests, no goodwill. Every round it found something that &lt;em&gt;worked&lt;/em&gt; in my browser and &lt;strong&gt;did not exist&lt;/strong&gt; as far as an agent could tell.&lt;/p&gt;

&lt;p&gt;These are the four that stung. All real code from &lt;a href="https://yonyon.ai" rel="noopener noreferrer"&gt;yonyon.ai&lt;/a&gt;, all fixed this round.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The "helpful" GET that made my endpoint read as missing
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;/ask&lt;/code&gt; endpoint (Microsoft's &lt;a href="https://github.com/nlweb-ai/NLWeb" rel="noopener noreferrer"&gt;NLWeb&lt;/a&gt; shape) answered &lt;code&gt;POST { "query": "..." }&lt;/code&gt;. For &lt;code&gt;GET&lt;/code&gt;, I did what felt like good API manners — return a machine-readable descriptor telling the caller how to use the endpoint:&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;// GET /ask — the old version&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Response&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;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;_meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nlws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NLWEB_VERSION&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;POST { "query": "..." } for answers...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&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;Polite, self-documenting, useless. The NLWeb &lt;strong&gt;reference client&lt;/strong&gt; — and therefore the scanner — issues &lt;code&gt;GET /ask?query=...&lt;/code&gt; and expects an answer. It got my descriptor back, which is not an answer, so the endpoint graded as &lt;em&gt;not implemented&lt;/em&gt;. My documentation-instead-of-data response was indistinguishable from a stub.&lt;/p&gt;

&lt;p&gt;The fix: &lt;code&gt;GET&lt;/code&gt; with a query parameter runs the real pipeline (including SSE via &lt;code&gt;?streaming=true&lt;/code&gt;); a bare &lt;code&gt;GET&lt;/code&gt; keeps the descriptor.&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&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;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;req&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;hasQuery&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;query&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="s2"&gt;q&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="s2"&gt;question&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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="nf"&gt;trim&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;hasQuery&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;handleAsk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// same pipeline as POST, SSE supported&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// bare GET still teaches probing agents how to call it&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;Lesson:&lt;/strong&gt; agents don't read your descriptor and adapt — they call you the way &lt;em&gt;their&lt;/em&gt; reference client calls everyone. Implement the spec's client behavior, not your idea of good manners.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The WebMCP tools nobody could see
&lt;/h2&gt;

&lt;p&gt;I exposed in-browser tools via &lt;a href="https://webmachinelearning.github.io/webmcp/" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt; (&lt;code&gt;navigator.modelContext&lt;/code&gt;, Chrome early preview): &lt;code&gt;ask_yonatan&lt;/code&gt;, &lt;code&gt;browse_projects&lt;/code&gt;, &lt;code&gt;book_intro_call&lt;/code&gt;. The natural React implementation is a client component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;WebMcpTools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&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;provide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&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;provideContext&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;provide&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&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="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;provide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="cm"&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;Feature-detected, try/caught, tidy. Also &lt;strong&gt;invisible&lt;/strong&gt;. Crawlers and scanners detect WebMCP support by scanning the &lt;em&gt;unhydrated&lt;/em&gt; document — they don't run your bundle, and even executing browsers without the experimental API skip the registration entirely. A hydration-gated capability has no static trace. As far as any external observer was concerned, the site had no WebMCP support at all.&lt;/p&gt;

&lt;p&gt;The fix is almost embarrassing: a server component that emits the registration as an &lt;strong&gt;inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;&lt;/strong&gt;, so it's right there in the raw HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server component — no hydration involved&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WEBMCP_REGISTRATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="s2"&gt;`(function () {
  var mc = navigator.modelContext;
  if (!mc || typeof mc.provideContext !== "function") return;
  mc.provideContext({ tools: [ /* same tools, plain JS */ ] });
})();`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;WebMcpScript&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WEBMCP_REGISTRATION&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Next.js already requires &lt;code&gt;'unsafe-inline'&lt;/code&gt; in CSP, so this adds no new policy cost. If your CSP is stricter, use a nonce.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; a runtime-registered agent capability needs a statically visible twin. If it can't be seen in &lt;code&gt;curl&lt;/code&gt; output, it doesn't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The status page that 404'd in production only
&lt;/h2&gt;

&lt;p&gt;I added &lt;code&gt;/status&lt;/code&gt; — a tiny health page with content negotiation: JSON for &lt;code&gt;Accept: application/json&lt;/code&gt;, HTML for humans. It worked in dev. In production: &lt;strong&gt;404&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The culprit was &lt;code&gt;next-intl&lt;/code&gt;'s middleware. My site is internationalized, and the middleware matches every page-ish path and routes it into the &lt;code&gt;[locale]&lt;/code&gt; tree. I'd even left myself a comment claiming route handlers escape the middleware. They don't. Bare &lt;code&gt;/status&lt;/code&gt; was being locale-routed into &lt;code&gt;/[locale]/status&lt;/code&gt;, which doesn't exist.&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;// src/proxy.ts — the matcher had to exclude /status explicitly&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/((?!api|studio|trpc|_next|_vercel|status|.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;..*).*)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there was a second bug hiding behind the first: I'd marked the route &lt;code&gt;force-static&lt;/code&gt; for cheapness. Static means Next.js prerenders &lt;strong&gt;one&lt;/strong&gt; variant — with an empty &lt;code&gt;Accept&lt;/code&gt; header — and serves that frozen response to everyone. My own content negotiation could never run. It had to be &lt;code&gt;force-dynamic&lt;/code&gt; with &lt;code&gt;Vary: Accept&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; i18n middleware and agent endpoints fight over the same URL space, and the agent endpoints lose silently. Every bare path you promise agents (&lt;code&gt;/ask&lt;/code&gt;, &lt;code&gt;/mcp&lt;/code&gt;, &lt;code&gt;/status&lt;/code&gt;) needs an explicit carve-out — and content negotiation is incompatible with static prerendering by definition.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;ai-train=yes&lt;/code&gt;, on purpose
&lt;/h2&gt;

&lt;p&gt;Most advice about AI crawlers is defensive: block GPTBot, block CCBot, opt out of training. My robots.txt does the opposite, explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Content Signals (https://contentsignals.org). Discovery is a goal for this
# site, so AI training is allowed — being in training corpora is the point.
&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;-&lt;span class="n"&gt;Signal&lt;/span&gt;: &lt;span class="n"&gt;search&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;, &lt;span class="n"&gt;ai&lt;/span&gt;-&lt;span class="n"&gt;input&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;, &lt;span class="n"&gt;ai&lt;/span&gt;-&lt;span class="n"&gt;train&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a portfolio. Its entire job is to be &lt;em&gt;found&lt;/em&gt; when someone — increasingly, someone's agent — asks "who can build a production RAG system?" An assistant that learned about my work during training, or retrieves it at inference time, is doing my marketing. Blocking that to protect content whose value is being known would be self-defeating.&lt;/p&gt;

&lt;p&gt;That trade-off is deliberate and it isn't right for everyone — if your content &lt;em&gt;is&lt;/em&gt; the product, sell it, don't donate it. But make the choice consciously. The default-deny advice assumes your content's value is captured by humans reading it on your domain. For a personal site optimizing for discovery, the math runs the other way. (The same file blocks Bytespider and other bulk scrapers that offer no assistant or search surface — welcome the tier that cites you, refuse the tier that doesn't.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern under all four
&lt;/h2&gt;

&lt;p&gt;"Agent-readable" turned out to be two separate properties, and I kept confusing them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Works when called correctly&lt;/strong&gt; — the endpoint answers, the tool executes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discoverable by something that won't call you correctly&lt;/strong&gt; — no JS execution, spec-default request shapes, bare paths, raw HTML.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything above failed on the second property while passing the first. My browser hydrated the WebMCP component; the scanner read static HTML. My docs explained &lt;code&gt;POST /ask&lt;/code&gt;; the prober sent &lt;code&gt;GET&lt;/code&gt;. Dev served &lt;code&gt;/status&lt;/code&gt;; production's middleware ate it.&lt;/p&gt;

&lt;p&gt;The only thing that caught any of this was pointing an external, JS-free, spec-literal scanner at the production domain and treating its score as the truth. Your own browser is the worst possible test client for agent-readability — it's too forgiving.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The full implementation (MCP server, NLWeb endpoint, llms.txt, .well-known tree) is covered in &lt;a href="https://dev.to/yonyonai/how-i-made-my-website-fully-agent-readable-an-mcp-server-nlweb-ask-in-nextjs-50m0"&gt;part one&lt;/a&gt;. Try the live surfaces: &lt;code&gt;curl -X POST https://yonyon.ai/ask -d '{"query":"what does yonatan build?"}'&lt;/code&gt; or &lt;code&gt;GET https://yonyon.ai/status&lt;/code&gt; with &lt;code&gt;Accept: application/json&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>agents</category>
    </item>
    <item>
      <title>How I made my website fully agent-readable: an MCP server + NLWeb /ask in Next.js</title>
      <dc:creator>Yonyon</dc:creator>
      <pubDate>Tue, 09 Jun 2026 10:13:55 +0000</pubDate>
      <link>https://dev.to/yonyonai/how-i-made-my-website-fully-agent-readable-an-mcp-server-nlweb-ask-in-nextjs-50m0</link>
      <guid>https://dev.to/yonyonai/how-i-made-my-website-fully-agent-readable-an-mcp-server-nlweb-ask-in-nextjs-50m0</guid>
      <description>&lt;h1&gt;
  
  
  How I made my website fully agent-readable: an MCP server + NLWeb /ask in Next.js
&lt;/h1&gt;

&lt;p&gt;Most websites are built for humans with browsers. Increasingly, the visitor is an &lt;strong&gt;AI agent&lt;/strong&gt; — Claude, a custom assistant, some autonomous tool — and it doesn't want your hero animation. It wants structured, machine-readable answers and &lt;em&gt;actions it can take&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I rebuilt my own site, &lt;a href="https://yonyon.ai" rel="noopener noreferrer"&gt;yonyon.ai&lt;/a&gt;, to be readable and &lt;em&gt;usable&lt;/em&gt; by agents end to end: a live &lt;code&gt;/ask&lt;/code&gt; endpoint (Microsoft's NLWeb shape), a real MCP server at &lt;code&gt;/mcp&lt;/code&gt;, in-browser WebMCP tools, plus &lt;code&gt;llms.txt&lt;/code&gt; and a &lt;code&gt;.well-known/&lt;/code&gt; discovery tree. This post is the worked example — the three pieces that did the heavy lifting, with the actual code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: locale routing eats your agent endpoints
&lt;/h2&gt;

&lt;p&gt;My site is internationalized with &lt;code&gt;next-intl&lt;/code&gt;. Its middleware matches every path and routes it into a &lt;code&gt;[locale]&lt;/code&gt; segment. So the moment I added a bare &lt;code&gt;/ask&lt;/code&gt; route for agents, the i18n middleware grabbed it first and tried to render &lt;code&gt;/[locale]/ask&lt;/code&gt; — &lt;strong&gt;404&lt;/strong&gt;. Same problem would hit &lt;code&gt;/mcp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Agents expect &lt;em&gt;bare, unprefixed&lt;/em&gt; paths (&lt;code&gt;/ask&lt;/code&gt;, &lt;code&gt;/mcp&lt;/code&gt;) — not &lt;code&gt;/en/ask&lt;/code&gt;. The fix is to intercept those paths &lt;strong&gt;in the proxy/middleware, before the i18n matcher runs&lt;/strong&gt;, and rewrite them onto the real API route. &lt;code&gt;rewrite()&lt;/code&gt; (not &lt;code&gt;redirect()&lt;/code&gt;) preserves the HTTP method and body, so a &lt;code&gt;POST /ask&lt;/code&gt; stays a &lt;code&gt;POST&lt;/code&gt;:&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;// src/proxy.ts  (next-intl middleware entrypoint)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createMiddleware&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-intl/middleware&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;routing&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./i18n/routing&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;intlMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Rewrite the bare /ask onto the /api/ask route handler BEFORE next-intl&lt;/span&gt;
  &lt;span class="c1"&gt;// pulls it into the [locale] tree (which would 404). rewrite() keeps the&lt;/span&gt;
  &lt;span class="c1"&gt;// method + body, so POST /ask works.&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/ask&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rewrite&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/ask&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;intlMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Skip /api, /_next, static files, etc. — only run on page-ish paths.&lt;/span&gt;
  &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/((?!api|studio|trpc|_next|_vercel|.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;..*).*)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole alias trick: &lt;strong&gt;bare agent paths are matched first and rewritten into &lt;code&gt;/api/*&lt;/code&gt;&lt;/strong&gt;, so they never reach the locale router. Add a line per endpoint you want to expose unprefixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A dependency-free MCP server
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; is how agents discover and call your tools. The official SDK is great — but it peer-depends on &lt;strong&gt;zod 3&lt;/strong&gt;, and my repo is on &lt;strong&gt;zod 4&lt;/strong&gt;. Rather than fight the dependency tree, I hand-rolled the handler. MCP's Streamable-HTTP transport is just &lt;strong&gt;JSON-RPC 2.0 over POST&lt;/strong&gt;, and I only needed a handful of methods: &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;tools/call&lt;/code&gt;, &lt;code&gt;ping&lt;/code&gt;. Zero dependency risk:&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;// src/app/api/mcp/route.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;force-dynamic&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;SERVER_INFO&lt;/span&gt; &lt;span class="o"&gt;=&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="s2"&gt;yonyon.ai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&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;PROTOCOL_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2025-06-18&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;TOOLS&lt;/span&gt; &lt;span class="o"&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ask_yonatan&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="s2"&gt;Ask about Yonatan Gross's work, projects, or experience.&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="s2"&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;question&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="s2"&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;maxLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&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="s2"&gt;question&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;additionalProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// Behavioural hints so agents can reason about side effects:&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;destructiveHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;openWorldHint&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="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// browse_projects, book_intro_call ...&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;rpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rpcError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&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="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code&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="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&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;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&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="nx"&gt;params&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;initialize&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;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;protocolVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PROTOCOL_VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tools&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;serverInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SERVER_INFO&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;tools/list&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;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TOOLS&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;tools/call&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&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;TOOLS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;name&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;rpcError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32602&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Unknown tool: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&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="c1"&gt;// structured error, not soft text&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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;callTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&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;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;ping&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;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nf"&gt;rpcError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32601&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Method not found: &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;`&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 details that matter for agent ergonomics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool annotations&lt;/strong&gt; (&lt;code&gt;readOnlyHint&lt;/code&gt;, &lt;code&gt;destructiveHint&lt;/code&gt;, &lt;code&gt;openWorldHint&lt;/code&gt;) let a planner decide whether a tool is safe to call autonomously.&lt;/li&gt;
&lt;li&gt;An unknown tool returns a &lt;strong&gt;structured JSON-RPC error&lt;/strong&gt; (&lt;code&gt;code&lt;/code&gt; + &lt;code&gt;message&lt;/code&gt;), not a friendly text blob — so agents branch on it programmatically instead of string-matching.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Discovery: llms.txt + .well-known
&lt;/h2&gt;

&lt;p&gt;Endpoints are useless if nothing can find them. Two conventions cover it:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/llms.txt&lt;/code&gt; — a human- and agent-readable map of the site, modeled on &lt;code&gt;robots.txt&lt;/code&gt; but for LLMs: who you are, what an agent can do here, and the exact endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# yonyon.ai — Yonatan Gross

&amp;gt; AI Platform Engineer &amp;amp; Backend Developer. Builds production AI systems
&amp;gt; end-to-end: RAG pipelines, multi-agent architectures, MCP servers.

## What an agent can do here
- Ask about my work — POST /ask with {"query":"..."}  (NLWeb, no auth)
- Browse projects, book a free 15-min intro call
- MCP server at /mcp (initialize, tools/list, tools/call)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.well-known/&lt;/code&gt; tree carries the machine-readable specs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public/.well-known/
├── mcp.json                 # points agents at /mcp
├── mcp/server-card.json     # MCP server identity card
├── openapi.json             # OpenAPI for the HTTP endpoints
├── api-catalog              # RFC 9727 linkset of all APIs
└── agent-skills/            # discrete "skills" (ask, browse, book) as SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because these live in &lt;code&gt;public/&lt;/code&gt;, they're served as plain static files — bypassing the locale router entirely, so there's no dotpath/404 issue. I also emit RFC 8288 &lt;code&gt;Link&lt;/code&gt; headers (&lt;code&gt;rel="describedby"&lt;/code&gt; → llms.txt, &lt;code&gt;rel="service-desc"&lt;/code&gt; → openapi.json) so an agent that only fetches headers still discovers everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;You can verify it live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://yonyon.ai/ask &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"query":"What does Yonatan build?"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single-shot, no-auth, agent-facing endpoint returns a grounded answer from the same RAG-backed model that powers the site chat. An agent can then call the MCP &lt;code&gt;book_intro_call&lt;/code&gt; tool and act.&lt;/p&gt;

&lt;p&gt;Building agent-ready surfaces is also just good practice for the corpus AI models train on — the more structured and citable your presence, the more likely a model recalls your work. I package these patterns (and a lot more) as &lt;strong&gt;&lt;a href="https://github.com/yonatangross/orchestkit" rel="noopener noreferrer"&gt;OrchestKit&lt;/a&gt;&lt;/strong&gt;, my open-source Claude Code agent framework — 111 skills, 37 agents, 211 hooks.&lt;/p&gt;

&lt;p&gt;If you're making something agent-readable and want a second pair of eyes, the front door at &lt;strong&gt;&lt;a href="https://yonyon.ai" rel="noopener noreferrer"&gt;yonyon.ai&lt;/a&gt;&lt;/strong&gt; is itself the demo — ask it a question, or grab a free 15-minute intro call.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>mcp</category>
      <category>agents</category>
    </item>
    <item>
      <title>Building an Automated Content Pipeline That Posts to 6 Platforms</title>
      <dc:creator>Yonyon</dc:creator>
      <pubDate>Thu, 12 Mar 2026 10:32:21 +0000</pubDate>
      <link>https://dev.to/yonyonai/building-an-automated-content-pipeline-that-posts-to-6-platforms-h8a</link>
      <guid>https://dev.to/yonyonai/building-an-automated-content-pipeline-that-posts-to-6-platforms-h8a</guid>
      <description>&lt;p&gt;Every developer knows the pain: you write a great article, publish it on one platform, and then spend the next hour manually reformatting and posting it everywhere else.&lt;/p&gt;

&lt;p&gt;I built an automated content distribution pipeline that publishes to 6 platforms from a single source of truth. Here's how.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Write once, distribute everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hashnode&lt;/strong&gt; (source of truth)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to&lt;/strong&gt; (you're reading this here)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt; (article share + commentary)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threads&lt;/strong&gt; (text teaser)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instagram&lt;/strong&gt; (card image + caption)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;daily.dev&lt;/strong&gt; (auto via RSS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each platform gets a tailored variant — not a blind copy-paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Platform Adapter Pattern
&lt;/h2&gt;

&lt;p&gt;Each platform implements a common interface with a single &lt;code&gt;publish()&lt;/code&gt; method. Adding a new platform is just implementing one class. Rate limiting is Redis-backed per platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart Content Composition
&lt;/h2&gt;

&lt;p&gt;The hardest part isn't the API calls — it's making content feel native. Twitter gets a 280-char hook. Threads gets a 500-char teaser. LinkedIn gets a 3,000-char article share.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quality Ladder
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Ollama (local, free) generates the first draft&lt;/li&gt;
&lt;li&gt;Quality gate checks readability and boring-start patterns&lt;/li&gt;
&lt;li&gt;Claude Haiku polishes if needed (~$0.002)&lt;/li&gt;
&lt;li&gt;Template fallback as safety net&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Feedback Loop
&lt;/h2&gt;

&lt;p&gt;A weekly task evaluates performance, extracts patterns, and injects learnings into future prompts. The system gets smarter over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.13, FastAPI, SQLAlchemy 2 async, Celery&lt;/li&gt;
&lt;li&gt;Next.js 16, TypeScript, TanStack Query&lt;/li&gt;
&lt;li&gt;AI quality ladder (Ollama → Haiku), RAG, pgvector&lt;/li&gt;
&lt;li&gt;1Password secrets, Redis rate limiting&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This post was cross-posted using the pipeline described above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>api</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
