MCP server for Inoreader — exposes feeds, articles, and curation tools to AI assistants.
Project description
inoreader-mcp
MCP server for Inoreader — exposes feeds, articles, and curation tools to AI assistants.
⚡ Vibecoded. This project was designed and implemented in collaboration with an AI coding assistant (Claude). Every architecture decision and code change has a paper trail in commit messages and
CHANGELOG.md. Read those if you want the why behind any piece of the codebase.
Status: v0.1.0. Feature-complete for the v1 scope (read + lightweight curation), single-tenant, stdio transport. Not yet published to PyPI / GHCR; the release workflow is parked under manual dispatch.
Requirements
- Python ≥ 3.11
- An Inoreader Pro / Supporter / Teams plan. Free-tier users cannot access the Inoreader Developer API (you'll get HTTP 403 on every request).
- A registered Inoreader developer app (AppId + AppKey) — get one at https://www.inoreader.com/all_articles#preferences-developer.
uvrecommended for local development.
Local development setup
# 1. Clone and enter the repo
git clone https://github.com/ibrapk/inoreader-mcp.git
cd inoreader-mcp
# 2. Install all dependencies (incl. dev + optional keyring extra)
uv sync --all-extras
# 3. Configure your Inoreader credentials
cp .env.example .env
$EDITOR .env # fill INOREADER_APP_ID and INOREADER_APP_KEY
.env is gitignored — your credentials never leave your machine.
Authorize once with inoreader-mcp auth
The server uses OAuth 2.0 with a local loopback callback. You authorize once;
tokens are persisted at ~/.config/inoreader-mcp/tokens.json (mode 0600) and
refreshed automatically thereafter.
Before the first run, configure your Inoreader app correctly in the developer dashboard:
-
OAuth Redirect URI must be exactly:
http://127.0.0.1:8765/callback(Or any URI you intend to pass via
--redirect-uri. The browser must be redirected to a URL that matches this value exactly — including scheme, host, port and path.) -
Access type must be Read + Write if you plan to use the curation tools (mark read, star, add tag, etc.). If your app is registered as Read only, the OAuth flow will fail with
invalid_scope — An unsupported scope was requestedbecause we requestscope=read writeto enable Zone 2 (mutating) endpoints. You can change this in your app's settings at any time and re-runinoreader-mcp auth.
Then:
# Load your env vars (or export them manually)
set -a && source .env && set +a
# Run the OAuth flow
uv run inoreader-mcp auth
The server opens your browser, you authorize, and the tokens land on disk.
Re-run inoreader-mcp auth any time to re-authorize from scratch.
auth flags
| Flag | Default | Purpose |
|---|---|---|
--redirect-uri URL |
http://127.0.0.1:8765/callback |
Override the loopback URI (must match what's registered in Inoreader) |
--timeout SECONDS |
300 |
How long to wait for the browser callback before giving up |
--no-browser |
off | Print the authorize URL instead of trying to open a browser |
--manual |
off | Headless mode (no local listener, no browser) — see below |
Authorizing on a headless server (no local browser)
If your server has no GUI (typical SSH-only host, container, CI runner), you can authorize without ever loading anything in a browser on the server itself. Two equivalent options:
Option 1 — Manual paste (--manual, recommended)
uv run inoreader-mcp auth --manual
The command prints a long https://www.inoreader.com/oauth2/auth?... URL.
- Open that URL in any browser on another device (your laptop, phone, …).
- Authorize the app on Inoreader's page.
- Inoreader redirects to
http://127.0.0.1:8765/callback?code=...&state=.... Your browser will likely show a connection-refused page — that's expected; nothing is listening on your laptop. The URL in the address bar is what we need. - Copy that full URL and paste it back into the headless terminal when prompted.
The server validates the state (CSRF) and exchanges the code for tokens.
Option 2 — SSH port forwarding
If your SSH workflow allows reconnecting with -L:
ssh -L 8765:127.0.0.1:8765 user@your-headless-host
# then, on the server:
uv run inoreader-mcp auth # default loopback flow
The browser on your laptop hits http://127.0.0.1:8765/callback, which the SSH
tunnel forwards to the listener running on the server. No --manual needed.
Troubleshooting authorization
| Symptom | Most likely cause | Fix |
|---|---|---|
invalid_scope — An unsupported scope was requested in the redirect URL |
Your Inoreader app is registered as Read only, but we request scope=read write |
Change the app's access type to Read + Write in the developer dashboard, then re-run inoreader-mcp auth |
redirect_uri_mismatch (or similar) on the consent page |
The --redirect-uri you're passing doesn't match the one registered for the app |
Update either side so they're identical, including port and path |
403 Forbidden from the API after authorizing |
Your Inoreader plan doesn't include developer API access | Upgrade to Pro / Supporter / Teams |
Browser shows "connection refused" on 127.0.0.1:8765/callback |
Expected with --manual — nothing is supposed to listen on your laptop. |
Just copy the URL from the address bar back into the terminal |
OAuth state mismatch after pasting |
You pasted a URL from a different auth run |
Re-run inoreader-mcp auth --manual and use the freshly opened authorize URL |
Read tools (Phase 2)
Once authorized, run the MCP server over stdio:
set -a && source .env && set +a
uv run inoreader-mcp serve
The server registers six read tools plus a diagnostics health snapshot.
Wire it up to Claude Code with:
claude mcp add inoreader -- env INOREADER_APP_ID=$INOREADER_APP_ID \
INOREADER_APP_KEY=$INOREADER_APP_KEY uv --directory /path/to/inoreader-mcp \
run inoreader-mcp serve
| Tool | What it does |
|---|---|
list_subscriptions |
All your feeds with folders, URLs, icon URLs (cached 60 s) |
list_folders_and_tags |
Folders, user tags, active searches, system streams + unread counts |
get_unread_counts |
Per-feed / per-folder / per-tag unread tallies |
list_articles |
Stream contents with metadata + a short preview. Filter by folder, feed, tag, unread_only, starred_only, since_seconds. Returns short article IDs you pass to get_article. |
get_article |
Full article body as Markdown (capped at max_chars). Pass include_image_urls=True for vision-capable clients. |
search_articles |
Full-text search, optionally scoped to a folder/feed/tag. |
diagnostics |
Auth status, Zone 1/2 quota, cache state, version. Run when something seems off. |
Write tools (Phase 3)
All write tools are gated behind INOREADER_READ_ONLY=1 — set that env var
to a truthy 1 and the server doesn't register any of them.
| Tool | What it does |
|---|---|
mark_read / mark_unread |
Toggle read state on a list of article_ids. Reversible. |
star / unstar |
Toggle the starred flag on a list of article_ids. |
add_tag / remove_tag |
Apply or remove a user tag on a list of article_ids. Tag name must be 1–64 chars from [A-Za-z0-9 _\-.]. |
mark_stream_read |
High blast radius. Marks every unread article in a stream (stream_id or folder name) as read. Strongly recommend calling with dry_run=True first to preview the count and a sample of titles. Optional max_age_days cutoff to leave anything newer untouched. |
Per-article writes batch at 100 IDs per request to fit within Inoreader's edit-tag limits. After any successful write, the hierarchy cache (unread counts, folders/tags) is invalidated so the next read reflects the change.
Note:
get_articlereturnsis_read,is_starred, andlabelsfor convenience, but Inoreader's bulk-fetch endpoint doesn't reliably populate per-user state on those entries (the response includes astate_caveatfield as a reminder). For authoritative state, uselist_articles(e.g.list_articles(starred_only=True)) instead.
Resources & prompts (Phase 4)
The server exposes three resources (browsable Markdown views the user can attach as context in supporting clients) and four prompts (slash-menu templates with safe-by-default workflows baked in):
| Resource URI | Content |
|---|---|
inoreader://subscriptions |
All feeds as a Markdown table (title, folders, URL) |
inoreader://folders |
Folder hierarchy with each folder's child feeds + unread counts |
inoreader://tags |
User tags with their tagged-unread counts |
| Prompt | Args | What it does |
|---|---|---|
catch_up |
none | Summarize my unread from the last ~24 h, grouped by folder; ask before any writes |
weekly_roundup |
none | Summarize what I starred in the last ~7 days, grouped by theme |
digest |
folder |
Per-folder digest of today's unread; ask before mass-marking-read |
find |
topic |
Search subscriptions for a topic, surface the most relevant 5–10 |
All prompts that involve writes pre-bake the safety rule: ask before mutating,
prefer mark_stream_read(dry_run=True) for stream-level operations.
Install helper (Phase 5)
Once you've set INOREADER_APP_ID / INOREADER_APP_KEY in env (or filled them
in interactively), run:
uv run inoreader-mcp install # prints the claude command
uv run inoreader-mcp install --auto-register # also runs claude mcp add for you
uv run inoreader-mcp install --repo-path . # use local repo via `uv --directory`
uv run inoreader-mcp install --manual # force headless paste-URL flow
The flow walks you through:
- Loading credentials (env or prompt)
- Authorizing if no valid tokens are present (auto-detects headless vs loopback)
- Printing — or running, with
--auto-register— theclaude mcp addcommand - Verifying with a
diagnosticsround-trip
Docker
# Build the image
docker build -t inoreader-mcp .
# Authorize once on the host (interactive — needs your browser)
uv run inoreader-mcp auth --manual
# Run the server, mounting your tokens into the container
docker run -i --rm \
-e INOREADER_APP_ID -e INOREADER_APP_KEY \
-v "$HOME/.config/inoreader-mcp:/root/.config/inoreader-mcp" \
inoreader-mcp serve
The published image is at ghcr.io/ibrapk/inoreader-mcp:latest once we tag a
release.
CI
.github/workflows/:
| Workflow | Trigger | What it runs |
|---|---|---|
ci.yml |
PR + push to main | ruff lint + format check, mypy --strict, pytest on Linux + macOS × Python 3.11 / 3.12 / 3.13 |
release.yml |
manual dispatch only | Parked. Trigger from the Actions tab when you want to publish: pick the tag, optionally tick publish_pypi and/or publish_ghcr. No auto-trigger on tag — explicit human click only. |
integration.yml |
manual workflow_dispatch only |
Runs the -m integration test suite against the live Inoreader API. Never on a schedule — the user notices breakage faster than a nightly cron would. |
CLI diagnostics
You can also call diagnostics outside of an MCP client, useful for one-off checks:
$ uv run inoreader-mcp diagnostics
{
"server_version": "0.1.0.dev0",
"auth": {"status": "ok", "user_name": "<your-username>", "scope": "read write", ...},
"quota": {"zone1": {"used": 4, "limit": 100, "reset_after": 1543}, ...},
"cache": {"entries": 0, "ttl_seconds": 60.0}
}
Article content shape
- HTML article bodies are converted to Markdown for compactness and to play well with how LLMs read text. Tables and complex layouts may degrade — see the troubleshooting section below if a specific feed produces poor output.
- By default,
<img>tags become[Image: alt-text]placeholders (URL dropped, tracking pixels collapse to nothing). Passinclude_image_urls=Trueonget_articleto keep full Markdown image syntax. get_articletruncates atmax_chars(default 50 000). The response setstruncated: trueandchars_remaining: Nso the model knows when to refetch.- Article IDs come in two formats. The tools return the short form
(
344691561) for compactness; both forms are accepted as inputs toget_article.
Configuration
All settings are environment variables.
| Var | Default | Purpose |
|---|---|---|
INOREADER_APP_ID |
(required) | Developer AppId |
INOREADER_APP_KEY |
(required) | Developer AppKey |
INOREADER_READ_ONLY |
0 |
If 1, hide the write tools (Phase 3+) |
INOREADER_LOG_LEVEL |
INFO |
DEBUG / INFO / WARNING / ERROR |
INOREADER_LOG_FORMAT |
text |
text or json |
INOREADER_DEBUG |
0 |
If 1, log full request/response (secrets redacted) |
INOREADER_USE_KEYRING |
0 |
If 1, store tokens in the OS keyring instead of the JSON file |
Development
uv run pytest # run the test suite
uv run ruff check . # lint
uv run ruff format . # format
uv run mypy src tests # type-check (strict)
Roadmap
| Phase | Scope | Status |
|---|---|---|
| 1 | Project scaffold, OAuth loopback flow, token storage, structured logging | ✅ Complete |
| 2 | Async HTTP client + transport, read tools, diagnostics |
✅ Complete |
| 3 | Write tools (mark_read, star, add_tag, mark_stream_read) |
✅ Complete |
| 4 | MCP resources + prompts (catch_up, digest, ...) |
✅ Complete |
| 5 | inoreader-mcp install interactive helper, Docker image, GitHub Actions |
✅ Complete |
| 6 | v0.1.0 release prep — CHANGELOG, version bump, ready to tag |
✅ Complete |
See CHANGELOG.md for the full v0.1.0 feature list.
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file inoreader_mcp-0.1.0.tar.gz.
File metadata
- Download URL: inoreader_mcp-0.1.0.tar.gz
- Upload date:
- Size: 129.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
07e55178cb32cba4aee7294e41c0c6730aeddf23ffb63ca4f9840d5a962fd9da
|
|
| MD5 |
1e2d02eadb45ea3bb6cf87fbce834e5f
|
|
| BLAKE2b-256 |
442061eb2439b929f597f638e048366329e04b25f75793746e24cee543663f0f
|
File details
Details for the file inoreader_mcp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: inoreader_mcp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 49.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
df7b594f206c28b49b08c2170df4f0785dc013c183f49c99c97e8923dd74420d
|
|
| MD5 |
b7b6e8529d9dc4ea46829614c9799031
|
|
| BLAKE2b-256 |
f46e6c3994a1d903c0f4c6a5fb632e28b426f453f9c2fc3891ca79dfcbed0e9c
|