Metadata-Version: 2.4
Name: arr-stack-mcp
Version: 0.2.0
Summary: MCP server giving any MCP-capable agent a clean, intelligent, unified interface to Sonarr, Radarr, Lidarr, Prowlarr, and Jellyfin.
Project-URL: Homepage, https://github.com/new-usemame/arr-stack-mcp
Project-URL: Repository, https://github.com/new-usemame/arr-stack-mcp
Project-URL: Issues, https://github.com/new-usemame/arr-stack-mcp/issues
Project-URL: Changelog, https://github.com/new-usemame/arr-stack-mcp/blob/main/CHANGELOG.md
Author-email: new-usemame <248195428+new-usemame@users.noreply.github.com>
License: MIT License
        
        Copyright (c) 2026 new-usemame
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: claude,jellyfin,lidarr,llm,mcp,prowlarr,radarr,sonarr
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia
Classifier: Topic :: System :: Networking
Requires-Python: >=3.12
Requires-Dist: anyio<5,>=4.6
Requires-Dist: httpx<1,>=0.28
Requires-Dist: mcp<2,>=1.27
Requires-Dist: pydantic-settings<3,>=2.6
Requires-Dist: pydantic<3,>=2.10
Requires-Dist: python-dateutil<3,>=2.9
Requires-Dist: pyyaml<7,>=6.0
Requires-Dist: rapidfuzz<4,>=3.10
Requires-Dist: structlog<26,>=25.5
Requires-Dist: typer<1,>=0.25
Description-Content-Type: text/markdown

# arr-stack-mcp

A Model Context Protocol server that exposes Sonarr, Radarr, Lidarr, and Jellyfin to any MCP client (Claude Desktop, Claude Code, n8n, ibis-bot, custom agents) through a hand-curated tool layer written for LLM consumption.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/release/python-3120/)

## What this gives an agent

28 curated tools split across four services:

| Service | Tools | Highlights |
|---|---|---|
| Sonarr | 8 | series search, TVDB lookup, queue, calendar, missing episodes, add/delete |
| Radarr | 8 | movie search, TMDB lookup, queue, calendar, missing, add/delete |
| Lidarr | 7 | artist + album search, MusicBrainz lookup, queue, add/delete |
| Jellyfin | 5 | library search, recent additions, now-playing sessions, library scan |

Every tool:

- Has a description written for an agent's decision-making, not for human docs.
- Pins its input and output with Pydantic v2, so the MCP catalogue carries strict JSON Schema.
- Returns a compact, self-describing envelope (`ok`, `query`, `count`, `total`, `items`).
- Translates upstream HTTP errors into structured `ToolError` envelopes with self-suggesting hints.
- Destructive operations require a two-call confirm-token flow.

## Quickstart

### uvx (one-shot)

```
uvx arr-stack-mcp serve --transport stdio
```

### Docker

```
docker run --rm -i \
  -e SONARR_URL=http://host.docker.internal:8989 \
  -e SONARR_API_KEY=$SONARR_API_KEY \
  -e RADARR_URL=http://host.docker.internal:7878 \
  -e RADARR_API_KEY=$RADARR_API_KEY \
  -e LIDARR_URL=http://host.docker.internal:8686 \
  -e LIDARR_API_KEY=$LIDARR_API_KEY \
  -e JELLYFIN_URL=http://host.docker.internal:8096 \
  -e JELLYFIN_API_KEY=$JELLYFIN_API_KEY \
  ghcr.io/new-usemame/arr-stack-mcp:0.1.0
```

### Local development (uv)

```
git clone https://github.com/new-usemame/arr-stack-mcp
cd arr-stack-mcp
uv sync
uv run arr-stack-mcp init           # writes a starter arr-stack-mcp.yaml
uv run arr-stack-mcp serve          # boots stdio MCP server
```

### Claude Desktop config

Add to `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, or `%APPDATA%\Claude\claude_desktop_config.json` on Windows:

```json
{
  "mcpServers": {
    "arr-stack": {
      "command": "uvx",
      "args": ["arr-stack-mcp", "serve"],
      "env": {
        "SONARR_URL": "http://localhost:8989",
        "SONARR_API_KEY": "...",
        "RADARR_URL": "http://localhost:7878",
        "RADARR_API_KEY": "...",
        "LIDARR_URL": "http://localhost:8686",
        "LIDARR_API_KEY": "...",
        "JELLYFIN_URL": "http://localhost:8096",
        "JELLYFIN_API_KEY": "..."
      }
    }
  }
}
```

## Config reference

`arr-stack-mcp init` writes a starter `arr-stack-mcp.yaml`. Every secret can also be supplied via env var. Inline expansion of `${VAR}` and `${VAR:-default}` is supported.

```yaml
log_level: info

transport:
  stdio: true
  http_enabled: false
  http_host: 127.0.0.1
  http_port: 8080

policy:
  read_only: false
  disable_destructive: false
  confirm_token_ttl_seconds: 300

services:
  sonarr:
    enabled: true
    url: http://localhost:8989
    api_key: ${SONARR_API_KEY}
    verify_tls: true
    timeout_seconds: 30
  radarr:
    enabled: true
    url: http://localhost:7878
    api_key: ${RADARR_API_KEY}
  lidarr:
    enabled: true
    url: http://localhost:8686
    api_key: ${LIDARR_API_KEY}
  jellyfin:
    enabled: true
    url: http://localhost:8096
    api_key: ${JELLYFIN_API_KEY}
    default_user_id: 4f8b6d2e-3a9c-4f1d-9c2a-1b3c5d7e9f01
```

### Runtime flags

| Flag | Env | Effect |
|---|---|---|
| `--read-only` | `ARRSTACK_READ_ONLY=true` | Skip every tool tagged `write` or `destructive` at registration. |
| `--disable-destructive` | `ARRSTACK_DISABLE_DESTRUCTIVE=true` | Skip only the `destructive` tools (deletes). |
| `--dry-run` | — | Plan-and-record mode (v0.2). Write/destructive tools short-circuit before the upstream mutation and log the intended payload to a ring buffer surfaced by `stack.dryrun_log`. Reads run normally. |
| `--transport stdio` | — | Default. Use for Claude Desktop and Claude Code. |
| `--transport streamable-http` | — | HTTP transport for n8n / remote consumers. |
| `--bearer-token-env ARR_STACK_MCP_BEARER_TOKEN` | (env name) | Env var the server reads the bearer token from. Streamable-HTTP on a non-loopback bind refuses to start when this env var is unset. |
| `--config path/to/config.yaml` | — | Override config path. Defaults to `./arr-stack-mcp.yaml`, then XDG. |

### Confirm-token persistence (v0.2)

Confirm tokens default to in-memory storage. Set `policy.state_db_path` in the config to persist tokens across server restarts and across streamable-HTTP request lifetimes:

```yaml
policy:
  state_db_path: ~/.local/state/arr-stack-mcp/state.db
```

The path is created idempotently. SQLite WAL mode lets multiple worker processes share the same DB safely.

### Hourly per-tool call cap (v0.2)

Opt-in runaway-agent protection. Tools without an entry are uncapped:

```yaml
policy:
  hourly_caps:
    sonarr.series_add: 50
    radarr.movie_add: 50
    sonarr.series_delete: 10
