Skip to main content

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."

CI PyPI Python Coverage License

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_resources so 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

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 a CI_RUNNER repository/organization variable to point CI at a different runner (e.g. ubuntu-latest).
  • Build cache — docker-publish caches image layers to the homelab buildcache registry by default (registry.internal.devguy.dev/buildcache); the DOCKER_CACHE_REGISTRY org variable overrides the prefix.

License

MIT. See LICENSE.

Metadata

Release files for arr-py-client 0.10.10

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for arr-py-client 0.10.10
File Size Uploaded
arr_py_client-0.10.10.tar.gz 387.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for arr-py-client 0.10.10
File Interpreter ABI Platform
arr_py_client-0.10.10-py3-none-any.whl Python 3 none any Details

Total release size: 896.2 kB

Release files / arr_py_client-0.10.10.tar.gz

Download URL arr_py_client-0.10.10.tar.gz
Size 387.0 kB
Tags Source
SHA-256 checksum
How to use checksums
78b0ca9528fad3315491b80052ef8ecca52d1273c0f20b9ab1ad72fad0d98c3e
BLAKE2b-256 checksum
How to use checksums
b3363799a3161e0376ce7d8d12521a573dff5db653c3c795c84f2fbf2c55086c
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

Release files / arr_py_client-0.10.10-py3-none-any.whl

Download URL arr_py_client-0.10.10-py3-none-any.whl
Size 509.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
611a6d951cba0f62b56cd9d31673c08c93c0c5c6b368b3dec095c55e867f9697
BLAKE2b-256 checksum
How to use checksums
caeabb42f43cb92eb705d484e623ea347667cad1f60cd0558e03004c6800019e
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

Release history Release notifications | RSS feed

0.11.0

2 release files

This release

0.10.10 This release

2 release files

0.10.9

2 release files

0.9.8

2 release files

0.9.7

2 release files

0.9.6

2 release files

0.9.5

2 release files

0.9.4

2 release files

0.9.3

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page