Skip to main content

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

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-brandradarr_*, sonarr_*, prowlarr_* mirror the HTTP API surface (list / get / system status / queue / blocklist / logs).
  • Composedarr_* cross-brand workflows: arr_health, arr_sync_plan, arr_sync_apply, arr_backfill, arr_janitor_run, arr_explain_grab.
  • Describe / referencearr_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 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

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

arr_py_client-0.9.2.tar.gz (302.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

arr_py_client-0.9.2-py3-none-any.whl (434.0 kB view details)

Uploaded Python 3

File details

Details for the file arr_py_client-0.9.2.tar.gz.

File metadata

  • Download URL: arr_py_client-0.9.2.tar.gz
  • Upload date:
  • Size: 302.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for arr_py_client-0.9.2.tar.gz
Algorithm Hash digest
SHA256 06a878df1c2e24ab4c3718db5d5cf22f6f4a2d812da1e3f46c10aaba2d2fb520
MD5 c7f682769fa6047a17f74993c33cb5eb
BLAKE2b-256 59589cd9a3f836f7cacd9134663ac03983ed933b850cc49f74c0d8dc15db1a4a

See more details on using hashes here.

Provenance

The following attestation bundles were made for arr_py_client-0.9.2.tar.gz:

Publisher: release.yml on allada-homelab/arr-py-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file arr_py_client-0.9.2-py3-none-any.whl.

File metadata

  • Download URL: arr_py_client-0.9.2-py3-none-any.whl
  • Upload date:
  • Size: 434.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for arr_py_client-0.9.2-py3-none-any.whl
Algorithm Hash digest
SHA256 add80dfaa388bb04017af0598f52ae4c700e2b88a2ba1b4930fcc899be35544b
MD5 8d383a5cf9b2824260adda4ed9e0db7b
BLAKE2b-256 150c654d1e92dbf59f6be0a21b7fdbc15c84e39ad175a1f7f73ad06469af769c

See more details on using hashes here.

Provenance

The following attestation bundles were made for arr_py_client-0.9.2-py3-none-any.whl:

Publisher: release.yml on allada-homelab/arr-py-client

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page