```

Per-process, per-tool, rolling-hour. Failed upstream calls still consume budget so a runaway loop hits the cap on the Nth attempt.

## Capability matrix

Tools follow the `<service>.<verb_object>` convention. Tags drive flag-based gating.

### Sonarr

| Tool | Tag | Use |
|---|---|---|
| `sonarr.system_status` | read | Version + branch + uptime. First call when diagnosing. |
| `sonarr.series_search` | read | Search the existing library (already-added). |
| `sonarr.series_lookup` | read | Search TVDB to discover series to add. |
| `sonarr.series_status` | read | Per-season breakdown (v0.2). Episode counts, files on disk, missing. |
| `sonarr.queue` | read | Active downloads with progress. |
| `sonarr.calendar` | read | Upcoming + recently-aired episodes. |
| `sonarr.missing` | read | Monitored episodes not on disk. |
| `sonarr.series_add` | write | Add a series by TVDB id. Idempotent. |
| `sonarr.series_delete` | destructive | Two-call confirm-token flow. |

### Radarr

| Tool | Tag | Use |
|---|---|---|
| `radarr.system_status` | read | Mirrors Sonarr. |
| `radarr.movie_search` | read | Search the existing library. |
| `radarr.movie_lookup` | read | Search TMDB to discover movies to add. |
| `radarr.queue` | read | Active downloads. |
| `radarr.calendar` | read | Upcoming + recently-released movies. |
| `radarr.missing` | read | Monitored movies not on disk. |
| `radarr.movie_add` | write | Add a movie by TMDB id. Idempotent. |
| `radarr.movie_delete` | destructive | Two-call confirm-token flow. |

### Lidarr

| Tool | Tag | Use |
|---|---|---|
| `lidarr.system_status` | read | — |
| `lidarr.artist_search` | read | Search the existing library. |
| `lidarr.artist_lookup` | read | Search MusicBrainz to discover artists. |
| `lidarr.artist_albums` | read | List albums under one artist. |
| `lidarr.queue` | read | Active downloads. |
| `lidarr.artist_add` | write | Add an artist by MusicBrainz id. Idempotent. |
| `lidarr.artist_delete` | destructive | Two-call confirm-token flow. |

### Jellyfin

| Tool | Tag | Use |
|---|---|---|
| `jellyfin.system_info` | read | Version + server name. Public endpoint. |
| `jellyfin.library_search` | read | Search items by name. Pass `user_id=` for per-user scoping (v0.2). |
| `jellyfin.recent_additions` | read | Newest items by date_added. Pass `user_id=` for per-user scoping (v0.2). |
| `jellyfin.users_list` | read | Enumerate user accounts (v0.2). Returns `user_id`, name, admin flag, last login. |
| `jellyfin.now_playing` | read | Currently-active sessions with progress. |
| `jellyfin.scan_library` | write | Trigger a library refresh. |

### Prowlarr (v0.2)

| Tool | Tag | Use |
|---|---|---|
| `prowlarr.system_status` | read | Version + branch. First call when diagnosing Prowlarr. |
| `prowlarr.health` | read | Health-check issues (warnings, errors, wiki links). |
| `prowlarr.indexer_list` | read | Configured indexers with implementation, protocol, priority, enable. |
| `prowlarr.indexer_stats` | read | Query / grab / failure counts per indexer + average response time. |
| `prowlarr.indexer_status` | read | Currently-failing indexers with retry-after timestamps. |
| `prowlarr.indexer_test_all` | write | Trigger the test-every-indexer probe; returns per-indexer pass/fail. |
| `prowlarr.search` | read | Search across configured indexers. Returns title, indexer, size, age, seeders. Does NOT download — use `*.add` on Sonarr / Radarr / Lidarr with catalog id. |

### stack.* (v0.2, cross-service)

| Tool | Tag | Use |
|---|---|---|
| `stack.health` | read | Probe every enabled service's `*.system_status` in parallel. Returns reachability matrix + `overall_ok`. |
| `stack.dryrun_log` | read | Read the ring buffer of would-have-fired mutations recorded under `--dry-run`. |
| `stack.report_issue` | read | Compose a pre-filled GitHub issue URL the user can post upstream. Never auto-submits. |
| `stack.find_anywhere` | read | Fan a query across every enabled arr + Jellyfin library; merged result with `source` per row. |
| `stack.queue_status_all` | read | Aggregate download queue across Sonarr, Radarr, Lidarr. Normalized row shape. |

## Confirm-token flow

Destructive tools (`*_delete`) implement a two-call confirm:

```
# Call 1: returns plan + token, no side effect
> sonarr.series_delete(sonarr_id=42, delete_files=False)
{
  "ok": true,
  "needs_confirm": true,
  "confirm_token": "abc123",
  "summary": "remove 'Foo Bar' (sonarr_id=42) from Sonarr; keep files on disk",
  "expires_in_seconds": 300
}

# Call 2: executes
> sonarr.series_delete(sonarr_id=42, delete_files=False, confirm_token="abc123")
{
  "ok": true,
  "deleted_sonarr_id": 42,
  "title": "Foo Bar",
  "files_deleted": false,
  "msg": "deleted sonarr_id=42"
}
```

Tokens are single-use, time-limited, and bound to the request payload — a token from one tool will not confirm another.

## Architecture

Two layers:

```
┌────────────────────────────────────────────────────────────┐
│  Curated MCP tools (arr_stack_mcp.tools.<service>.tools)  │
│  Hand-written, LLM-friendly names, descriptions, schemas. │
├────────────────────────────────────────────────────────────┤
│  Generated thin clients (arr_stack_mcp.generated.<svc>)   │
│  openapi-python-client output. Do not hand-edit; regen    │
│  via scripts/regen-clients.sh on upstream version bumps.  │
├────────────────────────────────────────────────────────────┤
│  httpx + pydantic + structlog + mcp.server.fastmcp         │
└────────────────────────────────────────────────────────────┘
```

See `docs/ARCHITECTURE.md` for the longer walk-through.

## Development

```
uv sync                              # install runtime + dev deps
uv run pytest -q                     # fast unit tests
uv run ruff check src/ tests/        # lint
uv run mypy src/ tests/              # type check (strict)
uv run ruff format src/ tests/       # format
```

Integration tests require the docker test stack:

```
scripts/test-stack-up.sh             # boots Sonarr/Radarr/Lidarr/Prowlarr/Jellyfin
uv run pytest -m integration         # runs once stack is healthy
scripts/test-stack-down.sh --clean   # teardown
```

## Contributing

See `CONTRIBUTING.md` for the OpenAPI regeneration flow and the verification checklist that gates every release.

## License

MIT
