Content
# feed-curator-mcp
[](https://github.com/yunbow/feed-curator-mcp/actions/workflows/ci.yml)
[](./LICENSE)
> Collect RSS feeds, score them, distill copyright-free facts, and draft articles — all from your host AI's chat, via the Model Context Protocol.
An **MCP server** that ingests RSS feeds, then lets the **host AI (the MCP client)** score each article, extract copyright-free facts, and draft original articles. There is **no external LLM API call** — the host AI does the reasoning, and the server stores everything in SQLite (`data/curator.db`).
Feeds and settings live in the database (`feeds` / `settings` tables), so you manage them entirely from chat — no `feeds.yaml` required.
## Why feed-curator-mcp?
- **Five focused steps** — fetch → score → extract-facts → list → generate, each driven from your host AI's chat
- **Host AI is the generator** — scoring / fact extraction / article writing are done by the connected AI (`prepare_*` hands it the material, `save_*` stores the result); no `claude -p` and no API keys
- **Copyright-aware** — facts and articles are independently rewritten in your own words; no translation or reproduction of source text
- **Deterministic dedup** — URL normalization + title similarity (Jaccard) filter duplicates before any AI reasoning
- **AI scoring** — nine value/buzz criteria scored 0–10 each, normalized to a 0–100 score
- **Local & inspectable** — plain SQLite storage, feeds & settings included; nothing leaves your machine except feed fetches
## Requirements
- Node.js 20+ (verified on v22)
- An MCP client as the host AI (e.g. Claude Desktop, Claude Code, or any MCP-capable client)
## Setup
```bash
npm install
npm run build # compile to dist/ (entry: dist/index.js)
```
## Connecting to a host AI
Register the server with your MCP client. The server speaks JSON-RPC over **stdio**.
### Claude Code
```bash
claude mcp add feed-curator -- node /absolute/path/to/feed-curator-mcp/dist/index.js
```
### Claude Desktop (`claude_desktop_config.json`)
```json
{
"mcpServers": {
"feed-curator": {
"command": "node",
"args": ["/absolute/path/to/feed-curator-mcp/dist/index.js"],
"env": {
"CURATOR_DB_PATH": "/absolute/path/to/data/curator.db"
}
}
}
}
```
Environment variables (all optional):
- `CURATOR_DB_PATH` — SQLite path (default `data/curator.db` under the working directory)
- `CURATOR_OUTPUT_DIR` — base directory for generated articles (default `output`)
### Inspecting the server
Use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to exercise tools manually (use v0.14.1+):
```bash
npm run build
npm run inspect # npx @modelcontextprotocol/inspector node dist/index.js
```
## Usage (5 steps, from chat)
Once connected, ask the host AI to call these tools. A typical run:
1. **Register feeds, then fetch** — `add_feed`, then `fetch_feeds`
- "Add the Hacker News feed `https://hnrss.org/frontpage`, then fetch."
2. **Score** — `prepare_scoring` → (host AI scores) → `save_scores`
- "Prepare scoring and score the new articles." The host AI rates 9 criteria (0–10); the server normalizes to 0–100.
3. **Extract facts** — `prepare_facts` → (host AI extracts) → `save_facts`
- From non-duplicate articles scoring ≥ threshold, the host AI writes copyright-free facts.
4. **List recommendations** — `list_facts`
- "Show me the recommended facts." (ungenerated only; `all: true` to include generated)
5. **Generate an article** — `prepare_article` → (host AI writes) → `save_article`
- "Generate an article from fact 1." The fact is marked generated and a Markdown file is written to `output/`.
## Tools
| # | Tool | What it does | Touches |
|---|---|---|---|
| — | `add_feed` / `list_feeds` / `remove_feed` | Manage RSS feeds | `feeds` table |
| — | `get_settings` / `update_settings` | Read/update thresholds & batch sizes | `settings` table |
| 1 | `fetch_feeds` | Fetch URL/title/summary from registered feeds | `articles` table |
| 2 | `prepare_scoring` | Dedup (URL + title similarity), return a batch + scoring instruction | `articles` (marks duplicates) |
| 2 | `save_scores` | Save host-AI scores (normalized to 0–100) | `articles` |
| 3 | `prepare_facts` | Return articles scoring ≥ threshold + extraction instruction | (read-only) |
| 3 | `save_facts` | Save host-AI copyright-free facts | `facts` table |
| 4 | `list_facts` | Recommended facts ordered by score | (read-only) |
| 5 | `prepare_article` | Return a fact + article-writing instruction | (read-only) |
| 5 | `save_article` | Save the drafted article, mark generated, write Markdown | `facts` / `output/` |
The `prepare_*` / `save_*` split is what keeps generation on the host AI: `prepare_*` returns the source material plus an instruction telling the AI which `save_*` tool to call next.
### Tool responses
Every tool returns a **two-layer response**: a human-readable Markdown summary (`content`) and machine-readable JSON (`structuredContent`) validated against each tool's `outputSchema`. Tools carry [annotations](https://modelcontextprotocol.io/docs/concepts/tools) (`readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`), and most successful results include a structured `next_step` that points the host AI to the next tool. Business errors (not found, missing prerequisites, path traversal) come back as structured `isError` results with an `error_code` and a `suggestion` so the AI can self-correct.
## Configuration (in the database)
Settings live in the `settings` table and are seeded with defaults on first run. Change them from chat via `update_settings`:
| Key | Default | Meaning |
|---|---|---|
| `score_threshold` | 50 | minimum score for fact extraction / recommendations |
| `title_similarity_threshold` | 0.7 | threshold for title-similarity duplicate detection |
| `score_batch_size` | 5 | batch size for `prepare_scoring` |
| `fact_batch_size` | 5 | batch size for `prepare_facts` |
| `target_country` | 日本 | home country used for the `gap` criterion and the assumed reader (e.g. `日本`, `United States`) |
| `target_language` | 日本語 | output language for extracted facts and generated articles (e.g. `日本語`, `English`) |
The defaults target a Japanese audience. To curate for another country/language — e.g. from chat: "Set target_country to United States and target_language to English." — call `update_settings`. From then on, `prepare_scoring` scores the `gap` criterion against that country, and `prepare_facts` / `prepare_article` instruct the host AI to write in that language.
## Database schema
- **feeds**: registered RSS feeds (`name`, `url` unique).
- **settings**: key/value configuration (seeded with defaults).
- **articles**: fetched articles. Holds duplicate flags (`is_duplicate` / `dup_reason`) and scores (`total_score`, per-criterion `score_detail`, `score_reason`).
- **facts**: copyright-free fact information. Holds `article_generated` (generated flag) and `generated_content` (the drafted article body).
## Scoring criteria (0–10 each, normalized to 0–100)
Novelty / Utility / Gap (vs. `target_country`) / Emotion / Authority / Trend / Surprise / Numbers impact / Empathy
The `Gap` criterion measures the difference against your `target_country` setting (default `日本`).
## Copyright
Steps 3 and 5 never translate or reproduce source text. Facts are reconstructed as concise, original statements, and articles are written from scratch based on those facts, in your `target_language` (enforced explicitly in the instructions returned to the host AI).
## Development
```bash
npm run build # compile to dist/
npm run type-check # tsc --noEmit
npm run lint # ESLint
npm test # node:test unit tests
```
All four run in CI on every push and pull request.
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md). Security policy: [SECURITY.md](./SECURITY.md).
## License
[MIT](./LICENSE)
---
Languages: English | [日本語](docs/i18n/ja/README.md) | [简体中文](docs/i18n/zh-CN/README.md) | [한국어](docs/i18n/ko/README.md) | [Español](docs/i18n/es/README.md)