<?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: The Ops Log</title>
    <description>The latest articles on DEV Community by The Ops Log (@theopslog).</description>
    <link>https://dev.to/theopslog</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%2F4052196%2Fa4d892f7-3827-4de7-8010-e50849d1364e.png</url>
      <title>DEV Community: The Ops Log</title>
      <link>https://dev.to/theopslog</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/theopslog"/>
    <language>en</language>
    <item>
      <title>Where you host your MCP server decides whether it still works in three months</title>
      <dc:creator>The Ops Log</dc:creator>
      <pubDate>Thu, 30 Jul 2026 23:30:29 +0000</pubDate>
      <link>https://dev.to/theopslog/where-you-host-your-mcp-server-decides-whether-it-still-works-in-three-months-1hg1</link>
      <guid>https://dev.to/theopslog/where-you-host-your-mcp-server-decides-whether-it-still-works-in-three-months-1hg1</guid>
      <description>&lt;p&gt;I probed every remote MCP server listed in the official registry — 10,716 endpoints — and then asked a question the aggregate numbers hide: &lt;strong&gt;when a listing is dead, where is it hosted?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer is not evenly distributed. It is not close to evenly distributed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure rate by hosting platform
&lt;/h2&gt;

&lt;p&gt;Counting an endpoint as failing if it returns a hard 404, fails DNS, times out, refuses the connection, or 5xxs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host&lt;/th&gt;
&lt;th&gt;Endpoints&lt;/th&gt;
&lt;th&gt;Failing&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ngrok&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;smithery.ai&lt;/td&gt;
&lt;td&gt;217&lt;/td&gt;
&lt;td&gt;191&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;trycloudflare tunnels&lt;/td&gt;
&lt;td&gt;115&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;74%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;railway.app&lt;/td&gt;
&lt;td&gt;297&lt;/td&gt;
&lt;td&gt;197&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;66%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onrender.com&lt;/td&gt;
&lt;td&gt;164&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;52%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fly.dev&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;43%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;workers.dev&lt;/td&gt;
&lt;td&gt;327&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;vercel.app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;198&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An MCP server on Vercel is roughly &lt;strong&gt;eleven times&lt;/strong&gt; more likely to still answer than one behind a Cloudflare quick tunnel, and about &lt;strong&gt;ten times&lt;/strong&gt; more likely than one on Smithery.&lt;/p&gt;

&lt;p&gt;That is not a statement about engineering quality. It is a statement about what each of those things &lt;em&gt;is&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern is ephemerality, not quality
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;trycloudflare.com&lt;/code&gt; URLs come from quick tunnels — they are designed to be temporary and the hostname changes every time you restart. ngrok free tunnels are the same idea. Railway and Render free tiers sleep and can be reclaimed. A URL from any of these is a development convenience that someone pasted into a permanent public directory.&lt;/p&gt;

&lt;p&gt;Vercel and &lt;code&gt;workers.dev&lt;/code&gt; behave differently: the URL is stable, the free tier does not expire the hostname, and a deployment that stops receiving traffic still resolves.&lt;/p&gt;

&lt;p&gt;So the registry is not full of abandoned projects so much as &lt;strong&gt;projects whose front door was never permanent to begin with.&lt;/strong&gt; The code may be fine. The listing points at a door that has moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  It concentrates hard
&lt;/h2&gt;

&lt;p&gt;1,490 endpoints in the registry are dead in the strongest sense — hard 404 at the advertised path, or DNS that no longer resolves. Those spread across only 343 domains, and &lt;strong&gt;the top ten domains account for 67% of them&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;191  smithery.ai
173  railway.app
125  wishpool.app
109  mctx.ai
100  klymax402.com
 84  trycloudflare.com
 63  onrender.com
 63  apify.com
 62  workers.dev
 34  alpic.live
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Several of those are platforms that generate MCP endpoints in bulk. When one of them changes a URL scheme or expires a tier, hundreds of registry entries break at once. This is the failure mode of a directory that stores URLs rather than resolving them.&lt;/p&gt;

&lt;h2&gt;
  
  
  177 listings never had a real URL at all
&lt;/h2&gt;

&lt;p&gt;While bucketing failures I found entries whose URLs still contain unsubstituted template variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://{api_host}/mcp
https://{HAPI_FQDN}:{HAPI_PORT}/mcp
https://{ATLAS_MCP_URL}
https://{roster_host}/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;177 listings contain a placeholder, a &lt;code&gt;localhost&lt;/code&gt;, or an &lt;code&gt;example.com&lt;/code&gt;. 73 of them fail DNS for the obvious reason.&lt;/p&gt;

&lt;p&gt;The interesting subset is the other half: &lt;strong&gt;14 of these are up.&lt;/strong&gt; URLs like &lt;code&gt;https://mcp.cardog.io/mcp?api_key={api_key}&lt;/code&gt; work because the placeholder is in a query parameter the server ignores when absent. The author meant it as documentation — "put your key here" — and the registry stored it as a literal endpoint. Both readings are reasonable. Only one of them is a URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do with this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you are publishing an MCP server:&lt;/strong&gt; the hosting choice is a durability decision about your listing, not just about your app. A quick tunnel in a permanent directory has an expected life measured in hours. Use something with a stable hostname before you publish the URL somewhere you cannot easily update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are consuming the registry:&lt;/strong&gt; do not treat a listing as an endpoint. About a quarter of them will not serve you, the failures cluster by platform, and roughly one in sixty contains a placeholder someone forgot to fill in. Resolve before you depend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you maintain a directory of URLs:&lt;/strong&gt; this is the argument for periodic revalidation. A registry that never re-checks its entries converges on being a list of things that used to exist.&lt;/p&gt;

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

&lt;p&gt;Every active remote entry in &lt;code&gt;registry.modelcontextprotocol.io&lt;/code&gt;, deduplicated by URL — 10,716 endpoints. One anonymous JSON-RPC &lt;code&gt;initialize&lt;/code&gt; each, 10s timeout, classified by actual response. Transport-aware: the registry declares 9,647 &lt;code&gt;streamable-http&lt;/code&gt; and 1,068 legacy &lt;code&gt;sse&lt;/code&gt; remotes, and the legacy transport opens with a GET rather than a POST, so probing everything with one verb inflates the 405/404 count. I checked that specifically — it changed almost nothing, but I checked before publishing rather than after.&lt;/p&gt;

&lt;p&gt;Anonymous probing is a lower bound. A server that requires a key is counted as alive-but-gated, not dead, and I cannot see whether it is healthy behind the key.&lt;/p&gt;

&lt;p&gt;Counts are one moment in time. Re-running is the point; single snapshots of a moving system are anecdotes with decimal places.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previously: &lt;a href="https://dev.to/theopslog/i-checked-every-mcp-server-in-the-official-registry-about-1-in-10-is-broken-1ehj"&gt;I checked every MCP server in the official registry&lt;/a&gt; — the census this analysis is built on, including the correction where my first attempt covered 3% of the registry and I published it as 'every'.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>devops</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I checked every MCP server in the official registry. A quarter of them are unusable.</title>
      <dc:creator>The Ops Log</dc:creator>
      <pubDate>Thu, 30 Jul 2026 21:15:54 +0000</pubDate>
      <link>https://dev.to/theopslog/i-checked-every-mcp-server-in-the-official-registry-about-1-in-10-is-broken-1ehj</link>
      <guid>https://dev.to/theopslog/i-checked-every-mcp-server-in-the-official-registry-about-1-in-10-is-broken-1ehj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Correction, 30 July 2026 — please read this first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I first published this, the headline said I had checked &lt;em&gt;every&lt;/em&gt; MCP server in&lt;br&gt;
the registry, and reported ~10% broken. &lt;strong&gt;Both were wrong, and the mistake was mine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My collection script stopped after 12 pages. I never checked whether there were more.&lt;br&gt;
There were: the registry paginates to &lt;strong&gt;623 pages&lt;/strong&gt;, and the real population is&lt;br&gt;
&lt;strong&gt;10,716 unique remote endpoints&lt;/strong&gt;, not the 297 I measured. I sampled &lt;strong&gt;2.8%&lt;/strong&gt; of it&lt;br&gt;
and described that as the whole thing.&lt;/p&gt;

&lt;p&gt;I have since re-run it against all 10,716. The corrected numbers are below, and they&lt;br&gt;
are materially worse than my sample suggested — roughly &lt;strong&gt;a quarter&lt;/strong&gt; of registry&lt;br&gt;
endpoints are unusable, not a tenth. A small sample of a registry is biased toward&lt;br&gt;
the servers listed earliest, which are disproportionately the well-maintained ones.&lt;/p&gt;

