ARR_MCP
Health Uyari
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Gecti
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
An MCP server for your *Arr stack for you to be able to communicate, find out information and also be able to do basic tasks
ARR-MCP
An MCP server for the *arr media stack. Connect Claude, Cursor, VS Code or any other MCP client to Sonarr, Radarr, Prowlarr and Bazarr — including multiple instances of each.
- Real MCP — JSON-RPC 2.0 over stdio and Streamable HTTP, built on the official Go SDK
- Multi-instance — run two Sonarrs (4K and 1080p) and address them by name
- Permission controls — read-only, confirm-before-write, or full access
- 105 tools across Sonarr, Radarr, Prowlarr and Bazarr — near-complete API coverage
- Single static binary, distroless container, multi-arch image
Jump to: Install with your AI · Manual quickstart · Find your API key · Configuration · Client setup · Permissions · Tools · Troubleshooting
Install with your AI
The fastest way in. Paste this into Claude, ChatGPT, Cursor, or whichever assistant you use:
Read https://raw.githubusercontent.com/GauranshMathur/ARR_MCP/main/docs/ai-install-prompt.md
and follow it to set up ARR-MCP for me. It is an instruction document — act on it, don't
summarise it back to me.
That last sentence matters: without it, assistants tend to fetch the page and report on it
rather than doing anything.
It works out which MCP client you are using — often the one you are already talking to —
then asks only what it needs and either performs the setup itself or gives you exact steps
for your operating system, ending with a command to check it worked.
If your assistant can't browse the web, paste the contents of
docs/ai-install-prompt.md instead — it is self-contained and
needs no network access.
Would rather do it by hand? Carry on below.
60-second quickstart
The shortest path from nothing to a working server. No config file, no clone — just
environment variables and one container.
1. Collect one URL and one API key for each service you want to expose. See
Find your API key if you don't know where they live.
2. Write them into a .env file:
cat > .env <<'EOF'
SONARR_URL=http://192.168.10.12:8989
SONARR_API_KEY=your-sonarr-api-key
RADARR_URL=http://192.168.10.14:7878
RADARR_API_KEY=your-radarr-api-key
EOF
Any service you leave out is simply not exposed. One service is enough to start.
3. Check that the credentials work before wiring up a client, so a mistake shows
up as a clear error rather than as a client that mysteriously has no tools:
docker run --rm --env-file .env ghcr.io/gauranshmathur/arr-mcp --check
OK sonarr/default (http://192.168.10.12:8989)
OK radarr/default (http://192.168.10.14:7878)
4. Add it to your client. For Claude Code:
claude mcp add arr -- docker run -i --rm --env-file /absolute/path/to/.env \
ghcr.io/gauranshmathur/arr-mcp --transport stdio
That is the whole setup. docs/clients.md has copy-pasteable blocks for
Claude Desktop, Cursor, VS Code, Windsurf, Zed, Cline, Roo Code, Continue.dev, LibreChat,
Goose and anything else that speaks MCP.
Use an absolute path for
--env-file. Your MCP client starts the server from its own
working directory, which is rarely the one you were standing in when you created.env.
Prefer a long-running server?
Run it once over HTTP and point every client at the same endpoint:
docker run -d --name arr-mcp --env-file .env -p 8080:8080 \
ghcr.io/gauranshmathur/arr-mcp
curl -s localhost:8080/health # {"status":"ok"}
MCP is served at http://localhost:8080/mcp. The container defaults to--transport http --addr 0.0.0.0:8080, so no arguments are needed. If you cloned the
repository, docker compose up -d does the same thing.
Find your API key
Each service shows its key in its own web UI. The key is a long hex string; copy it
exactly, with no surrounding whitespace.
| Service | Where to click | Default port |
|---|---|---|
| Sonarr | Settings → General → Security → API Key | 8989 |
| Radarr | Settings → General → Security → API Key | 7878 |
| Prowlarr | Settings → General → Security → API Key | 9696 |
| Bazarr | Settings → General → Security → API Key | 6767 |
Every one of these is a full-access credential for that application. ARR-MCP never
needs more than one key per instance, and the permission model is what
narrows down what the model can actually do with it.
Do I need a config file?
No, unless you run more than one instance of a service.
| You have | Use | Why |
|---|---|---|
| One Sonarr, one Radarr, one Prowlarr, one Bazarr | Environment variables | Nothing to disambiguate. <SERVICE>_URL and <SERVICE>_API_KEY is the whole configuration. |
Two Sonarrs (say main and anime), or a Bazarr per *arr pair |
config.yaml |
Instances need names so tools can target them, and each can carry its own permission policy. |
| One instance, but you want per-instance permissions or non-default server settings | config.yaml |
The environment-variable path always builds a single instance named default with the global policy. |
You can switch later without changing anything else: a config file supersedes the
environment variables entirely, it does not merge with them.
Configuration
Environment variables (one instance per service)
Set <SERVICE>_URL and <SERVICE>_API_KEY and run without --config:
SONARR_URL=http://192.168.10.12:8989
SONARR_API_KEY=...
RADARR_URL=http://192.168.10.14:7878
RADARR_API_KEY=...
PROWLARR_URL=http://192.168.10.18:9696
PROWLARR_API_KEY=...
BAZARR_URL=http://192.168.10.16:6767
BAZARR_API_KEY=...
A service is configured only when both its variables are set; setting just one is
treated as "not configured" rather than as an error. Each configured service gets a
single instance named default. If no service ends up configured at all, startup fails
with a message listing the variables it looked for.
Config file (multiple instances)
Copy config.example.yaml to config.yaml. Secrets stay in the environment and are
referenced as ${VAR}, so the file is safe to commit or mount from a ConfigMap:
services:
sonarr:
- name: main
url: http://192.168.10.12:8989
apiKey: ${SONARR_MAIN_API_KEY}
default: true
- name: anime
url: http://192.168.10.13:8989
apiKey: ${SONARR_ANIME_API_KEY}
Every tool then takes an optional instance argument. Omit it to use the one markeddefault. The configured names are advertised as a schema enum, so the model picks
from a closed set rather than guessing:
sonarr_search_series{query: "Severance", instance: "anime"}
Rules the loader enforces at startup, so a mistake never surfaces mid-conversation:
- Every instance needs a
name, aurland anapiKey. - Instance names must be unique within a service, and at most one may be
default. - With several instances and no
default, a tool call that omitsinstancefails with a
message listing the valid names — it does not silently pick the first one. - Only
sonarr,radarr,prowlarrandbazarrare accepted; anything else is rejected
rather than ignored, so a typo likesonar:is caught immediately.
An unset (or empty)
${VAR}is a startup error, never a silent empty value — an empty
API key would otherwise surface much later as a confusing 401.
Server settings
server:
transport: stdio # stdio | http
addr: 0.0.0.0:8080 # only used by the http transport
logLevel: info # debug | info | warn | error
Those are the defaults. Command-line flags override the file, so one mounted config can
serve both a stdio and an HTTP deployment:
arr-mcp --config /etc/arr-mcp/config.yaml --transport http --addr 0.0.0.0:8080
Instead of passing --config you can set ARR_MCP_CONFIG to the same path. That is worth
doing in containers: it means bare arr-mcp --check also finds the config, which is what
makes the Docker healthcheck work.
Permissions
Mutating tools are gated by policy:
permissions:
mode: confirm # readonly | confirm | full
confirmScope: write # write | destructive
fallback: deny # deny | allow
| Mode | Behaviour |
|---|---|
readonly |
Only read tools are registered. Mutating tools are invisible to the client. |
confirm |
Mutating tools ask the user first, via MCP elicitation. Default. |
full |
Everything runs immediately. |
confirmScope selects what gets confirmed: write covers both writes and deletes,destructive covers deletes only.
fallback decides what happens when the client cannot prompt. Confirmation is
delivered over MCP elicitation, which a client must advertise support for during
initialisation, and many clients still don't. The default deny fails closed, because the
alternative is worse than it looks: without it, connecting a client that lacks elicitation
would quietly downgrade confirm mode into full — you would believe every write was
being approved while none of them ever were. Setting allow grants that unprompted write
access deliberately, which is a reasonable choice for a trusted local client but should be
a decision, not an accident.
Any instance can override the global policy:
- name: anime
url: http://192.168.10.13:8989
apiKey: ${SONARR_ANIME_API_KEY}
permissions:
mode: readonly
A tool is advertised if any instance of that service allows it; the per-instance policy
is then applied when the call actually runs. So a read-only anime instance alongside a
writable main still shows sonarr_add_series in the tool list, and refuses it foranime at call time.
All tools also carry MCP readOnlyHint / destructiveHint annotations, so clients can
render their own warnings independently of this gating.
Connecting a client
Full copy-pasteable configuration for every client below lives in
docs/clients.md. The two forms everything reduces to:
stdio — the client launches the process and talks over stdin/stdout:
docker run -i --rm --env-file /absolute/path/to/.env ghcr.io/gauranshmathur/arr-mcp --transport stdio
The -i is required: without it Docker gives the container no stdin and the JSON-RPC
handshake never completes.
HTTP — the server runs somewhere and the client connects to it:
http://localhost:8080/mcp
/health on the same port answers {"status":"ok"} and is not part of the MCP protocol;
it exists for container and Kubernetes probes.
Kubernetes
Manifests for a Deployment, Service, ConfigMap and Secret template are indeploy/kubernetes/, with a kustomization.yaml so they can be
pointed at directly by Argo CD or kubectl apply -k.
Tools
Sonarr and Radarr are kept at parity: 32 of their tools are the same tool
registered for both services, and the rest differ only where the APIs genuinely
do (seasons and episodes versus movies and collections).
Sonarr (43)
| Area | Tools | Access |
|---|---|---|
| Library | sonarr_list_series, sonarr_search_series, sonarr_list_episodes, sonarr_calendar |
read |
| Wanted | sonarr_wanted_missing, sonarr_wanted_cutoff |
read |
| Files | sonarr_list_episode_files, sonarr_rename_preview |
read |
| Profiles | sonarr_list_quality_profiles, sonarr_list_quality_definitions, sonarr_list_custom_formats, sonarr_list_delay_profiles, sonarr_list_release_profiles |
read |
| Config | sonarr_list_root_folders, sonarr_naming_config, sonarr_list_indexers, sonarr_list_download_clients, sonarr_list_import_lists, sonarr_list_notifications |
read |
| Tags | sonarr_list_tags, sonarr_tag_details |
read |
| Operations | sonarr_queue, sonarr_queue_status, sonarr_history, sonarr_blocklist, sonarr_health, sonarr_disk_space, sonarr_system_status, sonarr_list_tasks, sonarr_list_updates |
read |
| Add & edit | sonarr_add_series, sonarr_edit_series, sonarr_set_season_monitored, sonarr_monitor_episodes, sonarr_create_tag |
write |
| Automation | sonarr_trigger_search, sonarr_refresh_series, sonarr_run_command |
write |
| Deletion | sonarr_delete_series, sonarr_delete_episode_files, sonarr_delete_queue_item, sonarr_delete_blocklist_item, sonarr_delete_tag |
destructive |
Radarr (41)
| Area | Tools | Access |
|---|---|---|
| Library | radarr_list_movies, radarr_search_movies, radarr_list_collections, radarr_calendar |
read |
| Wanted | radarr_wanted_missing, radarr_wanted_cutoff |
read |
| Files | radarr_list_movie_files, radarr_rename_preview |
read |
| Profiles | radarr_list_quality_profiles, radarr_list_quality_definitions, radarr_list_custom_formats, radarr_list_delay_profiles, radarr_list_release_profiles |
read |
| Config | radarr_list_root_folders, radarr_naming_config, radarr_list_indexers, radarr_list_download_clients, radarr_list_import_lists, radarr_list_notifications |
read |
| Tags | radarr_list_tags, radarr_tag_details |
read |
| Operations | radarr_queue, radarr_queue_status, radarr_history, radarr_blocklist, radarr_health, radarr_disk_space, radarr_system_status, radarr_list_tasks, radarr_list_updates |
read |
| Add & edit | radarr_add_movie, radarr_edit_movies, radarr_create_tag |
write |
| Automation | radarr_trigger_search, radarr_refresh_movies, radarr_run_command |
write |
| Deletion | radarr_delete_movie, radarr_delete_movie_files, radarr_delete_queue_item, radarr_delete_blocklist_item, radarr_delete_tag |
destructive |
Bazarr (14)
Subtitle management, including two instances if you run one per Sonarr/Radarr pair.
| Tool | Access |
|---|---|
bazarr_badges — outstanding counts, cheapest first call |
read |
bazarr_wanted_episodes, bazarr_wanted_movies |
read |
bazarr_list_series, bazarr_list_movies |
read |
bazarr_list_episode_subtitles — the only source of subtitle file paths |
read |
bazarr_list_providers, bazarr_list_languages |
read |
bazarr_health, bazarr_system_status |
read |
bazarr_search_episode_subtitles, bazarr_search_movie_subtitles |
write |
bazarr_delete_episode_subtitle, bazarr_delete_movie_subtitle |
destructive |
Prowlarr (7)
prowlarr_search, prowlarr_list_indexers, prowlarr_indexer_stats, prowlarr_health, prowlarr_history, prowlarr_system_status (read); prowlarr_run_command (write).
What responses contain
Upstream payloads are far too large to return as they arrive — a single Sonarr
custom format list is 283 KB, one series' episode files 252 KB, and Radarr's
collection list 259 KB. Every tool returns a projection: identities, counts and
the fields that answer a question, never overviews, artwork, alternate titles or
embedded media info.
Indexer, download client, import list and notification listings never return the
provider fields array. That array holds each provider's own credentials —
indexer API keys, download client passwords, notification webhook URLs — and
none of it belongs in a model's context.
Services with no configured instances register no tools at all, so the advertised list always reflects what is actually reachable.
Troubleshooting
Start with --check. It exercises exactly the credentials and URLs the tools will use, and
prints one line per instance, so it separates "my configuration is wrong" from "my client
is wrong" in a single command:
docker run --rm --env-file .env ghcr.io/gauranshmathur/arr-mcp --check
# or, from a local binary
arr-mcp --config config.yaml --check
401 Unauthorized
FAIL radarr/main (http://192.168.10.14:7878): radarr returned 401: Unauthorized
The URL is right — something answered — but the API key is not. Re-copy it from
Settings → General → Security; a trailing space or a truncated paste
is the usual cause. Check you did not swap keys between two instances of the same service,
which produces exactly this error on both.
connection refused or a timeout
Nothing is listening at that address. In order of likelihood:
- Wrong port. Sonarr 8989, Radarr 7878, Prowlarr 9696, Bazarr 6767 by default.
localhostused from inside a container. This is by far the most common mistake.
Inside a container,localhostmeans that container, not your machine — soSONARR_URL=http://localhost:8989tells ARR-MCP to look for Sonarr inside its own
otherwise-empty container, and it finds nothing. Use the LAN IP of the host
(http://192.168.10.12:8989), or the other container's service name if they share a
Docker network (http://sonarr:8989), orhttp://host.docker.internal:8989on Docker
Desktop. The same reasoning applies in Kubernetes: use the Service DNS name
(http://sonarr.media.svc.cluster.local:8989), neverlocalhost.- A URL base path was dropped. If you reach Sonarr at
/sonarrbehind a reverse
proxy, that prefix belongs in the URL:http://192.168.10.12/sonarr. https://with a self-signed certificate. The certificate must be trusted;
plainhttp://on the LAN avoids the problem entirely.
The client connects but shows no tools
Tools are registered per service, and a service with no configured instances registers
nothing. An empty tool list therefore means nothing was configured, not that registration
failed.
- Check the startup log on stderr — it prints one
sonarr: 1 instance(s) configured [[default]]line per service. No lines means no services were configured. - With environment variables, remember that both
<SERVICE>_URLand<SERVICE>_API_KEYmust be set for that service to count. - Check the client is passing the environment through. A
claude_desktop_config.json
entry with noenvblock and no--env-filestarts the server with an empty
environment; your shell's exported variables are not inherited. - If only the write tools are missing, that is
permissions.mode: readonlydoing its
job — read-only mode does not register them at all.
Writes are refused without ever prompting
client does not support elicitation: cannot confirm write tool sonarr_add_series;
set permissions.fallback=allow or permissions.mode=full to permit it
The default confirm mode asks for approval through MCP elicitation, and your client does
not implement it. There is no prompt to answer, so the call fails closed rather than
running unapproved — see Permissions for why that default is the safe one.
Either switch to a client that supports elicitation, or make the decision explicit:
permissions:
mode: confirm
confirmScope: destructive # only deletes need confirming
fallback: allow # writes proceed unprompted
confirmScope: destructive is usually the better trade: adds and command triggers run
freely, and the calls that remove things still fail closed.
The server starts, then exits immediately under stdio
That is normal. A stdio server lives for exactly as long as its client holds the pipe
open; running it by hand in a terminal ends as soon as stdin closes. Test it with--check, or with the MCP inspector (see Development), not by launching
it bare.
Startup fails with references unset environment variable(s)
A ${VAR} in config.yaml has no value in the process environment. Under Docker this
almost always means the variable is in your shell but was never passed into the container
— add it to --env-file / the compose env_file. Empty counts as unset, on purpose.
Nothing at all appears in the logs
Logging always goes to stderr. Under the stdio transport, stdout carries the JSON-RPC
stream and must not be written to by anything else. Most clients file that stderr away
under their own logs — Claude Desktop, for example, writes~/Library/Logs/Claude/mcp-server-arr.log on macOS. Raise the detail with--log-level debug.
Scope
ARR-MCP covers the *arr-named applications that share the common *arr API contract: the
same versioned /api shape, the same API-key header, the same /system/status and health
endpoints. That shared contract is the whole reason the project is cheap to extend — a
service is described by a ServiceSpec rather than a bespoke client, so it inherits the
transport, the instance registry and the permission model for free. Applications outside
that contract each cost a complete client instead, which is where the line is drawn.
Planned
Maintainerr, Cleanuparr and Notifiarr.
Not planned: media servers and request managers (Jellyfin, Overseerr, Plex)
None of them are *arr-named, and none of them speak the *arr API contract — Plex uses
its own token scheme and XML-flavoured API, and the request managers wrap their own
approval workflows around a different data model. Supporting any of them means a bespoke
client with its own auth handling, response shapes and tests, for a capability that
overlaps heavily with what the *arr tools already expose.
Not planned: download clients (NZBGet, SABnzbd, qBittorrent)
Download clients are deliberately out of scope, for two reasons.
You already have queue visibility. sonarr_queue and radarr_queue report
what is downloading, its status, size, time remaining and error message —
because the *arr apps track download client state themselves.sonarr_delete_queue_item can drop a stuck download and blocklist the release,
with removeFromClient telling the download client to discard it too. That
covers the operations people actually want, routed through the app that owns
the decision.
The cost is disproportionate. NZBGet speaks JSON-RPC over HTTP Basic auth,
SABnzbd uses a query-parameter mode= API, and qBittorrent needs cookie
session auth. None of them fit the ServiceSpec model, so each would be a
separate client with its own auth handling, response shapes and tests — a large
amount of surface area to duplicate a capability the *arr tools already expose.
If you need to drive a download client directly, do it through its own UI or a
dedicated MCP server.
CLI
--config PATH path to config.yaml (or set ARR_MCP_CONFIG)
--transport stdio stdio or http
--addr HOST:PORT listen address for http
--log-level LEVEL debug, info, warn, error
--check test connectivity to every configured instance and exit
--version print version
Flags override the config file, and every flag except --config has a config-file
equivalent under server:. --check exits non-zero if any instance fails, so it works in
a healthcheck or a CI step as well as by hand.
Development
go test ./... -race -cover
go build -o arr-mcp ./cmd/arr-mcp
Inspect the tool surface interactively:
npx @modelcontextprotocol/inspector ./arr-mcp --transport stdio --config config.yaml
Adding a service means describing its API rather than writing a new client — ServiceSpec carries the base path, health path and auth scheme, so services on different API versions and auth headers share one transport:
var BazarrSpec = ServiceSpec{
Name: "bazarr", BasePath: "/api", StatusPath: "/system/status",
Auth: AuthHeaderKey,
}
Releases are cut by release-please: conventional commits on main accumulate into a version-bump PR, and merging it tags the release and publishes multi-arch images to GHCR. Images are Trivy-scanned before push, so a vulnerable tag is never publicly pullable.
License
See LICENSE.
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi