Typed Python client + MCP server + declarative config sync for Radarr, Sonarr, and Prowlarr
Project description
arr-py-client
Typed Python client + MCP server + declarative config sync for Radarr, Sonarr, and Prowlarr. One SDK for scripts, one server for LLM agents, one YAML pipeline for "make my stack match this file."
Highlights
Fully typed, fully async-ready SDK.
Pydantic v2 models, httpx transport, complete Radarr v3 + Sonarr v3 +
Prowlarr v1 endpoint coverage, sync and async mirrors of every
operation. Ships py.typed so mypy / basedpyright / Pylance pick it
up immediately.
Drop-in MCP server for Claude, Cursor, and custom agents.
arr-py-mcp exposes 60+ tools over stdio or streamable-HTTP — the LLM
can list movies, explain a release grade, run a queue janitor, or sync
declarative config without any bespoke tool plumbing on your side.
Every response carries _meta.action_hints so chains self-suggest
their next call.
Multi-tenant out of the box.
One environment-var provider for solo use; a ClientProvider +
OAuth 2.1 path for SaaS deployments where every user has their own
stack. Per-tool scopes, RFC 6750 WWW-Authenticate headers, and an
audit= hook are already wired.
Declarative config sync (Recyclarr-style, in Python).
Describe tags, custom formats, and quality profiles in YAML (or JSON /
TOML). Call plan() to preview, apply() to converge — same
desired-state model for Radarr and Sonarr.
Workflow primitives that are boring on purpose.
queue.janitor() (policy-based cleanup with named bundles),
library.backfill() (rate-limited missing-content search with
.estimate()), releases.explain() (human-readable grading +
.advice()), arr_health() (cross-brand rollup). No LLM required —
they're ordinary Python you can schedule from cron.
Webhook receivers. Typed events from Radarr / Sonarr, plumbed through FastAPI or plain WSGI with zero extra deps.
Testing utilities.
make_fake_radarr() / make_fake_sonarr() for in-process fakes and an
@replay(...) decorator for record-on-miss / replay-on-hit against
real instances.
Small, focused CLI.
arr-py handles status and basic add; workflows deliberately live in
Python + MCP, not shell.
Python 3.11 – 3.14.
Install
pip install arr-py-client # core SDK
pip install arr-py-client[mcp] # + MCP server
pip install arr-py-client[config] # + YAML loader for config_sync
pip install arr-py-client[webhooks] # + FastAPI receiver helper
Works with pip, uv, pipx, and any PEP 621 installer.
Quickstart
SDK
from arr_py_client import RadarrClient
with RadarrClient(base_url="http://radarr:7878", api_key="YOUR_KEY") as client:
for m in client.movies.list()[:5]:
print(m.id, m.title, m.year)
Or let it read RADARR_BASE_URL / RADARR_API_KEY from the environment
(or a .env file):
from arr_py_client import RadarrClient
with RadarrClient() as client:
print(len(client.movies.list()))
Async twins ship for every client — AsyncRadarrClient,
AsyncSonarrClient, AsyncProwlarrClient:
import asyncio
from arr_py_client import AsyncSonarrClient
async def main() -> None:
async with AsyncSonarrClient() as client:
series = await client.series.list()
print(len(series), "shows")
asyncio.run(main())
MCP server (stdio)
Point Claude Desktop, Cursor, or any MCP client at a local process:
pip install arr-py-client[mcp]
export RADARR_BASE_URL=http://radarr:7878 RADARR_API_KEY=...
export SONARR_BASE_URL=http://sonarr:8989 SONARR_API_KEY=...
export PROWLARR_BASE_URL=http://prowlarr:9696 PROWLARR_API_KEY=... # optional
arr-py-mcp # stdio — register with Claude / Cursor
arr-py-mcp --transport http # streamable-http for remote clients
MCP server (Docker)
Prebuilt image on every release. Smoke-test in one command:
docker run --rm -p 3000:3000 \
-e RADARR_BASE_URL -e RADARR_API_KEY \
-e SONARR_BASE_URL -e SONARR_API_KEY \
-e PROWLARR_BASE_URL -e PROWLARR_API_KEY \
ghcr.io/allada-homelab/arr-py-client:latest
Or use the bundled compose file (reads .env from the repo root):
just mcp-up # docker compose up -d
curl -sSL -X POST http://localhost:3000/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"arr_health","arguments":{}}}'
Tags: latest, {version}, {major}.{minor}, {major}, plus
sha-<short> for immutable pinning. Full walk-through:
docs/guides/docker-deployment.md.
Features in depth
Typed client
Every request and response is a pydantic v2 model. Fields unknown to the
client are preserved — a schema drift on the server side won't crash
your code. Retries, url_base prefixes, custom httpx clients, and
per-call timeouts are all first-class.
from arr_py_client import RadarrClient
with RadarrClient(base_url="http://radarr:7878", api_key="k") as client:
movie = client.movies.get(id=42) # pydantic model, not dict
movie.monitored = True
client.movies.put(id=42, body=movie)
MCP server
60+ tools, one of three transports (stdio, streamable-HTTP, SSE for legacy clients). Tools split into three groups:
- Per-brand —
radarr_*,sonarr_*,prowlarr_*mirror the HTTP API surface (list / get / system status / queue / blocklist / logs). - Composed —
arr_*cross-brand workflows:arr_health,arr_sync_plan,arr_sync_apply,arr_backfill,arr_janitor_run,arr_explain_grab. - Describe / reference —
arr_describe_fields,arr_list_instances,arr_list_resourcesso the LLM can discover the schema at call time.
Every list / get tool returns {"data": ..., "_meta": {...}} with
action_hints pointing at the next plausible tool call — LLM chains
self-navigate without a giant system prompt.
Multi-tenant? Implement a ClientProvider, wire a TokenVerifier, and
mount the resulting ASGI app under /mcp:
from fastapi import FastAPI
from arr_py_client.mcp import (
build_server, CallbackTokenVerifier, InMemoryCachedProvider,
)
class MyProvider(InMemoryCachedProvider):
async def identity(self, ctx):
return ctx.principal.id
async def build_client(self, ctx, brand, instance_id, identity):
# lookup encrypted creds in your DB, return AsyncRadarrClient(...)
...
mcp = build_server(
provider=MyProvider(),
token_verifier=CallbackTokenVerifier(verify=app_auth.to_principal),
auth=AuthSettings(issuer_url=..., resource_server_url=...),
)
app = FastAPI()
app.mount("/mcp", mcp.streamable_http_app())
Full guide: docs/guides/mcp-multi-tenant.md.
Declarative config sync
Describe the desired state once; plan tells you what would change;
apply converges. Works for tags, custom formats, quality profiles.
# config.yaml
tags: [4k, anime, kids]
custom_formats:
- name: x265
specifications:
- name: x265
implementation: ReleaseTitleSpecification
required: true
fields: [{ name: value, value: "(h|x).?265" }]
quality_profiles:
- name: HD-Bluray
upgradeAllowed: true
cutoff: 7
formatItems:
- { name: x265, score: -10000 }
from arr_py_client import RadarrClient
from arr_py_client.config_sync import load, plan, apply
with RadarrClient() as client:
desired = load("config.yaml")
plan_ = plan(client, desired)
print(plan_.summary())
apply(client, plan_, dry_run=False)
Full schema + more examples: docs/examples/config-sync/.
Queue janitor
Named policy bundles for common opinions; BYO policies for the rest.
from arr_py_client import RadarrClient, POLICIES
with RadarrClient() as client:
report = client.queue.janitor(
policies=POLICIES.default, # .conservative | .aggressive | .ratio_preserving
protected_trackers=("private-tracker.example",),
dry_run=False,
)
print(report.total_matches)
Webhooks
Parse-only:
from arr_py_client.webhooks import parse_event, OnGrab
event = parse_event(request.json())
if isinstance(event, OnGrab):
notify(f"Grabbed {event.movie.title if event.movie else '?'}")
FastAPI:
from fastapi import FastAPI
from arr_py_client.webhooks import fastapi_router
app = FastAPI()
app.include_router(fastapi_router(on_event), prefix="/webhooks/arr")
Zero-dep WSGI:
from wsgiref.simple_server import make_server
from arr_py_client.webhooks import wsgi_app
make_server("0.0.0.0", 9000, wsgi_app(on_event)).serve_forever() # noqa: S104
How it compares
| arr-py-client | pyarr | Recyclarr | |
|---|---|---|---|
| Pydantic v2 models | yes | no (dicts) | n/a |
| Async | yes | no | n/a |
| Radarr / Sonarr v3 coverage | yes | yes | partial (config only) |
| Prowlarr v1 coverage | yes | yes | yes |
| Lidarr / Readarr | planned | yes | yes |
| MCP server | yes (60+ tools) | no | no |
| Declarative config sync | yes (YAML/JSON/TOML) | no | yes |
| Queue janitor / backfill / release explain | yes | no | no |
| Webhook receiver helper | yes | no | no |
Ships py.typed |
yes | no | n/a |
Documentation
- API reference: https://allada-homelab.github.io/arr-py-client/
- Architecture: docs/architecture.md
- Docker deployment: docs/guides/docker-deployment.md
- Multi-tenant MCP: docs/guides/mcp-multi-tenant.md
- TRaSH-Guides integration: docs/guides/trash-guides.md
- Connection diagnostics: docs/guides/connection-diagnostics.md
- Operator agent: docs/guides/operator-agent.md
- Roadmap: docs/roadmap.md
Development
git clone https://github.com/allada-homelab/arr-py-client
cd arr-py-client
uv sync --all-extras --all-groups
just test # unit suite
just typecheck # basedpyright in strict mode
just lint fmt # ruff check + format
Integration tests (spin up real Radarr / Sonarr / Prowlarr containers via
compose.integration.yml):
just int-up # start the test containers
just test-int # @pytest.mark.integration
just int-down # tear down
Regenerate clients from upstream OpenAPI specs:
just gen-radarr <radarr-tag>
just gen-sonarr <sonarr-tag>
just gen-prowlarr <prowlarr-tag>
Full contributor workflow, PR conventions, and release process: CONTRIBUTING.md.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file arr_py_client-0.9.5.tar.gz.
File metadata
- Download URL: arr_py_client-0.9.5.tar.gz
- Upload date:
- Size: 310.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b2881622d0a2e6e8dbe111ce1e7201c4410832bd1eb714e84eeba1b62acfa8a6
|
|
| MD5 |
8d4920ce83d50bc08e1c9ade12af4e79
|
|
| BLAKE2b-256 |
3a1b910b77ed908544e7afee515bb7db933776d00a95baf713560286894d838b
|
Provenance
The following attestation bundles were made for arr_py_client-0.9.5.tar.gz:
Publisher:
release.yml on allada-homelab/arr-py-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arr_py_client-0.9.5.tar.gz -
Subject digest:
b2881622d0a2e6e8dbe111ce1e7201c4410832bd1eb714e84eeba1b62acfa8a6 - Sigstore transparency entry: 1362298487
- Sigstore integration time:
-
Permalink:
allada-homelab/arr-py-client@c98418d6c9a1c43a267ce613a9d76acee9963cd1 -
Branch / Tag:
refs/tags/v0.9.5 - Owner: https://github.com/allada-homelab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
release.yml@c98418d6c9a1c43a267ce613a9d76acee9963cd1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file arr_py_client-0.9.5-py3-none-any.whl.
File metadata
- Download URL: arr_py_client-0.9.5-py3-none-any.whl
- Upload date:
- Size: 438.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26293c8c1c78be0aa7766119a4204b367776c4e307307cd40664ad6035a67415
|
|
| MD5 |
2440a17a558c7bef823c0c1346331be5
|
|
| BLAKE2b-256 |
6fef20efd70e08fe9b335893773ea174a30c6247bd74f24b96c26d404c6d3c85
|
Provenance
The following attestation bundles were made for arr_py_client-0.9.5-py3-none-any.whl:
Publisher:
release.yml on allada-homelab/arr-py-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arr_py_client-0.9.5-py3-none-any.whl -
Subject digest:
26293c8c1c78be0aa7766119a4204b367776c4e307307cd40664ad6035a67415 - Sigstore transparency entry: 1362298547
- Sigstore integration time:
-
Permalink:
allada-homelab/arr-py-client@c98418d6c9a1c43a267ce613a9d76acee9963cd1 -
Branch / Tag:
refs/tags/v0.9.5 - Owner: https://github.com/allada-homelab
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
release.yml@c98418d6c9a1c43a267ce613a9d76acee9963cd1 -
Trigger Event:
push
-
Statement type: