Skip to main content
Glama
zhiyuan7

FreshRSS MCP Server for AI Agents

by zhiyuan7
README.md
# FreshRSS MCP Server for AI Agents

An MCP server that wraps the FreshRSS Google Reader API, exposing feed management, safe full-article extraction, and YouTube transcript retrieval as tools for AI agents. It uses **Streamable HTTP** transport and works with Codex and other MCP clients.

Token-optimized: returns only essential fields with configurable summary truncation, achieving ~90% reduction vs raw RSS XML payloads.

This repository is derived from [ChrisLAS/freshrss-mcp](https://github.com/ChrisLAS/freshrss-mcp), created by Chris Fisher and released under the MIT License. The original copyright and license are preserved. See [NOTICE](NOTICE.md) for the upstream attribution and a summary of the added functionality.

## Added agent-reading capabilities

- `fetch_full_article`: extracts readable text from public article pages with redirect, size, content-type, and SSRF protections.
- `get_youtube_transcript`: retrieves existing YouTube captions through Supadata, optionally permits paid AI transcription, and caches results locally.
- Docker deployment for small always-on hosts such as Raspberry Pi.
- Tests for URL validation, secret masking, article extraction, transcript normalization, and tool error boundaries.

---

## NixOS Installation

The flake exports a NixOS module that runs the server as a hardened systemd service.

### 1. Add the flake input

In your host's `flake.nix`:

```nix
inputs = {
  freshrss-mcp.url = "github:ChrisLAS/freshrss-mcp";
  freshrss-mcp.inputs.nixpkgs.follows = "nixpkgs";
};
```

Pass it through to your NixOS configuration:

```nix
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
  modules = [
    ./system.nix
    freshrss-mcp.nixosModules.default
  ];
};
```

### 2. Configure the service

In your host config (e.g. `system.nix`):

```nix
services.freshrss-mcp-server = {
  enable = true;
  freshRssUrl = "https://freshrss.example.com";
  username = "youruser";
  passwordFile = "/home/youruser/.config/secrets/freshrss-mcp";
  port = 3005;
  host = "0.0.0.0";  # or "127.0.0.1" for loopback only
};
```

The password file must use systemd EnvironmentFile format:

```
FRESHRSS_PASSWORD=your-api-password-here
```

### 3. Rebuild

```bash
sudo nixos-rebuild switch
systemctl status freshrss-mcp-server  # verify it's running
```

The service runs as a sandboxed DynamicUser with hardened security settings.

---

## OpenClaw Gateway Setup

Register the FreshRSS MCP server in the `openclaw-mcp-bridge` plugin inside `~/.openclaw/openclaw.json`. Add a server entry to the existing `servers` array:

```json
{
  "plugins": {
    "entries": {
      "openclaw-mcp-bridge": {
        "enabled": true,
        "config": {
          "servers": [
            {
              "name": "FreshRSS",
              "url": "http://127.0.0.1:3005",
              "prefix": "freshrss"
            }
          ]
        }
      }
    }
  }
}
```

Then restart the gateway: `openclaw gateway restart`

Tools will appear with the `freshrss_` prefix, e.g. `freshrss_list_feeds`, `freshrss_get_unread_articles`.

---

## For AI Agents: Setup & Installation Guide

This section is written for agentic LLM sessions that need to deploy, configure, or extend this server.

### Quick Start (non-NixOS)

```bash
# Clone
git clone https://github.com/ChrisLAS/freshrss-mcp.git
cd freshrss-mcp

# Install deps (requires Python 3.12+ and uv)
uv sync

# Set required env vars
export FRESHRSS_URL="https://freshrss.example.com"
export FRESHRSS_USERNAME="youruser"
export FRESHRSS_PASSWORD="yourpass"

# Start server (binds to 127.0.0.1:8000/mcp by default)
uv run freshrss-mcp

# Override host/port
export MCP_SERVER_HOST="0.0.0.0"
export MCP_SERVER_PORT=3005
uv run freshrss-mcp
```

### Docker

```bash
docker build -t freshrss-mcp .
docker run --rm -p 127.0.0.1:8000:8000 \
  -e FRESHRSS_URL="https://freshrss.example.com" \
  -e FRESHRSS_USERNAME="youruser" \
  -e FRESHRSS_PASSWORD="your-api-password" \
  -e MCP_SERVER_HOST="0.0.0.0" \
  -v freshrss-mcp-cache:/cache \
  freshrss-mcp
```

Add `SUPADATA_API_KEY` only if you want the YouTube transcript tool. Never commit API keys or FreshRSS credentials to the repository.

### Quick Start (NixOS)

```bash
# Enter dev shell (provides uv, python, ruff)
nix develop

# Install deps and run tests
uv sync
uv run pytest -v

# Start the server
export FRESHRSS_URL="https://freshrss.example.com"
export FRESHRSS_USERNAME="youruser"
export FRESHRSS_PASSWORD="yourpass"
uv run freshrss-mcp
```

On NixOS, the Nix devshell sets `UV_PYTHON_DOWNLOADS=never` and `UV_PYTHON` automatically to avoid dynamically linked binary issues.

### Verify the Server

```bash
# List tools via JSON-RPC
curl -s http://127.0.0.1:8000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq .

# Call a tool
curl -s http://127.0.0.1:8000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_feeds","arguments":{}}}' | jq .

# MCP Inspector (interactive)
npx @modelcontextprotocol/inspector --url http://127.0.0.1:8000/mcp
```

### Environment Variables

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `FRESHRSS_URL` | Yes | — | FreshRSS instance URL |
| `FRESHRSS_USERNAME` | Yes | — | FreshRSS username |
| `FRESHRSS_PASSWORD` | Yes | — | FreshRSS API password |
| `FRESHRSS_API_PATH` | No | `/api/greader.php` | Google Reader API path |
| `SUPADATA_API_KEY` | No | — | Enables YouTube captions and optional AI transcription via Supadata |
| `MCP_TRANSCRIPT_CACHE_DIR` | No | `/cache` | Persistent transcript cache directory |
| `MCP_SERVER_HOST` | No | `127.0.0.1` | Bind address |
| `MCP_SERVER_PORT` | No | `8000` | Bind port |

### Available Tools

| Tool | Description | Key Args |
|------|-------------|----------|
| `fetch_full_article` | Safely extract readable text from a public article URL | `url`, `max_chars` |
| `get_youtube_transcript` | Retrieve and cache YouTube captions or authorized AI transcription | `video_url`, `language`, `max_chars`, `allow_ai_generation` |
| `get_unread_articles` | Fetch unread articles with filtering | `limit`, `feed_ids`, `since_timestamp`, `max_summary_length` |
| `get_articles_by_feed` | Articles from a specific feed | `feed_id`, `limit`, `include_read` |
| `search_articles` | Client-side keyword search in titles/summaries | `query`, `limit`, `feed_ids` |
| `list_feeds` | All subscribed feeds with unread counts | — |
| `get_feed_info` | Detailed info for one feed | `feed_id` |
| `get_feed_stats` | Statistics for all feeds | — |
| `mark_as_read` | Batch mark articles as read | `article_ids` |
| `mark_as_unread` | Batch mark articles as unread | `article_ids` |
| `star_article` | Star/favorite an article | `article_id` |
| `unstar_article` | Remove star from an article | `article_id` |

### Architecture Notes

- **Transport**: Streamable HTTP (POST `/mcp`), not stdio. The OpenClaw `openclaw-mcp-bridge` plugin discovers tools via HTTP.
- **Auth**: Lazy authentication — the FreshRSS client authenticates on the first API call, not at startup.
- **Error handling**: Every tool catches all exceptions and returns `"Error: ..."` strings. MCP protocol never sees uncaught exceptions.
- **Config**: pydantic-settings `BaseSettings` with `SecretStr` for the password. Validation happens at startup.
- **Dependencies**: `aiohttp`, `fastmcp`, `httpx`, `pydantic-settings`, and `trafilatura`. No version pins.
- **Secrets**: FreshRSS and Supadata credentials are read from environment variables and represented with `SecretStr`.
- **Tests**: Unit tests cover config, client, tools, models, article extraction, and transcript handling. Run with `uv run pytest -v`.

### Project Structure

```
src/freshrss_mcp/
  server.py    — FastMCP entry point, signal handlers, streamable-http transport
  tools.py     — MCP tool definitions with error boundaries
  client.py    — Async FreshRSS Google Reader API client (httpx)
  config.py    — pydantic-settings config from env vars
  models.py    — Article and Feed dataclasses
  article_fetcher.py — public-only HTTP fetch and readable-text extraction
  youtube_transcript.py — Supadata transcript client and persistent cache
tests/
  test_config.py   — Config validation, defaults, secret masking
  test_client.py   — Auth, feeds, articles, ID extraction
  test_tools.py    — Tool happy paths + error boundaries
  test_models.py   — Serialization, construction, edge cases
  test_article_fetcher.py — URL safety, redirect, and extraction behavior
  test_youtube_transcript.py — YouTube URL parsing and transcript normalization
```

## Known Limitations

- **Client-side search**: FreshRSS API lacks server-side search; `search_articles` fetches articles then filters locally.
- **No pagination**: Article fetches use a single `limit` parameter without cursor-based pagination.
- **No real-time updates**: The server is request-driven; no push/webhook mechanism for new articles.

## License

MIT. The upstream copyright notice remains in [LICENSE](LICENSE); modification details are in [NOTICE](NOTICE.md).