Secure MCP server for RSS/Atom feed reading with SQLite backend.
uvx mcp-feed-reader-crunchtoolspip install mcp-feed-reader-crunchtoolspodman run -v feedreader-data:/data quay.io/crunchtools/mcp-feed-reader| Variable | Default | Description |
|---|---|---|
FEED_READER_DB |
~/.local/share/mcp-feed-reader/feeds.db |
SQLite database path |
HTTP_PROXY / HTTPS_PROXY / NO_PROXY |
(none) | Standard httpx proxy variables, honoured for every feed fetch. Useful when a publisher rate-limits your egress IP — Reddit returns 429 to datacentre ranges regardless of User-Agent |
add_feed_tool— Add an RSS/Atom feed by URLlist_feeds_tool— List all feeds with unread countsget_feed_tool— Get feed detailsdelete_feed_tool— Remove a feedrefresh_feeds_tool— Crawl feed sources for new content (slow, prefer systemd timer)
list_entries_tool— List entries (filterable, paginated)read_entry_tool— Read full entry content (auto-marks read)mark_read_tool— Mark entries as readmark_unread_tool— Mark entry as unreadsearch_entries_tool— Full-text search (FTS5)
list_categories_tool— List categories with countscreate_category_tool— Create categoryrename_category_tool— Rename categorydelete_category_tool— Delete category
import_opml_tool— Import from OPML fileexport_opml_tool— Export as OPMLget_stats_tool— Dashboard stats
Nothing in the server crawls on its own. list_entries_tool and friends read a
cache; if no one calls --fetch, that cache goes stale and every reader sees an
empty feed with no error anywhere. A deployment without a crawl trigger is a
broken deployment — this bit the daily briefing, which reported "no news" for
days while all 61 feeds sat unfetched.
# Fetch all feeds via CLI
mcp-feed-reader-crunchtools --fetchdeploy/ ships the two units used in production. Copy them to
/etc/systemd/system/, adjust the container name in the service if yours
differs, then:
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-feeds-refresh.timerThe service runs --fetch inside the running container
(podman exec), so the database keeps the SELinux category of the process that
owns it. Do not bind-mount the data directory into a second container with
:Z to run the crawl — that relabels the volume and locks the live server out.
The timer fires hourly, plus once at 05:45. That second entry is not redundant: consumers window entries on publication time, so an entry published between the last crawl and a 06:00 report is missing from that report and already older than the next day's window start — it is lost for good. Any consumer on a fixed schedule wants a crawl shortly before it.
Feeds are fetched conditionally (ETag / If-Modified-Since). A rate limit,
gateway 5xx, or dropped connection is retried with exponential backoff, up to
three attempts in all, before the feed is recorded as failed.
AGPL-3.0-or-later