Metadata-Version: 2.4
Name: inoreader-mcp
Version: 0.6.0
Summary: MCP server for Inoreader — exposes feeds, articles, and curation tools to AI assistants.
Project-URL: Repository, https://github.com/ibrapk/inoreader-mcp
Project-URL: Issues, https://github.com/ibrapk/inoreader-mcp/issues
Author-email: ibrapk <mcp-inoreader.2phgc@8alias.com>
License: MIT
License-File: LICENSE
Keywords: ai,claude,feeds,inoreader,mcp,rss
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: markdownify>=0.11
Requires-Dist: mcp>=1.2
Requires-Dist: platformdirs>=4.0
Requires-Dist: pydantic>=2.5
Provides-Extra: keyring
Requires-Dist: keyring>=24.0; extra == 'keyring'
Provides-Extra: server
Requires-Dist: aiosqlite>=0.20; extra == 'server'
Requires-Dist: authlib>=1.3; extra == 'server'
Requires-Dist: cryptography>=42.0; extra == 'server'
Requires-Dist: itsdangerous>=2.2; extra == 'server'
Requires-Dist: jinja2>=3.1; extra == 'server'
Requires-Dist: starlette>=0.37; extra == 'server'
Requires-Dist: uvicorn>=0.30; extra == 'server'
Description-Content-Type: text/markdown

# 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`](CHANGELOG.md).
> Read those if you want the *why* behind any piece of the codebase.

> **Status:** v0.5.0 — published to [PyPI](https://pypi.org/project/inoreader-mcp/)
> and as a [Claude Desktop `.mcpb` extension](https://github.com/ibrapk/inoreader-mcp/releases/latest).
> 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/`](docs/deploy/).
>
> See [`CHANGELOG.md`](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://localhost: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](https://github.com/ibrapk/inoreader-mcp/releases/latest)
   (`inoreader-mcp-<version>.mcpb`).

3. **Install in Claude Desktop**: Settings → Extensions → scroll to the
   bottom and click **Advanced Settings** → **Install 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`](docs/install-claude-desktop.md).

---

## Other install methods

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

```bash
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/`](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
>
> ```bash
> # 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:
>
> ```bash
> # 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:
>
> ```bash
> # 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`](https://github.com/astral-sh/uv) recommended for local development.

## Local development setup

```bash
# 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](https://www.inoreader.com/all_articles#preferences-developer):

1. **OAuth Redirect URI** must be exactly:
   ```
   http://localhost: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:

```bash
# 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://localhost: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)

```bash
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://localhost: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`:

```bash
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://localhost: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:

```bash
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:

```bash
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:

```bash
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

```bash
# 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:

```bash
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:

```bash
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

```bash
# 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:

```bash
$ 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:

```bash
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](CHANGELOG.md) for the full v0.1.0 feature list.

## License

MIT — see [LICENSE](LICENSE).
