<?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: Todd Doane</title>
    <description>The latest articles on DEV Community by Todd Doane (@skibuff).</description>
    <link>https://dev.to/skibuff</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%2F2438797%2F390e0092-7538-4977-9390-fcb53dd32e9b.png</url>
      <title>DEV Community: Todd Doane</title>
      <link>https://dev.to/skibuff</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/skibuff"/>
    <language>en</language>
    <item>
      <title>A zero-dependency MCP server in one file: what the protocol actually needs</title>
      <dc:creator>Todd Doane</dc:creator>
      <pubDate>Sat, 31 Oct 2026 12:23:14 +0000</pubDate>
      <link>https://dev.to/skibuff/a-zero-dependency-mcp-server-in-one-file-what-the-protocol-actually-needs-khj</link>
      <guid>https://dev.to/skibuff/a-zero-dependency-mcp-server-in-one-file-what-the-protocol-actually-needs-khj</guid>
      <description>&lt;p&gt;We wanted two checks from our publish gate available inside any MCP client: "is every number in this draft in my facts file" and "does every link in it load". The official SDKs are fine, but for two tools we did not want a dependency tree, so we wrote the server against the protocol directly. It is one file, 125 lines, and it imports only &lt;code&gt;node:fs&lt;/code&gt; and &lt;code&gt;node:readline&lt;/code&gt;. We also got the error split wrong, twice, which makes it the most useful section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a stdio MCP server has to do
&lt;/h2&gt;

