@cyanheads/seerr-mcp-server
v0.1.0
Published
Search Jellyseerr/Overseerr, check availability, and create guarded media requests via MCP. STDIO or Streamable HTTP.
Maintainers
Readme
A workflow MCP server over a self-hosted Jellyseerr / Overseerr instance — the request layer that fronts Jellyfin/Plex/Emby plus Radarr and Sonarr. The unit of work is not "download a movie"; it is search → resolve the exact TMDB-backed title → check availability and request state → create a guarded request that Radarr/Sonarr act on. Jellyseerr owns permissions, quotas, routing, and status; this server never touches Radarr/Sonarr directly.
Two properties make it safe to hand an agent:
- Guarded writes. The one mutating tool (
seerr_request_media) defaults tomode: preview— it resolves the title and returns the exact payload that would be submitted without writing anything. The real request fires only onmode: request, and asks for an explicit confirmation first when the client supports elicitation. - PII/infra redaction. Raw Jellyseerr payloads carry operator email, Plex/Jellyfin tokens, internal service URLs, and filesystem paths. A single normalization choke point strips all of it before any tool output — requester objects are projected to
{ id, displayName }, and root-folder paths are gated behind an explicitincludePathsflag.
Tools
Six tools covering the request workflow — discover (search) → confirm (get) → understand routing (service_options) → request (request_media) → track (request_status / list_requests):
| Tool | Description |
|:---|:---|
| seerr_search_media | Search movies and TV by title; returns ranked matches with TMDB ID, year, overview, and decoded availability when Jellyseerr already tracks the title. The required first step before requesting. |
| seerr_get_media | Fetch exact movie/show details by TMDB ID + media type to confirm the title before a write; for TV, a per-season summary or one season's episode list. |
| seerr_list_requests | List recent requests with status/type/requester filters; echoes the applied filters and decodes every numeric status. |
| seerr_request_media | Guarded write. Previews the request payload by default (mode: preview); creates the request only on mode: request with an elicited confirmation. |
| seerr_request_status | Fetch one request by ID — decoded request + media availability (incl. 4K), requester, routing summary, and a state-tuned next-step hint. |
| seerr_service_options | Summarize configured Radarr/Sonarr services, default quality profiles, and instance capability flags (4K, partial requests, specials, media server). Filesystem paths redacted unless includePaths. |
Every status field is decoded to { raw, label } — both the numeric code Jellyseerr returns and a human label — so an agent never has to hardcode the enum mapping.
seerr_search_media
Title disambiguation entry point. Wraps GET /search, filters to movies and TV (people are always excluded), and decodes availability when the title is tracked.
- Free-text title queries matched against TMDB;
movie/tv/allmedia-type filter - Decoded availability (
status, plusstatus4kwhen 4K is enabled) for tracked titles only - Pagination by page, with a per-call result
limitto cap output size - Optional ISO 639-1
languagefor localized titles/overviews - Empty results are a normal success — returns
[]with a guidance notice, not an error
seerr_get_media
Confirm the exact title before a write. Wraps GET /movie/{id} or GET /tv/{id}, optionally a season's episodes.
- Availability plus any existing open request for the title (avoids duplicate requests)
- TV: omit
seasonNumberfor a per-season summary, or pass one to fetch that season's episode list (season 0 is Specials) - A TMDB ID that doesn't resolve surfaces as a clean
media_not_foundwith a search-recovery hint (Jellyseerr's raw HTTP 500 is classified in the service layer)
seerr_list_requests
Review recent requests and their lifecycle. Wraps GET /request.
- Lifecycle
filter(pending, processing, available, failed, …),mediaType, andrequestedByIdfilters - Sort by created (
added) or last-changed (modified), ascending or descending take/skippagination; the enrichment trailer echoes the filter set the server applied- Requester is PII-redacted to
{ id, displayName }; titles aren't on request objects, so they're omitted here — fetch one withseerr_get_mediawhen needed
seerr_request_media
The only mutation in the surface, and it is triple-guarded:
mode: preview(default) resolves the title and returns the exactPOST /requestpayload that would be submitted — no write. A sloppy call shows the payload and changes nothing.mode: requesttriggers actx.elicitconfirmation when the client supports it; declining cancels before submission.destructiveHint: trueis the fallback signal for non-interactive clients whose approval flow reads annotations.
- Capability validation (4K enabled? seasons valid? partial requests allowed?) runs locally against cached instance settings before any POST, so a bad request fails with an actionable typed error instead of a failed write
- TV requests take
seasons: "all"or an explicit list (e.g.[1, 2]); Specials are excluded unless the instance enables them - Optional routing overrides (
serverId,profileId,rootFolder,languageProfileId) — omit to use Jellyseerr's defaults (recommended) - An existing request for the title is surfaced in the output; a duplicate rejection from Jellyseerr maps to a typed
duplicate_requestpointing back at it
seerr_service_options
Lets an agent reason about request capability and routing without a separate status tool. Fans out service + settings + version reads with Promise.allSettled, so one failed leg degrades to a disclosed notice rather than failing the call.
- Instance capability summary: Jellyseerr version, media server, and the
movie4kEnabled/series4kEnabled/partialRequestsEnabled/specialEpisodesEnabledflags - Per-service routing: server ID, default-server flag, 4K capability, and the active + available quality profiles (IDs and names, safe to surface)
- Filesystem root-folder paths and free space are operator-private — omitted unless
includePaths: true
Resource and prompt
| Type | Name | Description |
|:---|:---|:---|
| Resource | seerr://request/{requestId} | Read-once summary of one request — decoded status + media availability + routing. Mirrors seerr_request_status. |
All request data is also reachable via tools — request enumeration is the job of seerr_list_requests (filterable, the tool-only access path), so the collection is intentionally not exposed as a resource. There are no prompts; the guarded-write workflow lives in the tool, not a prompt template.
Features
Built on @cyanheads/mcp-ts-core:
- Declarative tool and resource definitions — single file per primitive, framework handles registration and validation
- Unified error handling — handlers throw, framework catches, classifies, and formats
- Pluggable auth:
none,jwt,oauth - Swappable storage backends:
in-memory,filesystem,Supabase,Cloudflare KV/R2/D1 - Structured logging with optional OpenTelemetry tracing
- STDIO and Streamable HTTP transports
Jellyseerr-specific:
- Read + guarded-request only — admin-scope endpoints (approve/decline, retry, edit/delete, media/file deletion, user/settings/sync) are excluded by design, not by API limitation
- Status decoding centralized in one helper — request and media statuses (including the separate 4K availability) decode to
{ raw, label }everywhere, forward-compatible with new Jellyseerr status codes - Capability validation against cached instance settings catches most bad requests before they reach the API
- A short-TTL settings cache avoids a round-trip on every preview
Agent-friendly output:
- Mandatory PII/infra redaction — a single normalization choke point projects requester objects to
{ id, displayName }and drops operator email, Plex/Jellyfin tokens, internalserviceUrl, and filesystem paths before any output reaches the model - Provenance and disclosure — searches echo the effective query; capped lists disclose truncation; a degraded service leg surfaces a notice instead of silently dropping data
- Typed, actionable errors —
media_not_found,request_not_found,seasons_required,four_k_not_enabled,duplicate_request, and more carry a recovery hint so callers can branch and retry without parsing prose
Getting started
This server connects to your own Jellyseerr/Overseerr instance — there is no public hosted endpoint. Add the following to your MCP client configuration file, pointing SEERR_BASE_URL at your instance and supplying its API key.
{
"mcpServers": {
"seerr-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/seerr-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"SEERR_BASE_URL": "http://localhost:5055",
"SEERR_API_KEY": "your-api-key"
}
}
}
}Or with npx (no Bun required):
{
"mcpServers": {
"seerr-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/seerr-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info",
"SEERR_BASE_URL": "http://localhost:5055",
"SEERR_API_KEY": "your-api-key"
}
}
}
}For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 SEERR_BASE_URL=http://localhost:5055 SEERR_API_KEY=your-api-key bun run start:http
# Server listens at http://localhost:3010/mcpPrerequisites
- Bun v1.3.2 or higher (or Node.js v24+).
- A running Jellyseerr or Overseerr instance, and its API key (Settings → General → API Key).
Installation
- Clone the repository:
git clone https://github.com/cyanheads/seerr-mcp-server.git- Navigate into the directory:
cd seerr-mcp-server- Install dependencies:
bun install- Configure environment:
cp .env.example .env
# edit .env — set SEERR_BASE_URL and SEERR_API_KEYConfiguration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
| Variable | Description | Default |
|:---|:---|:---|
| SEERR_BASE_URL | Required. Base URL of the Jellyseerr/Overseerr instance, e.g. http://localhost:5055. The service appends /api/v1 — no /api/v1 suffix, no trailing slash. | — |
| SEERR_API_KEY | Required. Jellyseerr API key (Settings → General → API Key). Sent as the X-Api-Key header. | — |
| SEERR_REQUEST_TIMEOUT_MS | Per-request HTTP timeout in milliseconds. | 15000 |
| MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
| MCP_HTTP_PORT | Port for the HTTP server. | 3010 |
| MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
| MCP_LOG_LEVEL | Log level (RFC 5424). | info |
| LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
| STORAGE_PROVIDER_TYPE | Storage backend. | in-memory |
| OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
Build and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:httpRun checks and tests:
bun run devcheck # Lint, format, typecheck, security, changelog sync bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t seerr-mcp-server .
docker run --rm \
-e SEERR_BASE_URL=http://host.docker.internal:5055 \
-e SEERR_API_KEY=your-api-key \
-p 3010:3010 \
seerr-mcp-serverThe Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/seerr-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
| Directory | Purpose |
|:---|:---|
| src/index.ts | createApp() entry point — registers the six tools + one resource and inits the Seerr service. |
| src/config | Server-specific environment variable parsing and validation with Zod. |
| src/mcp-server/tools | Tool definitions (*.tool.ts). |
| src/mcp-server/resources | Resource definitions (*.resource.ts). |
| src/services/seerr | Jellyseerr API client, status decoders, and the PII/infra redaction normalizers. |
| tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md/AGENTS.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools and resources via the barrels in
src/mcp-server/*/definitions/index.ts - Wrap the Jellyseerr API: validate raw → normalize and redact to a domain type → return the output schema; never fabricate missing fields, and never let operator PII or paths reach output
Contributing
Issues and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run testLicense
Apache-2.0 — see LICENSE for details.
