cd /news/developer-tools/letting-an-ai-ask-what-shipped-in-th… · home topics developer-tools article
[ARTICLE · art-95632] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Letting an AI ask 'what shipped in this field this week?'

A developer built RSS Atlas, a directory of 2,907 RSS feeds across 19 languages and 37 categories, and exposed it to AI tools via an MCP server. The server, running on Cloudflare Workers, Supabase, and Cloud Run Jobs, offers four tools for searching and retrieving feeds and articles. The developer shares key lessons learned, including expressing authentication at the HTTP layer and using parameters instead of separate tools to reduce model decision branches.

read4 min views1 publishedAug 13, 2026

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 — "what came out in Rust tooling in the last two weeks" — scoped to a field, across the sources that cover it.

Those sources exist. They are RSS feeds. They are just not reachable from an AI tool.

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.

2,907 RSS feeds across 19 languages and 37 categories, holding 51,483 articles. An MCP server exposes it to AI tools.

claude mcp add --transport http rss-atlas https://mcp.rssatlas.com/mcp

Other clients take it in settings.json

:

{ "mcpServers": { "rss-atlas": { "httpUrl": "https://mcp.rssatlas.com/mcp" } } }

It runs on Cloudflare Workers (site and MCP server), Supabase (Postgres), and Cloud Run Jobs (crawler). There is a no-auth demo at https://mcp.rssatlas.com/demo

if you want to poke at it.

The server exposes exactly four tools: search_feeds

, get_feed

, get_articles

, get_rankings

.

Building "fetch articles from only my favourited feeds" wanted to be a fifth tool — get_favorite_articles

. It became a parameter instead:

favorites_only: z.boolean().optional()
  .describe("Restrict results to the signed-in user's favorites"),

The reason is that every added tool is another choice the model has to get right. With get_articles

and get_favorite_articles

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.

The rule now is: before adding a tool, check whether an existing tool's parameters can carry it.

This one cost the most time.

MCP lets a tool return isError: true

in its result. I used that for unauthenticated requests at first — HTTP 200, error in the payload.

Clients do not show a sign-in prompt for that.

They decide "authentication needed" from HTTP 401 plus a WWW-Authenticate header. 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.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.rssatlas.com/.well-known/oauth-protected-resource"

The authorization server is discovered from that header. So: authentication is expressed at the HTTP layer, not in tool results. Ordinary failures inside a tool call — no matches, bad argument — are still fine as isError

. Do not mix the two.

For anything scoped to a user, the user id is not a tool parameter. The only identity used is sub

from the verified access token:

if (favorites_only && !caller.userId) return fail("favorites_only requires a signed-in user");

Validation would have caught a wrong id. But with MCP the model composes the arguments, 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.

Structure beats validation for anything that must never be passed.

Cloudflare Workers has a Rate Limiting binding. I used it for the MCP server's limits.

It did not work — more precisely, it did not block.

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.

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.

The habit that came out of this: after implementing a rate limit, hammer past the threshold and watch it actually refuse.

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.

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.

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

Not MCP, but it ate a comparable amount of time.

Conditional GET plus redirects: /feed

301/feed/

304 Not Modified.

304 is a 3xx. Treat "3xx means redirect" literally and you go looking for a Location

header, which a 304 does not have, and you error out.

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

Fixing the redirect check to mean "3xx except 304" and requeueing took failures from 501 to 13.

isError

on a 200 never triggers a sign-in promptDemo, no auth required: https://mcp.rssatlas.com/demo

— and the directory is at rssatlas.com.

── more in #developer-tools 4 stories · sorted by recency
── more on @rss atlas 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/letting-an-ai-ask-wh…] indexed:0 read:4min 2026-08-13 ·