Skip to main content

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.5.0 — published to PyPI and as a Claude Desktop .mcpb extension. Three complementary install paths:

  • Claude Desktop one-click (since v0.5.0) — drag-and-drop the .mcpb from the latest GitHub Release into Settings → Extensions. Works on macOS (Apple Silicon + Intel) and Windows. See Install for Claude Desktop below.
  • Stdio CLI / Claude Code (since v0.1.0) — run as a local subprocess. pip install inoreader-mcp or uv tool install inoreader-mcp.
  • HTTP multi-user (since v0.2.0, hardened in v0.3.0+) — long-running web service with its own OAuth 2.0 authorization server (MCP 2025-06-18 spec compliant), Inoreader-OAuth onboarding, audit log, per-user + per-IP rate limit, encrypted-at-rest tokens with HMAC-keyed hashing, and an opt-in admin UI. pip install 'inoreader-mcp[server]' plus the deployment manifests under docs/deploy/.

See CHANGELOG.md for upgrade notes.


Install for Claude Desktop (one-click)

The fastest path. Works on macOS (Apple Silicon + Intel) and Windows.

  1. Register an Inoreader developer app at https://www.inoreader.com/all_articles#preferences-developer. Two settings matter:

    • OAuth Redirect URI: http://127.0.0.1:8765/callback (exact)
    • Access type: Read + Write (required for mark_read, star, mark_stream_read — pick Read-only if you don't want any of those).

    Copy the AppId and AppKey it generates.

  2. Download the latest .mcpb from the Releases page (inoreader-mcp-<version>.mcpb).

  3. Install in Claude Desktop: Settings → Extensions → scroll to the bottom and click Advanced SettingsInstall Extension → pick the .mcpb file you downloaded. Paste the AppId and AppKey into the form Claude Desktop shows. Optionally toggle "Read-only mode" if you don't want any write tools registered.

  4. Authorize from chat: ask Claude "connect my Inoreader account". Claude calls the connect_inoreader tool, gives you an authorize URL, you click → sign in to Inoreader → click Authorize. Your browser will land on a "connection refused" page — that's expected. Copy the URL from the address bar, paste it back into Claude, and Claude finishes the OAuth exchange. Tokens persist across restarts and refresh automatically.

That's it. Try /inoreader:catch_up or just ask Claude "summarize my unread feeds".

For more detail, screenshots, and the FAQ ("Gatekeeper blocked the extension", "How do I disconnect", …), see docs/install-claude-desktop.md.


Other install methods

PyPI (Claude Code, generic stdio MCP clients, scripting)

pip install inoreader-mcp
# or, with uv:
uv tool install inoreader-mcp

This pulls the inoreader-mcp CLI (auth, serve, install, diagnostics). You still need an Inoreader Pro/Supporter/Teams plan, a registered developer app, and to run inoreader-mcp auth once — see the sections below.

For local development against the source tree, use the Local development setup section instead.

Multi-user HTTP mode (v0.2.0+, hardened in v0.3.0). Long-running web service with full OAuth 2.0 (PKCE S256, dynamic client registration, refresh-token rotation), Inoreader-OAuth onboarding, encrypted-at-rest tokens, audit log, per-user rate limit, and an opt-in admin UI. Install the [server] extra (pip install 'inoreader-mcp[server]') and use the manifests in docs/deploy/ — both Docker Compose and Kubernetes versions are provided.

The HTTP service ships:

  • a full OAuth 2.0 authorization server (PKCE S256 only, dynamic client registration via RFC 7591, refresh-token rotation, RFC 8414 discovery)
  • an Inoreader-OAuth /login flow that provisions new users on first sign-in and stores their tokens encrypted-at-rest with Fernet
  • cookie-based sessions for the consent UI
# 1. Generate an at-rest encryption key + a session-cookie secret.
#    Keep both stable across restarts; losing the encryption key = users
#    must re-login because their stored Inoreader tokens become unreadable.
export INOREADER_MCP_ENCRYPTION_KEY=$(python -c \
  "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
export INOREADER_MCP_SESSION_SECRET=$(python -c \
  "import secrets; print(secrets.token_urlsafe(32))")

# 2. Initialize the DB. (Optional: import the existing single-tenant
#    tokens.json so you don't have to /login again.)
inoreader-mcp db init
inoreader-mcp migrate-single-tenant   # idempotent

# 3. In your Inoreader developer dashboard, register the redirect URI
#    `http://<host>:<port>/login/callback` for your app.

# 4. Boot the service.
inoreader-mcp serve --http --host 0.0.0.0 --port 8765

# 5. From an MCP client, drive the OAuth dance:
#    POST /register → GET /authorize (redirects to /login → Inoreader →
#    /login/callback → consent page) → POST /consent → POST /token →
#    Authorization: Bearer <access_token> on /mcp.
# Discovery metadata is at:
curl http://127.0.0.1:8765/.well-known/oauth-authorization-server

For a real public deployment, use the bundled Docker stack:

# Set required env (see compose.yml for the full list).
export INOREADER_APP_ID=...
export INOREADER_APP_KEY=...
export INOREADER_MCP_ENCRYPTION_KEY=$(python -c \
  "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())")
export INOREADER_MCP_SESSION_SECRET=$(python -c \
  "import secrets; print(secrets.token_urlsafe(32))")
export INOREADER_MCP_PUBLIC_HOST=inoreader-mcp.example.com
export INOREADER_MCP_LETSENCRYPT_EMAIL=you@example.com

docker compose up -d
# Caddy auto-provisions a Let's Encrypt cert for the public host;
# /healthz is unauthenticated for uptime checks.
# SQLite DB lives in the `app-data` volume — back it up nightly with:
#   scripts/backup.sh /var/lib/docker/volumes/<stack>_app-data/_data/inoreader-mcp.db /backups/

An optional admin UI can be turned on at deploy time:

# Generate the operator credential.
inoreader-mcp admin set-password
# → prints INOREADER_MCP_ADMIN_USERNAME=… and INOREADER_MCP_ADMIN_PASSWORD_HASH=…

# Add those + INOREADER_MCP_ADMIN_UI=1 to your env, then redeploy.
# https://your-host/admin → login → users / clients / audit pages.

When the flag is unset, /admin/* returns 404 and no admin code loads.

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.
  • uv recommended 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:

  1. 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.)

  2. 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 requested because we request scope=read write to enable Zone 2 (mutating) endpoints. You can change this in your app's settings at any time and re-run inoreader-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.

  1. Open that URL in any browser on another device (your laptop, phone, …).
  2. Authorize the app on Inoreader's page.
  3. 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.
  4. 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_article returns is_read, is_starred, and labels for convenience, but Inoreader's bulk-fetch endpoint doesn't reliably populate per-user state on those entries (the response includes a state_caveat field as a reminder). For authoritative state, use list_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:

  1. Loading credentials (env or prompt)
  2. Authorizing if no valid tokens are present (auto-detects headless vs loopback)
  3. Printing — or running, with --auto-register — the claude mcp add command
  4. Verifying with a diagnostics round-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 / release flow

TL;DR — pushing to main does nothing on GitHub. No CI run, no package publish, no Docker image build. Releases are 100% manual.

Local-first ritual

Every commit goes through make check on your machine before push:

make check       # lint + format-check + types + tests — the pre-push gate

When to run it:

  • Before every git push. Non-negotiable. The release dispatcher runs the same matrix in CI; if make check fails locally, the release will fail in GitHub too — only slower and noisier.
  • Before tagging a release. release.yml calls ci.yml against the tagged commit; the tag won't publish if the gate fails.
  • After a git pull that touched dependencies or shared modules — to confirm your working tree still passes against the merged code.

The Makefile is also what .github/workflows/ci.yml calls (each CI step shells out to a make target), so what passes locally is what passes in CI. No drift.

Cross-platform type drift + the HTTP smoke aren't part of make check — they're explicit one-off targets:

uv run mypy --platform darwin src tests   # before tagging a release
uv run mypy --platform win32 src tests
make smoke                                # if you touched the HTTP service

Run make (no args) to list every target.

Workflows (all manual-dispatch)

.github/workflows/:

Workflow Trigger When you'd use it What it runs
ci.yml workflow_dispatch + PR + workflow_call Before tagging a release for an extra clean-runner check, or directly via gh workflow run CI. Also called automatically by release.yml as a pre-publish gate. ruff lint + format, mypy --strict, pytest tests/api + tests/unit on Linux × Python 3.11 / 3.12 / 3.13
integration.yml workflow_dispatch + workflow_call When you want to verify the live Inoreader API hasn't drifted under our schemas. Also called automatically by release.yml. Runs the -m integration test suite with creds from the integration GitHub environment.
release.yml workflow_dispatch When you want to ship a new version to PyPI and/or ghcr.io. Calls ci.yml and integration.yml first as gates — neither publish job runs unless both pass. (1) validate-ci: full matrix on the tagged commit. (2) validate-integration: live API on the tagged commit. (3) publish-pypi: build sdist + wheel, push to PyPI. (4) publish-ghcr: build Docker image, push to ghcr.io/ibrapk/inoreader-mcp:<tag> and :latest.

Cutting a release

# 1. Bump version in pyproject.toml + src/inoreader_mcp/__init__.py
#    and write the section in CHANGELOG.md.

# 2. Run the local ritual (above) end to end.

# 3. Tag + push.
git tag v0.3.0
git push origin v0.3.0

# 4. Trigger the release. CI and integration tests run first against
#    the tag; only if both pass does anything get published.
gh workflow run Release \
    -f tag=v0.3.0 \
    -f publish_pypi=true \
    -f publish_ghcr=true

The release runs in 4 jobs:

  1. validate-ci — ruff + mypy + pytest matrix on the tagged commit. ~3 min.
  2. validate-integration — live Inoreader API smoke. ~30 s, but consumes a few zone-1 quota points.
  3. publish-pypi — only fires if both gates passed AND publish_pypi=true.
  4. publish-ghcr — only fires if both gates passed AND publish_ghcr=true.

Escape hatches (use sparingly):

  • -f skip_integration=true — bypasses the live-API gate. Only when Inoreader is provably down and you need to ship an unrelated fix.
  • -f skip_ci=true — bypasses the matrix gate. Only for a config- only release where CI itself is the change.

The image lands at ghcr.io/ibrapk/inoreader-mcp:<tag> and :latest. PyPI shows the version under https://pypi.org/project/inoreader-mcp/.

There is no scheduled trigger anywhere — nothing in this repo fires on cron, on push, or on tag-creation. Every release is a deliberate human click.

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). Pass include_image_urls=True on get_article to keep full Markdown image syntax.
  • get_article truncates at max_chars (default 50 000). The response sets truncated: true and chars_remaining: N so 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 to get_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

make check is the only command you need to remember — it runs lint, format-check, mypy, and the full test suite. Component targets are also available for tighter loops:

make check         # full pre-push gate (run before every git push)
make lint          # ruff check
make format        # ruff format (writes changes)
make types         # mypy on src + tests
make test          # pytest tests/api + tests/unit
make               # list every target

CI calls these same make targets, so anything that passes locally passes in GitHub Actions.

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

inoreader_mcp-0.5.1.tar.gz (278.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

inoreader_mcp-0.5.1-py3-none-any.whl (134.8 kB view details)

Uploaded Python 3

File details

Details for the file inoreader_mcp-0.5.1.tar.gz.

File metadata

  • Download URL: inoreader_mcp-0.5.1.tar.gz
  • Upload date:
  • Size: 278.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

Hashes for inoreader_mcp-0.5.1.tar.gz
Algorithm Hash digest
SHA256 47de39a92c1eb04b09800e834261da3ca4d86a11f5554ada61cd6c209416e637
MD5 0ca5d4bc0c7dbe8cb2a0b164465b2544
BLAKE2b-256 9919a400869e6d82a39977eb3091ae2c3b42e82aa685d4a81713fecdc31202ed

See more details on using hashes here.

File details

Details for the file inoreader_mcp-0.5.1-py3-none-any.whl.

File metadata

  • Download URL: inoreader_mcp-0.5.1-py3-none-any.whl
  • Upload date:
  • Size: 134.8 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

Hashes for inoreader_mcp-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e0a0c4c65581e4227fa78eef96a48904622e0c78ffd81590ea97733f4bf72940
MD5 8e22b5f44c734621f7146b2e29adea8a
BLAKE2b-256 abe916a1420f81432df05c73530dace5b160d1e80d7a381a64f2a65c7cbe239e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page