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.6.tar.gz (311.8 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.6-py3-none-any.whl (438.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: arr_py_client-0.9.6.tar.gz
  • Upload date:
  • Size: 311.8 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.6.tar.gz
Algorithm Hash digest
SHA256 92b0fd6f766c1b72c551129bd969e4e667854fe87781280adff82fce5bfc01dc
MD5 07e3ad3d7d4c8933106a88408a191deb
BLAKE2b-256 4abcf87d5efb1e7bfc7302a082240e8fdb7f7bd17925f4a3ad00086b5db66895

See more details on using hashes here.

Provenance

The following attestation bundles were made for arr_py_client-0.9.6.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.6-py3-none-any.whl.

File metadata

  • Download URL: arr_py_client-0.9.6-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

Hashes for arr_py_client-0.9.6-py3-none-any.whl
Algorithm Hash digest
SHA256 6e35e8150508b13205e5cb44b50432b13db114d4853a246827356f6fc599f4ee
MD5 7dd9c8fc655fd2abeeaaaa5488a1f76a
BLAKE2b-256 f96260250e76c6a260cd7d96c40fabe8499f4192c7cabafb0c627488a8bde52a

See more details on using hashes here.

Provenance

The following attestation bundles were made for arr_py_client-0.9.6-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