Back to Discover

seerr-mcp-server

connector

cyanheads

Search Jellyseerr/Overseerr, check availability, and create guarded media requests.

View on GitHub
0 starsSynced Aug 2, 2026

Install to Claude Code

/plugin marketplace add cyanheads/seerr-mcp-server

README

@cyanheads/seerr-mcp-server

Search Jellyseerr/Overseerr, check availability, and create guarded media requests via MCP. STDIO or Streamable HTTP.

6 Tools • 1 Resource

Version License MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework


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 to mode: preview — it resolves the title and returns the exact payload that would be submitted without writing anything. The real request fires only on mode: request, and asks for an explicit confirmation first when the client supports elicitation.
  • Operator-safe output. Every payload passes through one normalization layer (src/services/seerr/normalizers.ts) that allow-lists the fields it emits, so operator email, Plex/Jellyfin tokens and media-server IDs, and internal serviceUrl hosts never reach the model — requesters are projected to { id, displayName }. Redaction is always on; filesystem paths are the single opt-in, behind includePaths.

Tools

Six tools covering the request workflow — discover (search) → confirm (get) → understand routing (service_options) → request (request_media) → track (request_status / list_requests):

ToolDescription
seerr_search_mediaSearch 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_mediaFetch 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_requestsList recent requests with status/type/requester filters; echoes the applied filters and decodes every numeric status. Titles are opt-in via includeTitles.
seerr_request_mediaGuarded write. Previews the request payload by default (mode: preview); creates the request only on mode: request with an elicited confirmation.
seerr_request_statusFetch one request by ID — title, decoded request + media availability (incl. 4K), requester, routing summary, and a state-tuned next-step hint.
seerr_service_optionsSummarize 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 / all media-type filter
  • Decoded availability (status, plus status4k when 4K is enabled) for tracked titles only
  • Pagination by page, with a per-call result limit to cap output size
  • Optional ISO 639-1 language for 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 seasonNumber for 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_found with 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, and requestedById filters
  • Sort by created (added) or last-changed (modified), ascending or descending
  • take / skip pagination; the enrichment trailer echoes the filter set the server applied
  • Requester is PII-redacted to { id, displayName }
  • Titles aren't on request objects. includeTitles: true joins them from the media records — one lookup per distinct title on the page, so a 4K and a non-4K request for one film cost one call. Default is off, keeping the call to a single upstream read. Rows that can't be resolved keep every other field and are disclosed in the notice

seerr_request_media

The only mutation in the surface, and it is triple-guarded:

  1. mode: preview (default) resolves the title and returns the exact POST /request payload that would be submitted — no write. A sloppy call shows the payload and changes nothing.
  2. mode: request triggers a ctx.elicit confirmation when the client supports it; declining cancels before submission.
  3. destructiveHint: true is 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_request pointing 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 / specialEpisodesEnabled flags
  • 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

TypeNameDescription
Resourceseerr://request/{requestId}Read-once summary of one request — title, 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:

  • 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 errorsmedia_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/mcp

Prerequisites

  • 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

  1. Clone the repository:
git clone https://github.com/cyanheads/seerr-mcp-server.git
  1. Navigate into the directory:
cd seerr-mcp-server
  1. Install dependencies:
bun install
  1. Configure environment:
cp .env.example .env
# edit .env — set SEERR_BASE_URL and SEERR_API_KEY

Configuration

All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:

VariableDescriptionDefault
SEERR_BASE_URLRequired. 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_KEYRequired. Jellyseerr API key (Settings → General → API Key). Sent as the X-Api-Key header.
SEERR_REQUEST_TIMEOUT_MSPer-request HTTP timeout in milliseconds.15000
MCP_TRANSPORT_TYPETransport: stdio or http.stdio
MCP_HTTP_PORTPort for the HTTP server.3010
MCP_AUTH_MODEAuth mode: none, jwt, or oauth.none
MCP_LOG_LEVELLog level (RFC 5424).info
LOGS_DIRDirectory for log files (Node.js only).<project-root>/logs
STORAGE_PROVIDER_TYPEStorage backend.in-memory
OTEL_ENABLEDEnable 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:http
    
  • Run 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-server

The 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

DirectoryPurpose
src/index.tscreateApp() entry point — registers the six tools + one resource and inits the Seerr service.
src/configServer-specific environment variable parsing and validation with Zod.
src/mcp-server/toolsTool definitions (*.tool.ts).
src/mcp-server/resourcesResource definitions (*.resource.ts).
src/services/seerrJellyseerr 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/catch in tool logic
  • Use ctx.log for request-scoped logging, ctx.state for 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 test

License

Apache-2.0 — see LICENSE for details.

Rendered live from cyanheads/seerr-mcp-server's GitHub README — not stored, always reflects the source repo.

2 Install Methods

NameDescriptionCategorySource
npm packageInstall via npm (stdio transport)mcp-server@cyanheads/seerr-mcp-server
npm packageInstall via npm (streamable-http transport)mcp-server@cyanheads/seerr-mcp-server

0 Comments

Login required
Log in to post a comment or update on this repo.

No comments yet — be the first to share an update.