&lt;p&gt;I am leaving the mistake visible rather than quietly editing the numbers, because an&lt;br&gt;
article about not trusting unverified figures has no business hiding its own.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is a number going around that roughly half of all remote MCP servers are dead.&lt;/p&gt;

&lt;p&gt;I had repeated it myself, in the README of a tool I published. I could not find where it came from, so I measured it.&lt;/p&gt;

&lt;p&gt;The answer, measured across all 10,716 of them, is that about &lt;strong&gt;one in four&lt;/strong&gt; is unusable — and only &lt;strong&gt;half&lt;/strong&gt; will talk to you without credentials. The number traces to an April 2026 analysis of 2,181 remote endpoints. Mine covers a different population three months later, and the gap is mostly that — not a mistake by whoever ran it.&lt;/p&gt;

&lt;p&gt;Here is the method and the full breakdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;On 29 July 2026 I pulled every entry from the &lt;a href="https://registry.modelcontextprotocol.io" rel="noopener noreferrer"&gt;official MCP registry&lt;/a&gt; — 1,200 servers. Of those, &lt;strong&gt;297 had &lt;code&gt;status: active&lt;/code&gt; and advertised a remote endpoint URL&lt;/strong&gt; (the rest are stdio/local packages with nothing to probe over the network).&lt;/p&gt;

&lt;p&gt;Each got one anonymous JSON-RPC &lt;code&gt;initialize&lt;/code&gt; over streamable HTTP, with a 10 second timeout:&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;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"initialize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"protocolVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-06-18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"clientInfo"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-uptime"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1.0"&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;Then I classified the response: a valid &lt;code&gt;result&lt;/code&gt; containing &lt;code&gt;protocolVersion&lt;/code&gt; or &lt;code&gt;serverInfo&lt;/code&gt; is up, 401/403 is auth-gated, and everything else got bucketed by its actual failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Corrected results — full census, n = 10,716
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Completed an anonymous MCP handshake&lt;/td&gt;
&lt;td&gt;5,346&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;49.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth-gated (401/403) — alive, wants a key&lt;/td&gt;
&lt;td&gt;2,643&lt;/td&gt;
&lt;td&gt;24.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Not found (404) — the advertised URL is wrong&lt;/td&gt;
&lt;td&gt;1,044&lt;/td&gt;
&lt;td&gt;9.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS failure&lt;/td&gt;
&lt;td&gt;446&lt;/td&gt;
&lt;td&gt;4.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server error (5xx)&lt;/td&gt;
&lt;td&gt;236&lt;/td&gt;
&lt;td&gt;2.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;160&lt;/td&gt;
&lt;td&gt;1.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment required (402)&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;1.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Method not allowed (405)&lt;/td&gt;
&lt;td&gt;146&lt;/td&gt;
&lt;td&gt;1.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS failure&lt;/td&gt;
&lt;td&gt;107&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything else (redirects, 429, 400, odd, rpc errors)&lt;/td&gt;
&lt;td&gt;438&lt;/td&gt;
&lt;td&gt;4.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Reachable in any form: ~75%. Unusable: 23-25%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two full runs an hour apart, one POST-only and one transport-aware (the registry declares 9,647 &lt;code&gt;streamable-http&lt;/code&gt; and 1,068 legacy &lt;code&gt;sse&lt;/code&gt; remotes, and the legacy transport opens with a GET), landed at 25.4% and 23.4%. The gap is run-to-run network variance, not method. I am quoting the range rather than picking the prettier number.&lt;/p&gt;

&lt;p&gt;The single largest failure mode is not a dead host — it is &lt;strong&gt;404, an endpoint listed&lt;br&gt;
at a URL that does not serve MCP&lt;/strong&gt;. 1,044 entries advertise a path nobody is answering.&lt;br&gt;
That is a registry-hygiene problem more than an uptime problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where "half are dead" comes from
&lt;/h2&gt;

&lt;p&gt;Look at the first two rows. &lt;strong&gt;55.2% of these endpoints will not complete an anonymous handshake&lt;/strong&gt; — and that is suspiciously close to the number people quote.&lt;/p&gt;