&lt;p&gt;For a tools-only server on the stdio transport, the job is small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read JSON-RPC 2.0 messages from stdin, one per line.&lt;/li&gt;
&lt;li&gt;Write one JSON reply per line to stdout. The spec says the server must not write anything to stdout that is not a valid MCP message; logs go to stderr. [src: &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/transports" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/specification/2025-06-18/basic/transports&lt;/a&gt;]&lt;/li&gt;
&lt;li&gt;Answer four methods: &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;tools/call&lt;/code&gt; and &lt;code&gt;ping&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Stay silent on notifications (messages without an &lt;code&gt;id&lt;/code&gt;), such as &lt;code&gt;notifications/initialized&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The read loop is &lt;code&gt;readline&lt;/code&gt; over stdin. This is the same logic as &lt;code&gt;server.js&lt;/code&gt;, restructured for reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createInterface&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;rl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;line&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&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;let&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// blank or bad line: ignore&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&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;undefined&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="c1"&gt;// notification: never reply&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&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="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&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="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&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;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&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="nf"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;reply&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="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32603&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&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="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 &lt;code&gt;handle&lt;/code&gt; is a switch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;switch &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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&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="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_VERSIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;protocolVersion&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;protocolVersion&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PROTOCOL_VERSIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;// ["2025-11-25", "2025-06-18"]&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="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;publish-gate&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;VERSION&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;case&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="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;case&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="cm"&gt;/* run the tool, see below */&lt;/span&gt;
  &lt;span class="k"&gt;case&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="p"&gt;{};&lt;/span&gt;
  &lt;span class="nl"&gt;default&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;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// notification: no reply&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`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;span class="na"&gt;code&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;initialize&lt;/code&gt; answers with the client's protocol version only if it is one of the revisions we checked the server against; otherwise it offers the newest of those. The lifecycle spec asks for exactly that: same version if supported, otherwise another version the server supports. [src: &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle&lt;/a&gt;] The first version echoed whatever the client sent, which told a client asking for a newer revision that the server followed rules it had never been checked against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of error, and the one we got wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Update 2026-11-01:&lt;/strong&gt; a reader in the comments pointed out that this section followed the 2025-06-18 revision, and the 2025-11-25 revision moved input validation errors to the other bucket (SEP-1303). [src: &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/changelog" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/specification/2025-11-25/changelog&lt;/a&gt;] They were right; the code and this section are corrected below.&lt;/p&gt;

&lt;p&gt;The MCP spec splits tool errors in two. Protocol errors are ordinary JSON-RPC errors, for an unknown tool or a request that does not fit the call schema. Tool execution errors are a normal result with &lt;code&gt;isError: true&lt;/code&gt;, for input validation errors and failures while the tool runs, so the model sees the message and can retry. Clients should pass tool execution errors to the model and may pass protocol errors. [src: &lt;a href="https://modelcontextprotocol.io/specification/2025-11-25/server/tools" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/specification/2025-11-25/server/tools&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Our first version put everything in the second bucket. On 2026-10-31 we moved both an unknown tool and a call with no draft to protocol errors, reading the older revision. The second is an input validation error, so a model might never have learned which argument was missing. Now only an unknown tool gets a coded error, rethrown so the read loop turns it into a JSON-RPC error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invalidParams&lt;/span&gt; &lt;span class="o"&gt;=&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="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="na"&gt;code&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="c1"&gt;// in callTool: reject bad calls before doing any work&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;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="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;invalidParams&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="k"&gt;if &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="nx"&gt;draft_text&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;draft_path&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;give draft_text or draft_path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// in handle, case "tools/call":&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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;params&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;params&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&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="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// protocol error&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&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;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="na"&gt;isError&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now an unknown tool gets &lt;code&gt;-32602&lt;/code&gt;, an unknown method gets &lt;code&gt;-32601&lt;/code&gt;, a call without a draft and an unreadable &lt;code&gt;draft_path&lt;/code&gt; file both come back as an &lt;code&gt;isError&lt;/code&gt; result starting with &lt;code&gt;error:&lt;/code&gt;, and anything else thrown becomes &lt;code&gt;-32603&lt;/code&gt;. A stdio test sends an unknown tool, a call without a draft, an unreadable &lt;code&gt;draft_path&lt;/code&gt; and an unknown method, and checks each reply; another checks the version answer for a known, an unknown and a missing &lt;code&gt;protocolVersion&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each tool returns plain text: &lt;code&gt;PASS&lt;/code&gt;, or &lt;code&gt;FAIL (n)&lt;/code&gt; followed by one line per finding. That is a design choice for a model reading the result, not something we measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replies can arrive out of order
&lt;/h2&gt;

&lt;p&gt;Handlers are &lt;code&gt;async&lt;/code&gt;, so a slow call can finish after a fast one. In a test session on 2026-10-31, request 4 (an unknown method) was answered before request 3 (a &lt;code&gt;tools/call&lt;/code&gt; to &lt;code&gt;check_numbers&lt;/code&gt;). That is allowed: clients match replies by &lt;code&gt;id&lt;/code&gt;, never by order. Do not "fix" it with a queue unless a tool really needs ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test it without a client
&lt;/h2&gt;

&lt;p&gt;You can drive the server from a shell, which is a quick way to see what a client sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"jsonrpc":"2.0","method":"notifications/initialized"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":2,"method":"tools/list"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | node server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For unit tests, the server exports its check functions (&lt;code&gt;checkNumbers&lt;/code&gt;, &lt;code&gt;checkLinks&lt;/code&gt;) and injects what touches the outside world: &lt;code&gt;checkLinks&lt;/code&gt; takes a &lt;code&gt;fetchFn&lt;/code&gt;, so &lt;code&gt;node --test&lt;/code&gt; runs without the network. The server has 6 tests and they pass on Node v26.7.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install it
&lt;/h2&gt;

&lt;p&gt;The server is not on npm yet. From a clone, in Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/skibuff22/agent-ops-kit-lite
claude mcp add publish-gate &lt;span class="nt"&gt;--&lt;/span&gt; node &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/agent-ops-kit-lite/mcp/publish-gate/server.js"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have tested it in Claude Code (&lt;code&gt;claude mcp list&lt;/code&gt; shows Connected); other stdio MCP clients should take the same setup (command &lt;code&gt;node&lt;/code&gt;, the path to &lt;code&gt;server.js&lt;/code&gt; as the argument), but we have not tested them. &lt;code&gt;check_numbers&lt;/code&gt; takes the draft and your &lt;code&gt;facts.md&lt;/code&gt; as text or paths; &lt;code&gt;check_links&lt;/code&gt; takes the draft. The source is in &lt;a href="https://github.com/skibuff22/agent-ops-kit-lite" rel="noopener noreferrer"&gt;agent-ops-kit-lite&lt;/a&gt; (MIT), next to the free Claude Code plugin it belongs to.&lt;/p&gt;

&lt;p&gt;These are two of the deterministic checks in our publish gate. The paid &lt;a href="https://calyx-ai.net/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=mcp-server#publish-gate" rel="noopener noreferrer"&gt;Publish Gate&lt;/a&gt; adds the rest: style and voice checks, an LLM claim-by-claim check that fails closed, a SHA-256 lock, and publishers that refuse a draft that changed after it passed. $19 one-time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by Calyx AI, a product line of Quality Tester Solutions LLC. Drafted with AI assistance and checked by the gate it describes; the code and the test session are from our own repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>node</category>
      <category>ai</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>A publish gate for AI-assisted posts: check every claim before it goes out</title>
      <dc:creator>Todd Doane</dc:creator>
      <pubDate>Mon, 26 Oct 2026 12:23:11 +0000</pubDate>
      <link>https://dev.to/skibuff/a-publish-gate-for-ai-assisted-posts-check-every-claim-before-it-goes-out-5b2a</link>
      <guid>https://dev.to/skibuff/a-publish-gate-for-ai-assisted-posts-check-every-claim-before-it-goes-out-5b2a</guid>
      <description>&lt;p&gt;If an AI agent drafts what you publish, the risk is not bad grammar. It is a confident number nobody measured, a feature that does not exist, or a paragraph of stock phrases with your name on it. We let our store's daily agent post under a real person's name, so we built a gate it cannot skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes wrong without one
&lt;/h2&gt;

&lt;p&gt;A language model writes what is plausible. Plausible is fine for a first draft and wrong for a public claim. The failures we wrote the gate against are ordinary ones: a round number that sounds right, a feature described the way it will work next month, a link to a checkout page with no tracking, and the familiar vocabulary that tells a reader nobody read the draft.&lt;/p&gt;

&lt;p&gt;Asking the agent to "be accurate" does not fix this. The agent that wrote the claim is the one judging it, in the same context, with the same blind spots. The check has to sit outside the writing step and it has to be able to say no.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate, in three layers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A facts file is the only source of truth
&lt;/h3&gt;

&lt;p&gt;Every claim and every number a post may use lives in &lt;code&gt;facts.md&lt;/code&gt;, each with its evidence: a file path, a command and its output, or a URL. If it is not in the file, it is not claimable. The file also has a "not claimable" section: customer counts, benchmarks, anything nobody has measured. Writing that section down is what keeps later posts honest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deterministic checks are the floor
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Numbers:&lt;/strong&gt; every number in the body must appear in &lt;code&gt;facts.md&lt;/code&gt;, or the line carries a source link. Years, times, versions and list numbering are exempt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phrases:&lt;/strong&gt; a built-in list of stock AI phrases plus your own "Banned phrases" list from &lt;code&gt;voice.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure:&lt;/strong&gt; no emoji, at most one exclamation mark, no rhetorical-question openers, a cap on em-dash density.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Links:&lt;/strong&gt; every link must resolve, and checkout links must carry &lt;code&gt;utm_source&lt;/code&gt; so you can see which post sent the buyer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These cost nothing to run and they never get tired. They catch the cheap mistakes before a model is involved.&lt;/p&gt;

&lt;h3&gt;
  
  
  An LLM checks each claim, and fails closed
&lt;/h3&gt;

&lt;p&gt;The last check sends the draft and the facts file to a model and asks for a verdict per claim: SUPPORTED, UNSUPPORTED or CONTRADICTED. Anything other than SUPPORTED fails the post. If the model cannot be reached, the gate fails too. A check that passes when it cannot run is not a check.&lt;/p&gt;

&lt;p&gt;A PASS writes a small sidecar file with the SHA-256 of the post body. Every publisher script calls one function before it posts:&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;side&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require_verified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# refuses if the sidecar is missing,
&lt;/span&gt;                                      &lt;span class="c1"&gt;# the hash differs, or the LLM step was skipped
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit one word after the PASS and the publisher refuses until the gate runs again. That rule stops an agent from tweaking a post after it passed. It does not stop an agent with write access from changing the publisher itself; that part is still code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it caught, and what it did not
&lt;/h2&gt;

&lt;p&gt;The gate checks content. Today it showed us the edge of that. Our Show HN post passed the gate, the publisher submitted it and printed PUBLISHED, and the account's submissions page was empty. Hacker News had answered with a notice that it is temporarily restricting Show HNs from new accounts. The publisher had trusted the redirect after submitting and, when it could not find the item, reported a generic URL as success.&lt;/p&gt;

&lt;p&gt;That is a publisher bug, not a content problem, and we handled it the way we handle any failure: find the cause, fix it, add the test, then recover. The publisher now reports success only when the item appears under the account's own submissions, saves HN's response page otherwise, and reports the restriction as declined. We did not retitle the post to get around the rule; the draft is parked and the rule is logged. A gate tells you a post matches your facts file and your voice guide. It cannot tell you a community will take it, and your publishers need their own proof that the post actually exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the pattern without buying anything
&lt;/h2&gt;

&lt;p&gt;Start with the cheapest part: a &lt;code&gt;facts.md&lt;/code&gt; with evidence per line and a "not claimable" section, plus a rule in your &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt; that nothing is published unless every number in it is in that file. Then add the hash lock: write the body's hash on approval and refuse to publish if it changed. Those two steps are the core of the pattern.&lt;/p&gt;

&lt;p&gt;The free &lt;a href="https://github.com/skibuff22/agent-ops-kit-lite" rel="noopener noreferrer"&gt;Agent Ops Kit Lite&lt;/a&gt; (MIT) has the &lt;code&gt;CLAUDE.md&lt;/code&gt; structure this runs inside.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I sell the finished version. Publish Gate v1 is &lt;code&gt;verify_post.py&lt;/code&gt; with configurable facts and voice files, any model CLI through &lt;code&gt;PUBLISH_GATE_LLM&lt;/code&gt; (default &lt;code&gt;claude -p&lt;/code&gt;), the &lt;code&gt;require_verified&lt;/code&gt; helper, an example dev.to publisher, templates, a directive for your agent and 8 tests: $19, or $29 bundled with the Agent Ops Kit, at &lt;a href="https://calyx-ai.net/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=publish-gate#publish-gate" rel="noopener noreferrer"&gt;calyx-ai.net&lt;/a&gt;. This article was drafted with AI assistance and passed the gate it describes. Originally published at &lt;a href="https://calyx-ai.net/blog/publish-gate-ai-posts/" rel="noopener noreferrer"&gt;calyx-ai.net&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>python</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The self-annealing loop: make every AI agent failure leave a test behind</title>
      <dc:creator>Todd Doane</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:26:37 +0000</pubDate>
      <link>https://dev.to/skibuff/the-self-annealing-loop-make-every-ai-agent-failure-leave-a-test-behind-4joh</link>
      <guid>https://dev.to/skibuff/the-self-annealing-loop-make-every-ai-agent-failure-leave-a-test-behind-4joh</guid>
      <description>&lt;p&gt;When an AI coding agent hits an error, the easiest move is to get past it: retry, reset, reprocess. That makes today's run green and guarantees the same failure next week. The self-annealing loop is a short rule that changes what the agent does after something breaks, so the system gets harder to break each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agents default to recovery
&lt;/h2&gt;

&lt;p&gt;Ask a coding agent to finish a job and you have told it, implicitly, that a finished job is the goal. When a step fails, the shortest path back to "finished" is recovery: run it again, clear the stuck queue, restart the service, add a retry. Each of those is reasonable in isolation. Together they hide the cause, and because the next session starts with a fresh context, nothing about the failure survives except the workaround.&lt;/p&gt;

&lt;p&gt;A human engineer does the same thing under pressure. The difference is that the human usually remembers. The agent does not, unless you give it somewhere to write the lesson down and a rule that says it must.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop, in order
&lt;/h2&gt;

&lt;p&gt;Annealing is the metallurgy term for heating and cooling a metal so its internal stresses relax and it ends up tougher. In an agent-run system the "heat" is a failure. The loop is five steps, and the order is the whole point:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Diagnose the root cause.&lt;/strong&gt; Keep asking "why" until you reach something that, once changed, prevents the failure from recurring. "The request timed out" is a symptom. "The health check treats any timeout as a store outage" is a cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix it&lt;/strong&gt; in code, config or infrastructure, not in the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the test&lt;/strong&gt; that would have caught it, so the fix cannot silently regress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the standard down.&lt;/strong&gt; Add a line to the directive's &lt;em&gt;Edge cases &amp;amp; learnings&lt;/em&gt; section, and a lint if the pattern can be detected mechanically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then recover:&lt;/strong&gt; reprocess the stuck items, restart, re-run.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Recovery still happens. It just happens last, after the cause can no longer produce the same failure. A rule worth adding: if the agent cannot finish the first four steps in the session, it says so and leaves the item open instead of quietly recovering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Put it where the agent reads it first
&lt;/h3&gt;

&lt;p&gt;The loop only works if it is part of the agent's standing instructions, not a prompt you remember to type. In a D.O.E. repo (Directive, Orchestration, Execution, described in &lt;a href="https://calyx-ai.net/blog/why-ai-coding-agent-breaks-prod/" rel="noopener noreferrer"&gt;the first article&lt;/a&gt;) it lives in &lt;code&gt;CLAUDE.md&lt;/code&gt;. A condensed example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## When something fails (self-annealing)&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Diagnose the root cause. A symptom is not a cause.
&lt;span class="p"&gt;2.&lt;/span&gt; Fix it in code/config/infra.
&lt;span class="p"&gt;3.&lt;/span&gt; Add the test that would have caught it.
&lt;span class="p"&gt;4.&lt;/span&gt; Add the standard to the directive's "Edge cases &amp;amp; learnings"
   (and a lint if it is mechanically detectable).
&lt;span class="p"&gt;5.&lt;/span&gt; Only then recover. Never recover first and call it fixed.
Record each pass in the anneal log: date, symptom, cause, fix, test.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The anneal log matters more than it looks. It is how you see that the same area keeps failing, which is usually the signal that the first fix treated a symptom.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example: three anneal passes in four days
&lt;/h2&gt;

&lt;p&gt;We run our store with a daily agent job on a small Linux box. Each day a script checks the storefront and the checkout endpoint, and if either returns nothing (HTTP status &lt;code&gt;000&lt;/code&gt;), it raises an ATTENTION note: the agent must diagnose and fix the cause before doing anything else.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pass 1.&lt;/strong&gt; ATTENTION: storefront unreachable. Root cause: the box had lost its own uplink (gateway and DNS failing); the store was fine. Left behind: a "host offline" check before raising ATTENTION, curl timeouts and retries, and a fix for a &lt;code&gt;git pull&lt;/code&gt; that failed on a dirty log file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass 2.&lt;/strong&gt; The metrics job failed with a name-resolution error. Root cause: flaky DNS on the same box. Left behind: retries with backoff on transport errors only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass 3.&lt;/strong&gt; ATTENTION again, storefront unreachable. Root cause: the LAN was clean but the WAN was dropping most packets, and the single offline probe happened to succeed, so the pass-1 check did not fire. Left behind: six probes instead of one; two or more failures is classified as "host network degraded", not a store outage. Plus a standard in the store-operations directive, and a note that this is a hardware problem for a human.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pass 1 was a real fix and still not the whole answer, which is normal. What the log gives you is the history: by pass 3 it showed the same alarm with the same kind of cause, which pointed at the check itself rather than at the store.&lt;/p&gt;

&lt;p&gt;The last pass also shows the loop's honest exit: some root causes are not code. When the cause is a flaky network link, the right standard is "classify it correctly and tell a human", not another retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check before you call a pass done
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Could you state the cause in one sentence that does not contain the words "sometimes" or "flaky"? If not, keep asking why.&lt;/li&gt;
&lt;li&gt;Does a test fail on the old code and pass on the new? If you cannot write one, write the lint or the directive line and say why.&lt;/li&gt;
&lt;li&gt;Is the learning in the file the next session reads first, not in a commit message or a chat transcript?&lt;/li&gt;
&lt;li&gt;Did recovery happen after the fix, and was it verified (an HTTP OK, a matching hash, a row count)?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Start with one directive
&lt;/h2&gt;

&lt;p&gt;You do not need a framework to try this. Add the block above to your &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;.cursorrules&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt;, and give your most failure-prone job a directive with an &lt;em&gt;Edge cases &amp;amp; learnings&lt;/em&gt; section. After a week, read that section. It is a list of failures that now have a test, a rule or a lint against them.&lt;/p&gt;

&lt;p&gt;The free &lt;a href="https://github.com/skibuff22/agent-ops-kit-lite" rel="noopener noreferrer"&gt;Agent Ops Kit Lite&lt;/a&gt; (MIT) has the &lt;code&gt;CLAUDE.md&lt;/code&gt; with this loop, the directive template and a secrets linter. It also installs as a Claude Code plugin: &lt;code&gt;/plugin marketplace add skibuff22/agent-ops-kit-lite&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://calyx-ai.net/blog/self-annealing-loop/" rel="noopener noreferrer"&gt;calyx-ai.net&lt;/a&gt;. Disclosure: I sell the full &lt;a href="https://calyx-ai.net/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=self-annealing-loop" rel="noopener noreferrer"&gt;Agent Ops Kit&lt;/a&gt; ($19 launch price through November 2, then $29), which adds the self-annealing, bug-fix and agent-governance directives, three example directives, seven execution scripts and multi-agent templates. The free lite kit above stands on its own. This article was drafted with AI assistance; the incident list is from our own run log.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why your AI coding agent keeps breaking prod (and the 3-layer fix)</title>
      <dc:creator>Todd Doane</dc:creator>
      <pubDate>Sun, 18 Oct 2026 12:08:23 +0000</pubDate>
      <link>https://dev.to/skibuff/why-your-ai-coding-agent-keeps-breaking-prod-and-the-3-layer-fix-fgj</link>
      <guid>https://dev.to/skibuff/why-your-ai-coding-agent-keeps-breaking-prod-and-the-3-layer-fix-fgj</guid>
      <description>&lt;p&gt;Claude Code, Cursor and Codex are excellent at single steps. Production work is not a single step. Here is the arithmetic of why agents fall apart on real tasks, and the structure we use to run a small company's platform with agents without babysitting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom: brilliant in the demo, flaky on Tuesday
&lt;/h2&gt;

&lt;p&gt;You ask the agent to deploy a fix. It edits the right file, writes a plausible commit message, runs the deploy, and then something small goes wrong: a CRLF line ending breaks a shell script on the server, an environment variable is read from the wrong file, a webhook key rotates on redeploy. The agent notices the failure, "fixes" it by retrying or resetting state, and reports success. Next week, in a fresh session, it does exactly the same thing.&lt;/p&gt;

&lt;p&gt;None of this is the model being dumb. It is the shape of the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The arithmetic: errors compound
&lt;/h2&gt;

&lt;p&gt;Suppose your agent gets each step right 90% of the time, which is generous for a step like "parse this API response and write the right rows". Chain steps together and the probabilities multiply:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Steps the model executes&lt;/th&gt;
&lt;th&gt;Success rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3 decisions, scripts do the rest&lt;/td&gt;
&lt;td&gt;0.9³ = &lt;strong&gt;73%&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 steps&lt;/td&gt;
&lt;td&gt;0.9⁵ = &lt;strong&gt;59%&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 steps&lt;/td&gt;
&lt;td&gt;0.9¹⁰ = &lt;strong&gt;35%&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Real tasks (fetch, parse, transform, validate, write, deploy, verify) are ten to fifty steps. If the model &lt;em&gt;executes&lt;/em&gt; every one of them by improvising code or commands in the moment, the success rate of the whole job collapses, and you end up watching it.&lt;/p&gt;

&lt;p&gt;The fix is not a better prompt. It is moving the steps that have a right answer out of the model entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3-layer fix: Directive, Orchestration, Execution
&lt;/h2&gt;

&lt;p&gt;We call it D.O.E. Each layer does one kind of work, and the boundaries between them are the point.&lt;/p&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;Nature&lt;/th&gt;
&lt;th&gt;Lives in&lt;/th&gt;
&lt;th&gt;Changed by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Directive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Intent, in prose: goal, inputs, tools, outputs, edge cases, how to verify&lt;/td&gt;
&lt;td&gt;&lt;code&gt;directives/*.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Humans write; agents append learnings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Orchestration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Judgment: which directive applies, which inputs, did it work&lt;/td&gt;
&lt;td&gt;The agent; &lt;code&gt;CLAUDE.md&lt;/code&gt; is its job description&lt;/td&gt;
&lt;td&gt;You, rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Determinism: fetch, parse, transform, deploy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;executions/*.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Agents write; tests guard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A directive never contains code. A script never contains policy ("ask the user if…"). The agent never contains either: it reads one and runs the other. Now the model makes three decisions instead of fifty executions, and decisions are the part a model is actually good at. A script that parses a response correctly once parses it correctly forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a directive looks like
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Directive: Deploy the API&lt;/span&gt;

&lt;span class="gu"&gt;## Goal&lt;/span&gt;
Ship the current branch to the API server and prove it's live.

&lt;span class="gu"&gt;## Execution tools&lt;/span&gt;
executions/deploy_service.py --target api   # normalizes CRLF/BOM, uploads, restarts

&lt;span class="gu"&gt;## Edge cases &amp;amp; learnings&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Shell scripts committed from Windows had CRLF endings and
  failed with "bad interpreter". deploy_service.py now normalizes them.
&lt;span class="p"&gt;-&lt;/span&gt; The webhook key rotates on redeploy; the script refreshes it.

&lt;span class="gu"&gt;## Verification&lt;/span&gt;
GET /health returns 200 and the commit hash matches HEAD.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;em&gt;Edge cases &amp;amp; learnings&lt;/em&gt; section is where the return on investment lives. Every gotcha the agent meets is written down where the next session reads it &lt;em&gt;before&lt;/em&gt; acting, instead of being rediscovered at 2 a.m.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that makes it get better: self-annealing
&lt;/h2&gt;

&lt;p&gt;Structure alone stops the bleeding. What makes month three dramatically better than week one is a rule about failure: &lt;strong&gt;recovery is not a fix&lt;/strong&gt;. When something breaks, the agent runs a fixed loop, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Diagnose the root cause&lt;/strong&gt;: keep asking "why" until you reach something that, once fixed, prevents recurrence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix it&lt;/strong&gt; in code, config or infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the test&lt;/strong&gt; that would have caught it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a standard&lt;/strong&gt; to the directive (and a lint, if the pattern is mechanically detectable).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then recover&lt;/strong&gt;: reprocess the stuck items, restart the service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most agent sessions do step 5 first and stop. "Let's just reprocess the stuck items" and "I'll add a cron to retry failures" feel like progress and leave the cause in place. Making recovery the &lt;em&gt;last&lt;/em&gt; step means every failure leaves behind a test, a rule and sometimes a lint, so whole classes of mistakes stop being possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  A real example from building our own store
&lt;/h3&gt;

&lt;p&gt;While building the checkout for our store, a secrets linter found a live Stripe key hard-coded in an old helper script in our own repo. Removing that one key would have been recovery. The fix was making &lt;code&gt;lint_secrets.py&lt;/code&gt; run before every commit and adding a line to &lt;code&gt;CLAUDE.md&lt;/code&gt; requiring it. That kind of mistake can no longer reach git in that repo, whoever (or whatever) is typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to start today, for free
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Put a &lt;code&gt;CLAUDE.md&lt;/code&gt; (or &lt;code&gt;.cursorrules&lt;/code&gt; / &lt;code&gt;AGENTS.md&lt;/code&gt;) at your repo root that states the three layers and the self-annealing loop.&lt;/li&gt;
&lt;li&gt;Pick the one job your agent fails at most often. Write a directive for it with the six sections above, and point it at a script.&lt;/li&gt;
&lt;li&gt;Move the deterministic parts of that job into the script. Give the script a test.&lt;/li&gt;
&lt;li&gt;Tell the agent: &lt;em&gt;"Read CLAUDE.md, then run the deploy directive."&lt;/em&gt; Watch what it appends to &lt;em&gt;Edge cases &amp;amp; learnings&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We publish a free starter, &lt;a href="https://github.com/skibuff22/agent-ops-kit-lite" rel="noopener noreferrer"&gt;Agent Ops Kit Lite on GitHub&lt;/a&gt; (MIT), with the &lt;code&gt;CLAUDE.md&lt;/code&gt;, the operating-principles directive, the directive template and the secrets linter. It is enough to try the approach on one job this afternoon.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://calyx-ai.net/blog/why-ai-coding-agent-breaks-prod/" rel="noopener noreferrer"&gt;calyx-ai.net&lt;/a&gt;. Disclosure: I sell the full &lt;a href="https://calyx-ai.net/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=why-agents-break-prod" rel="noopener noreferrer"&gt;Agent Ops Kit&lt;/a&gt; ($29) — the extra directives, tested execution scripts and multi-agent templates. The free lite kit above stands on its own. This article was drafted with AI assistance and reviewed against the framework we run in production.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>devops</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
