作者构建了含 2907 个 RSS 源、覆盖 37 个类别的目录,并封装为 MCP Server,使 AI 助手可查询特定领域最近两周的实时信息,而非依赖训练数据或泛搜索。
让 AI 问出"这个领域这周发了什么"
向助手询问最新动态,你得到的要么是训练数据里的内容,要么是网页搜索的前几条结果。你真正想问的东西却问不了——比如"过去两周 Rust 工具链有什么新东西"——按领域限定范围,在真正覆盖该领域的信源里检索。
这些信源是存在的。它们是 RSS feed。只是在 AI 工具里触达不到。
所以我建了一个 RSS feed 目录,并给它配了一个 MCP server。这篇文章讲的是四个做完之后才变得显而易见的决策,不是产品tour。
2907 个 RSS feed,跨越 19 种语言和 37 个分类,共 510483 篇文章。一个 MCP server 向 AI 工具暴露了这些数据。
claude mcp add --transport http rss-atlas https://mcp.rssatlas.com/mcp
其他客户端放在 settings.json 里:
{ "mcpServers": { "rss-atlas": { "httpUrl": "https://mcp.rssatlas.com/mcp" } } }
跑在 Cloudflare Workers(站点和 MCP server)、Supabase(Postgres)和 Cloud Run Jobs(爬虫)上。如果你想试试,有个无需认证的 demo:https://mcp.rssatlas.com/demo
server 只暴露了四个工具:search_feeds、get_feed、get_articles、get_rankings。
做"只从我收藏的 feed 取文章"这个功能时,本来想做成第五个工具——get_favorite_articles。最终做成了一个参数:
favorites_only: z.boolean().optional()
.describe("Restrict results to the signed-in user's favorites"),
原因是每加一个工具,模型就多一个必须做对的抉择。有了 get_articles 和 get_favorite_articles 并列,模型每次调用都要在它们之间抉择。作为参数根本不是抉择——只是参数而已。少一个分支。
现在的规则是:加工具之前,先看看现有工具的参数能不能承载它。
这条耗费了最多时间。
MCP 允许工具在结果里返回 isError: true。一开始我把它用在了未认证请求上——HTTP 200,payload 里放 error。
但客户端不会为此弹出登录提示。
它们判断"需要认证"是看 HTTP 401 加上 WWW-Authenticate header。200 对传输层来说意味着调用成功了,所以里面的内容没有办法让客户端启动认证流程。对用户看起来就是一个报莫名错误的工具。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.rssatlas.com/.well-known/oauth-protected-resource"
认证服务器就是从那个 header 发现的。所以:认证是在 HTTP 层表达的,不是在工具结果里。工具调用内部的普通失败——没有匹配、参数错误——仍然可以用 isError。这两种不应该混在一起。
对于任何按用户维度的东西,user id 不是工具参数。唯一使用的身份信息来自已验证 access token 的 sub:
if (favorites_only && !caller.userId) return fail("favorites_only requires a signed-in user");
校验本来也可以捕获错误的 id。但 MCP 里模型负责组合参数,所以一个能命名其他用户的参数本身就是一个容易出错的形状。如果字段根本不存在,这类 bug 就不会存在。
对于绝对不能被传进来的东西,结构约束优于校验。
Cloudflare Workers 有一个 Rate Limiting binding。我把它用在了 MCP server 的限速上。
但它没有生效——更准确地说,没有真正拦住。
文档里明说了它不是用来精确计数的。它是分布式的,计数不会在边缘节点即时同步,接近阈值时请求还是会过。适合粗粒度地塑造流量,不是用来可靠地拦住第 31 次调用。
重写成了 Durable Object,序列化到单个实例,因此计数精确。实测后:认证用户第 31 次请求返回 429,demo 用户第 11 次。
从中养成的习惯:实现限速后,要猛击超过阈值,看它真的拒绝。
值得明确说清楚,因为这驱动了好几个决策:产品里没有摘要,也没有 AI 分类。分类是规则和阈值的事。摘要发生在你自己的 LLM 里。
原因是读者已经在为一个模型付费了。服务端调一次,他们就要付两次钱,间接的。通过 MCP,摘要本来就应该在用户那边——更快、更便宜,而且是用户选择模型。
同样原因,feed 内容上限是标题加 500 字符摘要加链接。不存全文。你去源头读,RSS 本来就是这个用途。
跟 MCP 无关,但花了差不多一样多的时间。
条件 GET 加上重定向:/feed → 301 → /feed/ → 304 Not Modified。
304 是 3xx。字面理解"3xx 就是重定向",你会去找 Location header,而 304 没有这个字段,然后就报错了。
麻烦的是,只有曾经成功到足以存下 ETag 的 feed 才会触发这个——所以表现最好的信源先坏,而且因为列表照样能渲染,它们只是悄悄变旧了。
把重定向检查改成"3xx 除了 304",然后重新入队,故障数从 501 降到了 13。
用参数解决问题再加工具;每个工具都是模型可能做错的抉择
认证失败是 HTTP 401——200 里的 isError 永远不会触发登录提示
删除绝对不能传进来的参数;不要只是做校验
Workers Rate Limiting binding 不会真正限流。需要精确计数时用 Durable Object
Demo,无需认证:https://mcp.rssatlas.com/demo —— 目录在 rssatlas.com。