<?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: RSS Atlas</title>
    <description>The latest articles on DEV Community by RSS Atlas (@rss-atlas).</description>
    <link>https://dev.to/rss-atlas</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%2F4076571%2Fde6cdee0-580a-44f1-985e-dc6c98737c0e.png</url>
      <title>DEV Community: RSS Atlas</title>
      <link>https://dev.to/rss-atlas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rss-atlas"/>
    <language>en</language>
    <item>
      <title>Letting an AI ask 'what shipped in this field this week?'</title>
      <dc:creator>RSS Atlas</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:48:03 +0000</pubDate>
      <link>https://dev.to/rss-atlas/letting-an-ai-ask-what-shipped-in-this-field-this-week-1nk0</link>
      <guid>https://dev.to/rss-atlas/letting-an-ai-ask-what-shipped-in-this-field-this-week-1nk0</guid>
      <description>&lt;p&gt;Ask an assistant about recent developments and you get one of two things: whatever was in its training data, or the top few web search results. What you cannot ask is the thing you actually want — &lt;em&gt;"what came out in Rust tooling in the last two weeks"&lt;/em&gt; — scoped to a field, across the sources that cover it.&lt;/p&gt;

&lt;p&gt;Those sources exist. They are RSS feeds. They are just not reachable from an AI tool.&lt;/p&gt;

&lt;p&gt;So I built a directory of them and gave it an MCP server. This post is about four decisions that only became obvious after shipping, not a tour of the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;2,907 RSS feeds across 19 languages and 37 categories, holding 51,483 articles. An MCP server exposes it to AI tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http rss-atlas https://mcp.rssatlas.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other clients take it in &lt;code&gt;settings.json&lt;/code&gt;:&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;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rss-atlas"&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;"httpUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.rssatlas.com/mcp"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs on Cloudflare Workers (site and MCP server), Supabase (Postgres), and Cloud Run Jobs (crawler). There is a no-auth demo at &lt;code&gt;https://mcp.rssatlas.com/demo&lt;/code&gt; if you want to poke at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Four tools, and adding a parameter instead of a fifth
&lt;/h2&gt;

&lt;p&gt;The server exposes exactly four tools: &lt;code&gt;search_feeds&lt;/code&gt;, &lt;code&gt;get_feed&lt;/code&gt;, &lt;code&gt;get_articles&lt;/code&gt;, &lt;code&gt;get_rankings&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Building "fetch articles from only my favourited feeds" wanted to be a fifth tool — &lt;code&gt;get_favorite_articles&lt;/code&gt;. It became a parameter instead:&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;favorites_only&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Restrict results to the signed-in user's favorites&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is that every added tool is another choice the model has to get right. With &lt;code&gt;get_articles&lt;/code&gt; and &lt;code&gt;get_favorite_articles&lt;/code&gt; side by side, the model decides between them on every call. As a parameter it is not a decision at all — it is an argument. One less branch.&lt;/p&gt;

&lt;p&gt;The rule now is: before adding a tool, check whether an existing tool's parameters can carry it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Auth failures must be HTTP 401, not a 200 with an error
&lt;/h2&gt;

&lt;p&gt;This one cost the most time.&lt;/p&gt;

&lt;p&gt;MCP lets a tool return &lt;code&gt;isError: true&lt;/code&gt; in its result. I used that for unauthenticated requests at first — HTTP 200, error in the payload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clients do not show a sign-in prompt for that.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They decide "authentication needed" from &lt;strong&gt;HTTP 401 plus a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header&lt;/strong&gt;. A 200 means the call succeeded as far as the transport is concerned, so whatever is inside gives the client no reason to start an auth flow. To the user it looks like a tool that throws a confusing error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;401&lt;/span&gt; &lt;span class="ne"&gt;Unauthorized&lt;/span&gt;
&lt;span class="na"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer resource_metadata="https://mcp.rssatlas.com/.well-known/oauth-protected-resource"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The authorization server is discovered from that header. So: &lt;strong&gt;authentication is expressed at the HTTP layer, not in tool results.&lt;/strong&gt; Ordinary failures inside a tool call — no matches, bad argument — are still fine as &lt;code&gt;isError&lt;/code&gt;. Do not mix the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Take the user id out of the arguments
&lt;/h2&gt;