&lt;p&gt;But 134 of those 164 are returning a clean 401 or 403. They are running. They are answering. They want an API key, which is a completely reasonable thing for a hosted service to want.&lt;/p&gt;

&lt;p&gt;Whether the original analysis counted those as dead I genuinely don't know — I can't see its raw data. What I can say is that on the population I measured, treating auth-gated servers as down would inflate the failure rate about fivefold, and the two figures are not measuring the same thing: 2,181 endpoints found in the wild in April versus 297 registry-listed active servers in July. A curated registry should be healthier than a broad crawl, and three months is a long time in this ecosystem. If you want the honest one-liner: &lt;strong&gt;the registry is in better shape than the wider endpoint population was in April, and 'half of MCP is dead' does not describe what is in the registry today.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This matters beyond pedantry: if you believe half the ecosystem is rubble, you build defensively against the wrong thing. The actual failure distribution is a small tail of genuinely dead hosts — mostly &lt;strong&gt;DNS that no longer resolves&lt;/strong&gt;, which is the signature of an abandoned demo rather than a flaky service.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure the number doesn't capture
&lt;/h2&gt;

&lt;p&gt;Liveness is the easy question, and it is not the one that will hurt you.&lt;/p&gt;

&lt;p&gt;A server can return a perfect handshake and still break every agent that depends on it, because what agents actually consume is the &lt;strong&gt;tool contract&lt;/strong&gt;: names, descriptions, and input schemas. Rename a tool, add a required parameter, tighten an enum — the endpoint stays green and your agent fails at call time, mid-run, looking for all the world like a model error.&lt;/p&gt;

&lt;p&gt;Downtime is loud. Schema drift is silent, and it surfaces as "the AI is being weird today."&lt;/p&gt;

&lt;p&gt;So an uptime check that pings a URL is measuring the least interesting property available. What you want to watch is whether the tool inventory and schemas changed since the last time you looked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An anonymous handshake is a lower bound.&lt;/strong&gt; Auth-gated servers might also be broken behind their auth wall. I cannot see past it and am not claiming otherwise — 10.1% is the floor for "broken", not a ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One probe, one moment.&lt;/strong&gt; A server that was timing out at 14:00 UTC may be fine now. This is a snapshot, not an uptime percentage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registry-listed only.&lt;/strong&gt; Plenty of MCP servers are never registered; this says nothing about them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirects counted as not-usable.&lt;/strong&gt; Four servers returned 307/308. A tolerant client would follow them. I did not, because following redirects blindly is how an endpoint checker becomes an SSRF vector.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The sweep is about sixty lines: page through &lt;code&gt;/v0/servers&lt;/code&gt;, keep active entries with a remote URL, POST one &lt;code&gt;initialize&lt;/code&gt;, bucket the responses. If you run it and get materially different numbers, I would genuinely like to know.&lt;/p&gt;

&lt;p&gt;I also published the checker as a free tool — it does the handshake, the tool inventory, and the schema-drift comparison, as a &lt;a href="https://mcp-uptime.theopslog.workers.dev/mcp" rel="noopener noreferrer"&gt;live MCP server&lt;/a&gt; (&lt;code&gt;io.github.park11innyc-lgtm/mcp-uptime&lt;/code&gt; in the registry) and as an Apify Actor. No auth, no cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;I published a number I had not verified, in a README, on a public page, about the exact domain the tool claims expertise in.&lt;/p&gt;

&lt;p&gt;That is the same mistake I keep writing about from other angles: &lt;a href="https://dev.to/theopslog/published-is-not-deliverable-what-five-storefront-apis-dont-tell-you-412h"&gt;trusting a status field instead of the artifact&lt;/a&gt;, and &lt;a href="https://dev.to/theopslog/the-parts-of-building-an-mcp-server-that-the-tutorials-skip-3n07"&gt;the operational parts of MCP that tutorials skip&lt;/a&gt;. A widely-repeated statistic is a status field. It feels like knowledge and it is actually someone else's unverified claim, forwarded.&lt;/p&gt;

