it-digest
# news-digest
A small personal news digest: RSS aggregator with optional LLM summarization,
exposed as an **MCP server** for Claude Code / Claude Desktop (plus a FastAPI
HTTP layer). Built as a hobby project "delve into Python", focused on asyncio, raw SQL, the Anthropic API and the Model
Context Protocol.
# README CONTENTS:
- [What it does](#what-it-does)
- [Architecture](#architecture)
- [Design decisions](#design-decisions)
- [Getting started](#getting-started)
- [Development](#development)
- [Configuration](#configuration)
- [Repo structure](#repo-structure)
## What it does
Register the server in Claude Code and ask *"fetch the latest articles and
give me an overview of today's AI news"*. The model calls the `fetch_latest`
and `get_articles_for_digest` tools on its own and writes the digest from the
source material.
The app is **domain-agnostic**: it digests whatever your feeds cover. The
bundled defaults are tech feeds, so the examples below are tech - but point
`feeds.txt` at economics, sports or local-news sources and everything
downstream (archive, search, digest, topic filter) follows. A sample run
with the defaults:
> **Today's AI news (Aug 11, 2026)**
>
> *Local and edge inference - today's strongest theme*
> - H3-metal (391 pts) - antirez wrote native MiniMax-H3 inference for Apple
> Silicon in plain C. The biggest AI story of the day on HN.
> - Needle 2 (472 pts) - a 14 MB agentic LLM: tool calls and structured
> extraction on phones and Raspberry Pi 5 (~500 tok/s).
>
> *Business and society*
> - As AI eats the web (693 pts, 744 comments) - how AI answers drain the
> web...
## Architecture
```
┌─────────────────────────┐
RSS/Atom feeds ──▶│ ingest.py │
(httpx async, │ httpx.AsyncClient │
N sources │ + feedparser (sync!) │
in parallel) │ → list[Article] │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ storage.py │
│ SQLite, raw SQL │
│ articles, sources │
│ dedup via UNIQUE(url) │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ llm.py │
│ Anthropic API: │
│ classify (Haiku) │
│ → rank → summarize │
│ (Opus), structured out │
└────────────┬────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ mcp_server.py │ │ api.py │
│ MCPServer, stdio │ │ FastAPI, Pydantic │
│ tools: fetch_latest, │ │ GET /digest/latest │
│ search, digest, │ │ GET /articles │
│ source management │ │ │
└─────────────────────────┘ └─────────────────────────┘
▲
│ stdio transport
Claude Code / Claude Desktop (MCP host)
```
## Design decisions
- **Two phases of intelligence.** Phase 1 MCP tools (`fetch_latest`,
`search_archive`, `get_articles_for_digest`) return data only - the host
model does the synthesis, so the server needs no API key. Phase 2
(`make_digest`) runs its own pipeline against the Anthropic API: a cheap
model (Haiku) classifies every article, a stronger one (Opus) only
summarizes the top N. With MCP you have to decide on which side the LLM
call runs - both variants live here side by side on purpose.
- **SQLite + raw SQL, no ORM.** A local single-user tool: the DB is one file,
dedup is `UNIQUE(url)` + `INSERT OR IGNORE`, every query is parametrized.
On a bigger schema I would reach for SQLAlchemy/SQLModel for the same
reasons I use Drizzle in TypeScript.
- **feedparser runs via `asyncio.to_thread`.** No blocking calls inside async
code - either the library has an async variant (httpx), or it goes to a
worker thread.
- **One dead feed never kills the run.** `fetch_feed` returns `None` instead
of raising; a failing source is skipped and the rest proceed.
## Getting started
You will need these tools installed:
- [git](https://git-scm.com/downloads) - to clone this repo (on macOS,
`xcode-select --install` gets you it)
- [uv](https://docs.astral.sh/uv/getting-started/installation/) - Python
package manager (also installs Python itself if you have none)
- [Claude Code](https://code.claude.com/docs) - the MCP host you will talk to
No git? Download the repo as a ZIP from the green **Code** button on GitHub,
unpack it, and start from `cd news-digest` below.
You do not "run" this app directly - you clone it, register it as an MCP
server and then talk to it through Claude. In your terminal:
```bash
git clone https://github.com/davpu/news-digest
cd news-digest
uv sync
claude mcp add news-digest -- uv run --directory "$(pwd)" python src/mcp_server.py
```
The registration is scoped to the directory you run `claude mcp add` from -
start your Claude Code sessions there (`cd news-digest && claude`) to
see the server.
Then just ask, in plain language (examples assume the default tech feeds -
with your own sources, ask about your own domain):
- *"fetch the latest articles and give me an overview of today's AI news"*
- *"did we have anything about Kubernetes lately?"*
or use the built-in prompt template as a one-click action:
`/mcp__news-digest__daily_digest` (arguments: `topic`, `days`).
Phase 2 (`make_digest`, module `llm.py`) needs `ANTHROPIC_API_KEY` in `.env`
(see `.env.example`).
## Development
Each module doubles as a smoke test when run directly:
```bash
uv run python src/ingest.py # feed fetching
uv run python src/storage.py # SQLite layer
```
## Configuration
Everything model- or content-facing lives outside the code, which is also
what makes the app domain-agnostic:
- `feeds.txt` - **what to digest from**: one feed URL per line, any domain;
while empty, the app runs on bundled defaults (`DEFAULT_FEEDS` in
`src/ingest.py`). You can also just ask Claude - the `list_sources`,
`add_source` and `remove_source` tools manage this file conversationally,
and each new feed is downloaded and validated before it is added. The
`setup_sources` prompt template bootstraps a whole new domain in one go
("find me quality economics feeds")
- `prompts/interests.md` - default relevance profile for the LLM
classification step (phase 2)
- **what to digest**: the `daily_digest` prompt template and the
`get_articles_for_digest` tool both take an optional `topic`, so the same
archive can produce an AI digest, a security digest, or anything else
## Repo structure
- `src/` - the code (5 modules, see diagram)
- `prompts/` - model-facing text kept out of code
The MCP server also exposes a `daily_digest(topic, days)` prompt template
(MCP prompts primitive), so hosts can offer the whole flow as a one-click
action.
TDQS
Scored across 7 tools
Most tools have clearly distinct purposes: add_source vs remove_source are opposites, fetch_latest vs search_archive vs get_articles_for_digest each target different actions. The only slight ambiguity is between get_articles_for_digest and make_digest, but the latter is explicitly marked as not implemented and directs to the former.
All tool names follow the same verb_noun pattern in lowercase with underscores (add_source, remove_source, fetch_latest, search_archive, get_articles_for_digest, make_digest, list_sources). This is highly consistent and predictable.
With 7 tools, the set is well-scoped for an RSS digest server: source management (add, remove, list), content fetching, archive search, and digest creation. Each tool serves a distinct purpose without unnecessary bloat or missing essentials.
The core workflows are covered: source CRUD (add/remove/list), fetching latest articles, searching the archive, and retrieving articles for a digest. Minor gaps include lack of an update_source tool and the unimplemented make_digest, but the alternatives provided make the surface functional.