Skip to main content
Glama
README.md
# inoreader-mcp

MCP server for the [Inoreader](https://www.inoreader.com) RSS reader API. Lets Claude help you triage articles, analyze feed health, and manage subscriptions.

## Setup

### 1. Create an Inoreader API application

Go to the [Inoreader Developer Portal](https://www.inoreader.com/developers/) and create a new application.

- Set the redirect URI to `http://localhost:3333/callback`
- Note your **Client ID** and **Client Secret**

### 2. Install

This project is **not** published to npm. The name `inoreader-mcp` on the npm registry
belongs to an unrelated project, so `npx inoreader-mcp` installs someone else's code.
Build it from source instead:

```bash
git clone https://github.com/justmytwospence/inoreader-mcp
cd inoreader-mcp
npm install && npm run build
```

### 3. Add to Claude Code

```bash
claude mcp add -s user \
  -e INOREADER_CLIENT_ID=your-client-id \
  -e INOREADER_CLIENT_SECRET=your-client-secret \
  inoreader -- node /absolute/path/to/inoreader-mcp/dist/index.js
```

Or for Claude Desktop, add to your config file:

```json
{
  "mcpServers": {
    "inoreader": {
      "command": "node",
      "args": ["/absolute/path/to/inoreader-mcp/dist/index.js"],
      "env": {
        "INOREADER_CLIENT_ID": "your-client-id",
        "INOREADER_CLIENT_SECRET": "your-client-secret"
      }
    }
  }
}
```

### 4. Authenticate

On first use, ask Claude to call the `setup_auth` tool. It will give you an OAuth URL to open in your browser. After authorizing, copy the `code` parameter from the redirect URL and pass it back. Tokens are saved to `~/.config/inoreader-mcp/tokens.json` and refresh automatically.

## Resources

Context that MCP clients can read directly without tool calls.

| Resource | URI | Description | API Cost |
|----------|-----|-------------|----------|
| `rate-limits` | `inoreader://rate-limits` | Current API rate limit usage and remaining budget | 0 |
| `subscriptions` | `inoreader://subscriptions` | All feeds with id, title, URL, and folder assignments | 1 Z1 |
| `folders` | `inoreader://folders` | Folder and tag structure | 1 Z1 |
| `unread-counts` | `inoreader://unread-counts` | Unread counts per feed and folder (non-zero, sorted descending) | 1 Z1 |

## Tools

### API primitives

Thin wrappers around individual Inoreader API endpoints.

#### Reading

| Tool | Description | API Cost |
|------|-------------|----------|
| `get_unread_counts` | Unread counts for all feeds/folders, sorted by count | 1 Z1 |
| `get_articles` | Fetch articles with filters (stream, status, date range, pagination). `compact=true` returns id/title/source/flags only (~10x smaller). `include_total=false` skips the count request. | 1-2 Z1/page |
| `get_article_ids` | Lightweight ID-only fetch for counting/batch ops. Use `get_articles(compact=true)` for a middle ground with titles. | 1 Z1 |
| `get_article_content` | Full HTML content for specific articles by ID | 1 Z1 |
| `search_articles` | Keyword search across all feeds. Supports `compact=true`. | 1 Z1/page |

#### Subscriptions

| Tool | Description | API Cost |
|------|-------------|----------|
| `list_subscriptions` | All feeds with folders, URLs, metadata | 1 Z1 |
| `manage_subscription` | Subscribe, edit (rename/move), or unsubscribe | 1 Z2 |

#### Organization

| Tool | Description | API Cost |
|------|-------------|----------|
| `manage_tags` | Mark read/unread/starred, apply/remove tags (batch support) | 1 Z2 |
| `batch_manage_tags` | Apply different tags to different article groups in one call — ideal for triage workflows. Verified. | 1 Z2 + 1 Z1 per 50 articles |
| `mark_all_read` | Mark all items in a feed/folder as read | 1 Z2 |
| `list_folders_and_tags` | All folders and tags with unread counts (unread only; use `get_article_ids` for totals) | 1 Z1 |

#### Account

| Tool | Description | API Cost |
|------|-------------|----------|
| `get_user_info` | Authenticated user info | 1 Z1 |
| `get_rate_limit_status` | Check remaining API budget (reads persisted state) | 0 |

### Composite tools

Higher-level workflows that combine multiple API calls or add client-side logic.

#### Feed management

| Tool | Description | API Cost |
|------|-------------|----------|
| `get_uncategorized_feeds` | Feeds with no folder, as compact tuples | 1 Z1 |
| `suggest_feed_cleanup` | Deterministic cleaned-up title proposals for uncategorized feeds — strips vendor boilerplate (`Blog on X`, `X's Blog`, trailing `RSS`/`Feed`, redundant site suffix). Read-only; apply with `manage_subscription`. | 1 Z1 |
| `categorize_feeds` | Bulk-assign feeds to folders from a `{folder: [id, ...]}` map. Verified. | 1 Z2/feed + 2 Z1 |
| `reassign_feeds` | Move feeds between folders in bulk. Verified — a feed counts as moved only when it is in the destination *and* out of the source. | 1 Z2/feed + 2 Z1 |
| `analyze_feeds` | Bayesian feed health analysis with category-level priors. Degrades and reports rather than exhausting the read budget. | 3 + pages [+ 1 Z1/feed for volume] |

#### Saved items

| Tool | Description | API Cost |
|------|-------------|----------|
| `get_saved_items` | Union of starred articles + saved web pages + Keep-tagged items in one call. Deduplicates and adds `saved_via` field. Use instead of 3+ separate calls. | 3 Z1 |
| `get_saved_web_pages` | List saved pages with `removable` filter (excludes starred and `keep`-tagged). Supports `compact=true`. | 1 Z1/page |
| `remove_saved_web_pages` | Batch-remove saved pages by ID. Verified. | 1 Z2 + 1 Z1 per 50 |

#### Classifier calibration

| Tool | Description | API Cost |
|------|-------------|----------|
| `extract_classifier_data` | Pull Inoreader Intelligence summaries for verified-tagged articles, parse RECOMMENDATION + CONFIDENCE. Optional population scan for the score distribution. | ~4-8 Z1 (+population) |
| `analyze_classifier_calibration` | Reliability diagram, ECE, monotonicity violations, threshold diagnostic, Manski FNR bounds, audit-conditional recall posterior | same as extract |
| `recommend_audit_articles` | Active-learning suggestions for which recommend-skip articles to audit | same as extract |

Starred articles, saved web pages, and Keep-tagged items are disjoint collections in the Inoreader API. `get_saved_items` fetches all three and deduplicates them. Each item includes a `saved_via` array (`["starred"]`, `["saved_web_page", "keep"]`, etc.) showing which collections it belongs to.

The `keep` tag (via `manage_tags add_tag='keep'`) protects a page from cleanup without starring it. Use `get_saved_web_pages(filter='removable')` for pages that are neither starred nor kept.

**Z1** = Zone 1 (read), **Z2** = Zone 2 (write). Call `get_rate_limit_status` for the live
per-zone limit and usage rather than assuming a number.

## Prompts

Pre-built workflows that combine resources and tools into guided tasks.

| Prompt | Description | Arguments |
|--------|-------------|-----------|
| `triage-unread` | Review unread articles, summarize each, suggest read/star/skip | `folder?`, `count?` |
| `feed-health-review` | Analyze feed engagement, identify unsubscribe candidates | `months?`, `folder?` |
| `organize-uncategorized` | Find feeds with no folder and suggest assignments | -- |
| `summarize-recent` | Digest recent articles grouped by source with key themes | `folder?`, `hours?` |
| `review-saved-web-pages` | Review saved pages, decide which to keep or remove | -- |
| `analyze-classifier` | Build a reliability diagram for the Inoreader Intelligence classifier from verified-tagged articles | `breakdown_by?`, `bins?` |

## Classifier calibration workflow

If you use Inoreader Intelligence to classify articles as worth reading vs. skippable, the calibration tools let you treat that LLM as a binary classifier and analyze its reliability over time using only data already in Inoreader (no new LLM calls).

### 1. Configure the prompt

In Inoreader Intelligence settings, create a custom summary prompt:

```
Summarize this article in 2-3 sentences. Then on new lines, output exactly:

RECOMMENDATION: read | skip
CONFIDENCE: <integer 0-100>

Be conservative — most articles are not worth reading in full. Recommend "read"
only if the article likely contains substantive new information that cannot be
obtained from the summary alone.
```

Optionally set up an Inoreader automation rule with the "Create summary" action so summaries are auto-generated for new articles in folders you care about.

### 2. Tag articles after reading

When you finish reading an article, apply one of these tags via `manage_tags` (forward slashes are part of the label name, not a folder hierarchy):

| Tag | Meaning |
|-----|---------|
| `read/worth-it` | Opened a recommend-read article, was worth it (true positive) |
| `read/not-worth-it` | Opened a recommend-read article, wasn't worth it (false positive) |
| `audit/worth-it` | Opened a recommend-skip article as an audit, was worth it (false negative caught) |
| `audit/not-worth-it` | Opened a recommend-skip article as an audit, wasn't worth it (true negative confirmed) |

### 3. Run the analysis

Call the `analyze-classifier` prompt periodically (or once verified counts pass ~30). It runs `extract_classifier_data`, then `analyze_classifier_calibration`, and walks you through the reliability diagram, ECE, monotonicity violations, and threshold diagnostic.

### 4. Optionally audit

If you want to validate that the calibration curve extends below the decision threshold, call `recommend_audit_articles`. It returns a small batch of recommend-skip articles weighted by where the calibration curve has the most uncertainty. Read them, apply the `audit/*` tags, and the next `analyze_classifier_calibration` run will produce a posterior over recall using that data.

### Why this is structured this way

This is a *selective labels* problem: you observe ground truth for articles the LLM recommended (you read them), and not for articles it told you to skip (unless you audit). The calibration curve **on the recommended slice is fully identified** from observed data — that's the headline product, with per-bin Beta-Binomial posteriors and an isotonically-smoothed monotone curve. Below the decision threshold, behavior is unobserved without audits; that's why the threshold diagnostic only states an upper bound (via monotonicity) and the recall posterior only appears once `audit/*` tags exist.

## Write verification

Every bulk write reads its own result back and reports only what the server confirms.

This exists because of a specific failure. A 42-feed `reassign_feeds` call returned
`{"succeeded": 42, "failed": 0}` having actually moved 21: eleven feeds had not moved,
and fourteen had been added to the destination without being removed from the source —
from a single request carrying both `a=` and `r=` that Inoreader answered `200`. The
discrepancy surfaced days later, by reading all 1818 subscriptions back by hand.

Two things allowed it. `succeeded` counted HTTP 2xx responses rather than applied
changes, and `by_folder` was built from the *input* map, so it could not represent a
shortfall at all. Inoreader also returns `200 "OK"` for an edit to a feed that does not
exist, so no amount of inspecting the request or its response can establish what
happened. Only reading the state back can.

So the bulk tools apply, wait, re-read, retry the shortfall once, and report:

| Field | Meaning |
|---|---|
| `verified` | Confirmed present on the server |
| `unverified` | The write returned 2xx but the server does not show the change |
| `failed` | The write itself errored |
| `not_attempted` | Skipped, e.g. budget |

`isError` is set whenever `verified < intended`. **Trust `verified`, never the size of
your request.** `unverified` is the bucket the old code reported as success.

Verification is cheap for folder writes — `subscription/list` returns every feed with
its labels in one request, so 4 feeds and 400 cost the same, in the zone that is not
scarce. Article tags cost one read per 50.

## Rate Limits

Every tool description includes its API cost so Claude can budget calls. `get_rate_limit_status` reports live usage without making an API request, reading state
persisted under `~/.config/inoreader-mcp/rate-limit.json` by earlier calls.

Do not hardcode a daily limit. This README claimed ~100/zone/day for a long time; the
account it was developed against actually reports 2000. Ask the tool.

## License

MIT

TDQS

A4/5.0

Scored across 12 tools

Disambiguation4/5

Most tools have distinct purposes targeting specific resources or actions, such as get_articles for fetching content and manage_subscription for feed management. However, some overlap exists between get_articles and get_article_ids, where the latter is a lightweight version, which could cause minor confusion if not carefully described.

Naming Consistency5/5

Tool names consistently follow a verb_noun pattern throughout, such as get_articles, list_subscriptions, and manage_tags. There are no deviations in naming conventions, making the set predictable and easy to understand.

Tool Count5/5

With 12 tools, the server is well-scoped for RSS feed management, covering authentication, subscription handling, article retrieval, and user interactions. Each tool serves a clear purpose without redundancy, fitting typical tool count ranges for such a domain.

Completeness4/5

The tool surface provides comprehensive coverage for RSS feed operations, including CRUD-like actions for subscriptions and tags, article management, and user info. A minor gap is the lack of a tool for directly editing or deleting individual articles, but agents can work around this using batch operations in manage_tags.

Maintenance

ActivityMaintained
ResponsivenessNo issues