&lt;p&gt;For anything scoped to a user, the user id is not a tool parameter. The only identity used is &lt;code&gt;sub&lt;/code&gt; from the verified access token:&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;favorites_only&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;favorites_only requires a signed-in user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation would have caught a wrong id. But with MCP &lt;strong&gt;the model composes the arguments&lt;/strong&gt;, so an argument that can name another user is a shape that invites the mistake. If the field does not exist, the class of bug does not exist.&lt;/p&gt;

&lt;p&gt;Structure beats validation for anything that must never be passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Workers Rate Limiting binding does not block
&lt;/h2&gt;

&lt;p&gt;Cloudflare Workers has a Rate Limiting binding. I used it for the MCP server's limits.&lt;/p&gt;

&lt;p&gt;It did not work — more precisely, it did not &lt;em&gt;block&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The docs say plainly that it is not intended for precise counting. It is distributed, counts do not settle instantly across edges, and near the threshold requests get through. It is for shaping rough traffic, not for reliably stopping the 31st call.&lt;/p&gt;

&lt;p&gt;Rewrote it on a Durable Object, which serialises to a single instance and therefore counts exactly. Measured afterwards: the 31st request returns 429 for authenticated users, the 11th for the demo.&lt;/p&gt;

&lt;p&gt;The habit that came out of this: &lt;strong&gt;after implementing a rate limit, hammer past the threshold and watch it actually refuse.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  No AI on the site itself
&lt;/h2&gt;

&lt;p&gt;Worth stating because it drove several decisions: there is no summarisation and no AI classification in the product. Classification is rules and thresholds. Summarising happens in your own LLM.&lt;/p&gt;

&lt;p&gt;The reason is that the reader is already paying for a model. Calling one server-side means they pay twice, indirectly. Over MCP the summary belongs on their side anyway — faster, cheaper, and their choice of model.&lt;/p&gt;

&lt;p&gt;For the same reason feed content is capped at &lt;strong&gt;title + 500-character excerpt + link&lt;/strong&gt;. Nothing is stored in full. You read it at the source, which is what RSS was for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: 500 healthy feeds silently stopped updating
&lt;/h2&gt;

&lt;p&gt;Not MCP, but it ate a comparable amount of time.&lt;/p&gt;

&lt;p&gt;Conditional GET plus redirects: &lt;code&gt;/feed&lt;/code&gt; → &lt;strong&gt;301&lt;/strong&gt; → &lt;code&gt;/feed/&lt;/code&gt; → &lt;strong&gt;304 Not Modified&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;304 is a 3xx. Treat "3xx means redirect" literally and you go looking for a &lt;code&gt;Location&lt;/code&gt; header, which a 304 does not have, and you error out.&lt;/p&gt;

&lt;p&gt;The nasty part is that &lt;strong&gt;only feeds that once succeeded well enough to store an ETag can hit this&lt;/strong&gt; — so your best-behaved sources break first, and because the listing still renders, they just quietly go stale.&lt;/p&gt;

&lt;p&gt;Fixing the redirect check to mean "3xx except 304" and requeueing took failures from 501 to 13.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Solve it with a parameter before adding a tool; every tool is a decision the model can get wrong&lt;/li&gt;
&lt;li&gt;Auth failures are HTTP 401 — &lt;code&gt;isError&lt;/code&gt; on a 200 never triggers a sign-in prompt&lt;/li&gt;
&lt;li&gt;Delete arguments that must never be passed; do not merely validate them&lt;/li&gt;
&lt;li&gt;The Workers Rate Limiting binding does not block. Use a Durable Object when the count must be right&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Demo, no auth required: &lt;code&gt;https://mcp.rssatlas.com/demo&lt;/code&gt; — and the directory is at &lt;a href="https://rssatlas.com" rel="noopener noreferrer"&gt;rssatlas.com&lt;/a&gt;.&lt;/p&gt;

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