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 fastmcp.server.auth import RemoteAuthProvider
from pydantic import AnyHttpUrl
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(),
auth=RemoteAuthProvider(
token_verifier=CallbackTokenVerifier(verify=app_auth.to_principal),
authorization_servers=[AnyHttpUrl("https://auth.example.com/")],
base_url="https://api.example.com/mcp",
),
)
mcp_app = mcp.http_app()
# The MCP app's lifespan must run, or the streamable-http session manager
# is never initialized.
app = FastAPI(lifespan=mcp_app.lifespan)
app.mount("/mcp", mcp_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.
CI configuration
- CI runner — workflows run on
${{ vars.CI_RUNNER || 'homelab-runners' }}; set aCI_RUNNERrepository/organization variable to point CI at a different runner (e.g.ubuntu-latest). - Build cache —
docker-publishcaches image layers to the homelab buildcache registry by default (registry.internal.devguy.dev/buildcache); theDOCKER_CACHE_REGISTRYorg variable overrides the prefix.
License
MIT. See LICENSE.
Metadata
Release files for arr-py-client 0.11.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| arr_py_client-0.11.0.tar.gz | 387.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| arr_py_client-0.11.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 896.2 kB
Release files / arr_py_client-0.11.0.tar.gz
| Download URL | arr_py_client-0.11.0.tar.gz |
|---|---|
| Size | 387.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ca62045b78a0807efb402eaa8abc3066e7f212970644e4cc457d46576a902cc7
|
|
BLAKE2b-256 checksum How to use checksums |
cc7d82eb6708f85450bda3048c04e0ecfa750b255e44541d0796f886be2c707f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.
Transparency logRelease files / arr_py_client-0.11.0-py3-none-any.whl
| Download URL | arr_py_client-0.11.0-py3-none-any.whl |
|---|---|
| Size | 509.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8ee4329e4a174dcb090b6b161bbf02dc58d327acb6ed0acd95ed4c252dbb78df
|
|
BLAKE2b-256 checksum How to use checksums |
fe86f1a68a69e14d52b7a7c8f5685f493e2a709b8076c129c53f3b6f68809cd7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.
Transparency log