&lt;p&gt;Measuring it took under an hour.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>"Published" is not "deliverable": what five storefront APIs don't tell you</title>
      <dc:creator>The Ops Log</dc:creator>
      <pubDate>Wed, 29 Jul 2026 17:24:15 +0000</pubDate>
      <link>https://dev.to/theopslog/published-is-not-deliverable-what-five-storefront-apis-dont-tell-you-412h</link>
      <guid>https://dev.to/theopslog/published-is-not-deliverable-what-five-storefront-apis-dont-tell-you-412h</guid>
      <description>&lt;p&gt;I spent a week publishing digital products to five platforms — Etsy, Gumroad, Whop, Polar, and a Cloudflare Worker of my own — mostly through their APIs rather than their dashboards. I wanted the whole pipeline automated: build the file, create the listing, attach the download, set the price, go live.&lt;/p&gt;

&lt;p&gt;It worked. Every dashboard showed green. Every API returned &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two of the products would have taken a customer's money and delivered nothing.&lt;/p&gt;

&lt;p&gt;Here's what I learned about why, and what I check now instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure: a priced, published product with no file
&lt;/h2&gt;

&lt;p&gt;The first one I caught by accident. A Gumroad product sat in the products table marked &lt;strong&gt;Published&lt;/strong&gt;, with a price, a description, cover art, and a working checkout button. Its Content tab was empty. Nothing attached. A buyer would have paid $9.99 and received a download page with no download on it.&lt;/p&gt;

&lt;p&gt;Then I found the same thing on Whop — three products, all marked &lt;strong&gt;Visible&lt;/strong&gt;, all with an empty Content app.&lt;/p&gt;

&lt;p&gt;Neither platform considers this an error state. Nothing warns you. From every screen an operator normally looks at, these listings were finished.&lt;/p&gt;

&lt;p&gt;That's the trap, and it's structural rather than a bug: &lt;strong&gt;the listing and the payload are separate objects, and only the listing has a status.&lt;/strong&gt; "Published" is a fact about the listing. It says nothing about whether the thing being sold exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious check doesn't work
&lt;/h2&gt;

&lt;p&gt;My first instinct was to fetch the public product URL and check the response.&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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;https://store.example.com/l/my-product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;   &lt;span class="c1"&gt;# proves almost nothing
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern storefronts are client-rendered. The server returns the same HTML shell whether the product has ten files or none — the file list is populated later by JavaScript, if at all. I diffed the raw HTML of a known-good product against a known-broken one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;filenames found : NONE
extensions      : NONE
file_size values: NONE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identical. Both of them. The page HTML contains no file metadata at all, so scraping it can't distinguish a working product from an empty one. A &lt;code&gt;200&lt;/code&gt; here means the web server is up. That's the entire claim it supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "the API said OK" doesn't work either
&lt;/h2&gt;

&lt;p&gt;This is the part that genuinely surprised me.&lt;/p&gt;

&lt;p&gt;When I found the empty product, I re-uploaded the file. Gumroad's upload flow is a normal presign → S3 → complete sequence, and it worked — I got back a real, canonical file URL. Then I attached it to the existing product and got &lt;code&gt;200 OK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The file was not attached.&lt;/p&gt;

&lt;p&gt;So I tried the other plausible field names:&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="err"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[][&lt;/span&gt;&lt;span class="err"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;HTTP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;file_info&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;response:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;file_url&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="err"&gt;HTTP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;file_info&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;response:&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;files&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="err"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;HTTP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;file_info&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;response:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three return &lt;code&gt;200&lt;/code&gt;. None of them do anything. The API accepts unknown form parameters, ignores them silently, and reports success — so &lt;code&gt;200&lt;/code&gt; from this endpoint means "your request was syntactically fine," not "the thing you asked for happened."&lt;/p&gt;

&lt;p&gt;The actual conclusion was that &lt;strong&gt;there is no API path to attach a file to an existing product on this platform.&lt;/strong&gt; Upload works; attach doesn't. It has to be done by hand in the dashboard. I only discovered that by checking the result instead of trusting the response code — three times in a row the API told me I'd succeeded.&lt;/p&gt;

&lt;p&gt;If you take one thing from this post: an API that ignores unknown parameters cannot tell you your integration is correct. A &lt;code&gt;200&lt;/code&gt; from such an endpoint and a &lt;code&gt;200&lt;/code&gt; from a no-op are the same bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payloads decay
&lt;/h2&gt;

&lt;p&gt;I assumed this was a launch-day problem — verify once at publish time, move on.&lt;/p&gt;

&lt;p&gt;Then a product I had verified came back empty. Same field that reported &lt;code&gt;38.9 KB&lt;/code&gt; on day one returned &lt;code&gt;{}&lt;/code&gt; on day two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lash Tech Profit System      file_info={"Size": "38.5 KB"}
House Cleaning Tracker       file_info={"Size": "35.1 KB"}
Cleaning Profit System       file_info={}          &amp;lt;-- was 38.9 KB yesterday
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing on my side touched it. I still don't know the cause, and honestly the cause matters less than the correction: &lt;strong&gt;payload presence is a monitored property, not a launch checklist item.&lt;/strong&gt; A product that was correct yesterday is not evidence about today.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I check now
&lt;/h2&gt;

&lt;p&gt;One rule: &lt;strong&gt;verify the artifact, not the status.&lt;/strong&gt; Concretely, three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Find the field that describes the payload, not the listing.&lt;/strong&gt; On Gumroad that's &lt;code&gt;file_info&lt;/code&gt; from &lt;code&gt;GET /v2/products&lt;/code&gt;. It was the only signal in the entire surface — dashboard, public page, and update responses all failed to distinguish working from broken. Every platform has one somewhere. Find it before you launch, not during an incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Size-match against the local source.&lt;/strong&gt; Not "a file exists" — &lt;em&gt;the right file&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;reported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file_info&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;Size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;        &lt;span class="c1"&gt;# "38.9 KB"
&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&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="nf"&gt;getsize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local_zip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# 39884 bytes -&amp;gt; 38.9 KB
&lt;/span&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;reported&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;human_kb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload mismatch: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reported&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches the empty case and also the truncated upload and the stale-previous-version case, which a boolean "has a file" check sails straight past.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Re-check on a schedule, and alert on transitions.&lt;/strong&gt; Given payloads decay, going from &lt;em&gt;verified&lt;/em&gt; to &lt;em&gt;empty&lt;/em&gt; is the event worth an alert — arguably more urgent than a failed deploy, because it's silent and it's pointed directly at your customers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;Every one of these failures has the same structure: &lt;strong&gt;I accepted a proxy for the thing I actually cared about.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I cared whether a customer could download a file. I checked a status column, an HTTP code, and an API response — three proxies, each one cheap, each one wrong in a different way. The status column described a different object. The HTTP code described the web server. The API response described request parsing.&lt;/p&gt;

&lt;p&gt;The check that worked was the one that asked about the artifact directly, and compared it to something I independently knew to be true.&lt;/p&gt;

&lt;p&gt;That generalizes well beyond storefronts. Anywhere a system reports on its own success, it's worth asking what object that report is actually about — and whether anything in the chain ever touched the bytes you care about.&lt;/p&gt;

&lt;p&gt;Sales so far: zero. But at least nobody has paid me for an empty file.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Related: &lt;a href="https://dev.to/theopslog/the-parts-of-building-an-mcp-server-that-the-tutorials-skip-3n07"&gt;The parts of building an MCP server that the tutorials skip&lt;/a&gt; — the same "verify the artifact, not the status" habit, applied to MCP tool contracts.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>testing</category>
      <category>python</category>
    </item>
    <item>
      <title>The parts of building an MCP server that the tutorials skip</title>
      <dc:creator>The Ops Log</dc:creator>
      <pubDate>Wed, 29 Jul 2026 02:08:23 +0000</pubDate>
      <link>https://dev.to/theopslog/the-parts-of-building-an-mcp-server-that-the-tutorials-skip-3n07</link>
      <guid>https://dev.to/theopslog/the-parts-of-building-an-mcp-server-that-the-tutorials-skip-3n07</guid>
      <description>&lt;p&gt;Building your first &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; server takes about twenty minutes. The official quickstart is genuinely good: &lt;code&gt;npm install&lt;/code&gt;, register a tool, connect a stdio transport, and Claude or Cursor can call your code. Hello, world.&lt;/p&gt;

&lt;p&gt;Then you try to make it something other people can actually use, and you fall off a cliff.&lt;/p&gt;

&lt;p&gt;I know the cliff is real because someone measured it. An April 2026 scan of 2,181 remote MCP endpoints found &lt;strong&gt;52% of them completely dead&lt;/strong&gt;, and only about 9% fully healthy. These aren't abandoned toys — they're servers people shipped and expected to work. They didn't die from protocol bugs. The protocol is the easy part. They died from everything &lt;em&gt;around&lt;/em&gt; the protocol, which is exactly what the tutorials skip.&lt;/p&gt;

&lt;p&gt;Here are the parts that actually matter, and how I handle each. There's a small MIT-licensed starter kit at the bottom that ships all of it, but the ideas are portable to any stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. On stdio, &lt;code&gt;stdout&lt;/code&gt; is not yours
&lt;/h2&gt;

&lt;p&gt;The first one bites everybody. On the stdio transport, &lt;strong&gt;stdout is the JSON-RPC channel&lt;/strong&gt;. A single stray &lt;code&gt;console.log&lt;/code&gt; — yours, or a dependency's — injects a line into the protocol stream, and the client dies with a cryptic JSON parse error that points nowhere near the actual &lt;code&gt;console.log&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix is one word: log to &lt;code&gt;stderr&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;// stdout is the protocol channel on stdio. Log to stderr ONLY.&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SERVER_INFO&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; v&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="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ready on stdio`&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 it. But you have to &lt;em&gt;know&lt;/em&gt; it, and no quickstart tells you, because the quickstart never logs anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Failures have to be legible, or your agent is flying blind
&lt;/h2&gt;

&lt;p&gt;When a tool throws, the default experience is terrible: the agent sees &lt;code&gt;internal error&lt;/code&gt; and cannot tell an auth failure from a bad argument from an upstream 500. It can't decide whether to retry, fix its input, or give up — so it often retries a doomed call in a loop and burns your API budget overnight. That "quiet retry" is one of the most-cited ways agents rack up cost in production.&lt;/p&gt;

&lt;p&gt;The fix is a typed error that carries a machine-readable code, and a wrapper that turns a throw into a proper MCP error result instead of a dropped connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToolError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&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;ToolErrorCode&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&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="c1"&gt;// every handler funnels through this:&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;toToolResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;ToolError&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;internal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&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="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;`[&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="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;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="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 the agent sees &lt;code&gt;[forbidden_host] Host "x" is not in ALLOWED_FETCH_HOSTS&lt;/code&gt; and can actually reason about it. Legibility is a feature you build for the &lt;em&gt;model&lt;/em&gt;, not just the human reading logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Any tool that fetches a URL is an open proxy until you say otherwise
&lt;/h2&gt;

&lt;p&gt;The moment you write a tool that takes a URL and fetches it, you've built a potential SSRF proxy: an agent (or a prompt-injected one) can point it at &lt;code&gt;http://169.254.169.254/&lt;/code&gt; or your internal admin panel and read the response. A fetch tool without a host allowlist is a security incident waiting for a trigger.&lt;/p&gt;

&lt;p&gt;So the example fetch tool refuses anything it wasn't told to allow, plus enforces https and a hard timeout:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid_input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;allowedHosts&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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forbidden_host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is not allowlisted (SSRF guard).`&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;controller&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;AbortController&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;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// no unbounded hangs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three guards, none of which the "here's a tool that calls an API" tutorial includes.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Auth is the wall most remote servers never get over
&lt;/h2&gt;

&lt;p&gt;Survey data from 2026 is blunt: OAuth is the single biggest blocker for production MCP servers, over half of remote servers fall back to static keys, and OAuth failures tend to be &lt;em&gt;silent&lt;/em&gt; — the hardest kind to debug.&lt;/p&gt;

&lt;p&gt;You don't need full OAuth to get remote-safe. You need auth that &lt;strong&gt;fails closed&lt;/strong&gt; and tells you why. Bearer tokens, done properly, are the right first step:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// An HTTP MCP server with auth off is the default that gets scraped. Refuse to run open.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No MCP_BEARER_TOKENS configured; refusing all requests.&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="c1"&gt;// ...constant-time compare, real 401 + WWW-Authenticate header, never log the token.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important word is &lt;em&gt;closed&lt;/em&gt;. If you forget to configure tokens, the server rejects everything rather than quietly serving your tools to the internet. Full delegated OAuth 2.1 — per-user scopes, token exchange, refresh — is a real, larger job for public multi-tenant servers; the mistake is pretending a hand-rolled flow is that, or shipping with auth off "for now."&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Statelessness is the cold-start fix
&lt;/h2&gt;

&lt;p&gt;Here's the subtle one behind a lot of those 52%-dead endpoints. MCP's streamable HTTP transport can hold session state in process memory. Deploy that to anything that scales to zero or spreads load across instances — Lambda, Cloud Run, Fly, Workers — and the follow-up request lands on a &lt;strong&gt;cold instance that never saw the session&lt;/strong&gt;. The client hangs. No error, no log, just dead.&lt;/p&gt;

&lt;p&gt;The cheap fix is to not hold session state at all: build a fresh server and transport per request.&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bearerAuth&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildServer&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;transport&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;StreamableHTTPServerTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;sessionIdGenerator&lt;/span&gt;&lt;span class="p"&gt;:&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;// stateless&lt;/span&gt;
  &lt;span class="nx"&gt;res&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;close&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleRequest&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;res&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;body&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;If you genuinely need cross-request state later, back it with Redis or a durable object keyed by session id — never a module variable. But start stateless. It's the shape that survives the platform you'll actually deploy on.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Test the failure paths, or you haven't tested
&lt;/h2&gt;

&lt;p&gt;The last habit is the cheapest: when people do write tests for MCP tools, they test the demo — call the tool, assert the happy result. But every section above describes a &lt;em&gt;guard&lt;/em&gt;, and a guard you haven't tested is a guard you don't have. The tests that earn their keep assert that the server &lt;strong&gt;refuses&lt;/strong&gt; correctly:&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejects a host that is not allowlisted (SSRF guard)&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="o"&gt;=&amp;gt;&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;httpGetJson&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&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;https://evil.example.com/x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMatchObject&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forbidden_host&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejects non-https URLs&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="o"&gt;=&amp;gt;&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;httpGetJson&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&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;http://api.github.com/x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMatchObject&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid_input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's being asserted: not just that it throws, but that it throws with the &lt;em&gt;right machine-readable code&lt;/em&gt; — because that code is the contract from section 2, the thing the agent reasons about. If a refactor ever turns &lt;code&gt;forbidden_host&lt;/code&gt; into a generic &lt;code&gt;internal&lt;/code&gt;, the type checker won't care, the happy-path test won't care, and your agent quietly loses the ability to tell "blocked by policy" from "server bug." This test is the only thing standing there.&lt;/p&gt;

&lt;p&gt;Same principle for auth: the test worth writing is the one where &lt;strong&gt;no token is configured&lt;/strong&gt; and the server refuses everything. Fail-closed is a behavior; pin it.&lt;/p&gt;

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

&lt;p&gt;None of this is hard once you've named it. That's the whole point: the failure mode isn't difficulty, it's &lt;em&gt;invisibility&lt;/em&gt; — every one of these is missing from the tutorials, so everyone rediscovers them the same way, in production, from a hanging client.&lt;/p&gt;

&lt;p&gt;I packaged all six into a small, MIT-licensed TypeScript starter — stdio + streamable HTTP, the fail-closed bearer auth, the legible &lt;code&gt;ToolError&lt;/code&gt;, the SSRF-guarded example tool, the refusal tests, and a &lt;code&gt;DEPLOYMENT.md&lt;/code&gt; that walks the cold-start problem. It builds, it's tested, and it's meant to be read top to bottom in one sitting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/park11innyc-lgtm/mcp-server-starter-kit" rel="noopener noreferrer"&gt;https://github.com/park11innyc-lgtm/mcp-server-starter-kit&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Clone it, delete what you don't need, and skip the cliff.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Related: &lt;a href="https://dev.to/theopslog/published-is-not-deliverable-what-five-storefront-apis-dont-tell-you-412h"&gt;"Published" is not "deliverable": what five storefront APIs don't tell you&lt;/a&gt; — the same habit applied to shipping digital products: verify the artifact, never the status field.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;P.S. — You may have noticed every lesson above is really an&lt;/em&gt; operations &lt;em&gt;lesson wearing a developer costume: retries, budgets, legible failures, knowing when a thing is dead. If you live on the ops side of that line — the inbox, the CRM, the glue work — I applied the same production posture to a non-developer problem: an &lt;a href="https://buy.polar.sh/polar_cl_5gdnXSqbpJUC4AK75DIpEWfN47YYMoYrrGI2f18DnaZ" rel="noopener noreferrer"&gt;inbox-triage → CRM automation system for n8n&lt;/a&gt;, with the dedup, cost caps, and plain-English runbook that free workflow templates skip.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
