|
2 | 2 |
|
3 | 3 | This project uses **bd** (beads) for issue tracking. Run `bd onboard` to get started. |
4 | 4 |
|
5 | | -## Quick Reference |
| 5 | +## What This Project Is |
| 6 | + |
| 7 | +FreshRSS MCP server using **Streamable HTTP** transport, designed for the **OpenClaw** gateway's `openclaw-mcp-bridge` plugin. It wraps the FreshRSS Google Reader API and exposes 10 MCP tools for RSS feed management. |
| 8 | + |
| 9 | +**Transport**: Streamable HTTP (POST to `/mcp`) -- NOT stdio. The OpenClaw MCP bridge discovers tools via HTTP POST with JSON-RPC 2.0, not by spawning a subprocess. |
| 10 | + |
| 11 | +**Runtime**: Python 3.12+, managed by `uv`. Deployed on NixOS as a systemd service. |
| 12 | + |
| 13 | +## Project Layout |
6 | 14 |
|
7 | | -```bash |
8 | | -bd ready # Find available work |
9 | | -bd show <id> # View issue details |
10 | | -bd update <id> --status in_progress # Claim work |
11 | | -bd close <id> # Complete work |
12 | | -bd sync # Sync with git |
13 | 15 | ``` |
| 16 | +freshrss-mcp/ |
| 17 | + pyproject.toml # uv/PEP 621 — entry point: freshrss-mcp |
| 18 | + uv.lock # Committed lockfile |
| 19 | + .python-version # 3.13 |
| 20 | + flake.nix # Dev shell + NixOS module (systemd service) |
| 21 | + src/freshrss_mcp/ |
| 22 | + server.py # FastMCP entry point (streamable-http transport) |
| 23 | + tools.py # 10 MCP tool definitions with error boundaries |
| 24 | + client.py # FreshRSS Google Reader API client (async, httpx) |
| 25 | + config.py # pydantic-settings config from env vars |
| 26 | + models.py # Article and Feed dataclasses |
| 27 | + tests/ # 67 unit tests |
| 28 | + test_config.py # Config validation, defaults, secret masking |
| 29 | + test_client.py # Auth, feeds, articles, ID extraction, edge cases |
| 30 | + test_tools.py # Every tool happy path + error boundary |
| 31 | + test_models.py # Serialization, construction, mutability |
| 32 | +``` |
| 33 | + |
| 34 | +## Key Architecture Decisions |
14 | 35 |
|
15 | | -## Project Overview |
| 36 | +1. **Lazy authentication**: `client.py` calls `_ensure_authenticated()` before every API method. No need to pre-auth at startup. |
16 | 37 |
|
17 | | -FreshRSS MCP server using **Streamable HTTP** transport for OpenClaw integration. |
| 38 | +2. **Error boundaries**: Every tool in `tools.py` catches all exceptions and returns `"Error: ..."` strings. MCP protocol never sees uncaught exceptions. |
18 | 39 |
|
19 | | -**Key files:** |
20 | | -- `src/freshrss_mcp/server.py` — Entry point, FastMCP with streamable-http |
21 | | -- `src/freshrss_mcp/tools.py` — All MCP tool definitions |
22 | | -- `src/freshrss_mcp/client.py` — FreshRSS Google Reader API client |
23 | | -- `src/freshrss_mcp/config.py` — pydantic-settings config from env vars |
24 | | -- `tests/` — 67 tests across config, client, tools, and models |
| 40 | +3. **pydantic-settings**: `config.py` uses `BaseSettings` with `SecretStr` for the password. Missing env vars produce clear validation errors at startup. |
| 41 | + |
| 42 | +4. **Single client instance**: `FreshRSSClient` is created once in `server.py` and shared across all tool calls. No per-call client creation. |
| 43 | + |
| 44 | +5. **No version pins**: `pyproject.toml` has no version constraints on dependencies, per the MCP build spec. |
| 45 | + |
| 46 | +## How to Run |
25 | 47 |
|
26 | | -**Running:** |
27 | 48 | ```bash |
28 | | -uv sync && uv run pytest -v # test |
29 | | -uv run freshrss-mcp # start server |
| 49 | +# Dev |
| 50 | +uv sync && uv run pytest -v |
| 51 | + |
| 52 | +# Start server (needs env vars) |
| 53 | +export FRESHRSS_URL="https://freshrss.trailertrash.io" |
| 54 | +export FRESHRSS_USERNAME="chrisf" |
| 55 | +export FRESHRSS_PASSWORD="..." |
| 56 | +export MCP_SERVER_PORT=8765 |
| 57 | +uv run freshrss-mcp |
| 58 | + |
| 59 | +# Server binds to http://127.0.0.1:8765/mcp |
30 | 60 | ``` |
31 | 61 |
|
32 | | -**Transport:** Streamable HTTP (not stdio). OpenClaw's `openclaw-mcp-bridge` plugin connects via HTTP POST to `/mcp`. |
| 62 | +## NixOS Integration |
| 63 | + |
| 64 | +The flake exports `nixosModules.default`. Host config (rvbee) at `/home/chrisf/build/config`: |
| 65 | + |
| 66 | +```nix |
| 67 | +# In flake.nix inputs: |
| 68 | +freshrss-mcp.url = "github:ChrisLAS/freshrss-mcp"; |
| 69 | +freshrss-mcp.inputs.nixpkgs.follows = "nixpkgs"; |
| 70 | +
|
| 71 | +# In rvbee modules: |
| 72 | +freshrss-mcp.nixosModules.default |
| 73 | +
|
| 74 | +# In services config: |
| 75 | +services.freshrss-mcp-server = { |
| 76 | + enable = true; |
| 77 | + freshRssUrl = "https://freshrss.trailertrash.io"; |
| 78 | + username = "chrisf"; |
| 79 | + passwordFile = "/home/chrisf/.config/secrets/freshrss-mcp"; |
| 80 | + port = 3005; |
| 81 | + host = "0.0.0.0"; |
| 82 | +}; |
| 83 | +``` |
| 84 | + |
| 85 | +The password file must be an env file: `FRESHRSS_PASSWORD=<value>`. |
| 86 | + |
| 87 | +## OpenClaw Bridge Config |
| 88 | + |
| 89 | +The server is consumed by the `openclaw-mcp-bridge` plugin in `~/.openclaw/openclaw.json`: |
| 90 | + |
| 91 | +```json |
| 92 | +{ |
| 93 | + "plugins": { |
| 94 | + "entries": { |
| 95 | + "openclaw-mcp-bridge": { |
| 96 | + "config": { |
| 97 | + "servers": [ |
| 98 | + { |
| 99 | + "name": "FreshRSS", |
| 100 | + "url": "http://127.0.0.1:3005", |
| 101 | + "prefix": "freshrss" |
| 102 | + } |
| 103 | + ] |
| 104 | + } |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +Tools appear as: `freshrss_list_feeds`, `freshrss_get_unread_articles`, etc. |
| 112 | + |
| 113 | +## Quick Reference |
| 114 | + |
| 115 | +```bash |
| 116 | +bd ready # Find available work |
| 117 | +bd show <id> # View issue details |
| 118 | +bd update <id> --status in_progress # Claim work |
| 119 | +bd close <id> # Complete work |
| 120 | +bd sync # Sync with git |
| 121 | +``` |
33 | 122 |
|
34 | 123 | ## Landing the Plane (Session Completion) |
35 | 124 |
|
36 | | -**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. |
37 | | - |
38 | | -**MANDATORY WORKFLOW:** |
39 | | - |
40 | | -1. **File issues for remaining work** - Create issues for anything that needs follow-up |
41 | | -2. **Run quality gates** (if code changed) - Tests, linters, builds |
42 | | -3. **Update issue status** - Close finished work, update in-progress items |
43 | | -4. **PUSH TO REMOTE** - This is MANDATORY: |
44 | | - ```bash |
45 | | - git pull --rebase |
46 | | - bd sync |
47 | | - git push |
48 | | - git status # MUST show "up to date with origin" |
49 | | - ``` |
50 | | -5. **Clean up** - Clear stashes, prune remote branches |
51 | | -6. **Verify** - All changes committed AND pushed |
52 | | -7. **Hand off** - Provide context for next session |
53 | | - |
54 | | -**CRITICAL RULES:** |
55 | | -- Work is NOT complete until `git push` succeeds |
56 | | -- NEVER stop before pushing - that leaves work stranded locally |
57 | | -- NEVER say "ready to push when you are" - YOU must push |
58 | | -- If push fails, resolve and retry until it succeeds |
| 125 | +When ending a session, you MUST: |
| 126 | + |
| 127 | +1. File issues for remaining work |
| 128 | +2. Run quality gates: `uv run pytest -v` and `ruff check src/ tests/` |
| 129 | +3. Update issue status via `bd close` |
| 130 | +4. Push to remote: |
| 131 | + ```bash |
| 132 | + git pull --rebase && bd sync && git push |
| 133 | + git status # MUST show "up to date with origin" |
| 134 | + ``` |
| 135 | + |
| 136 | +Work is NOT complete until `git push` succeeds. |
0 commit comments