Skip to content

ArjunVenat/JellyfinMCPServer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 

Repository files navigation

jellyfin-mcp

An MCP server that gives Claude (or any MCP-compatible AI) direct, natural-language access to your Jellyfin media library.

Ask Claude to find something to watch tonight and it will analyze your entire watch history, cross-reference Letterboxd ratings you've imported, consult TMDB for curated recommendations, and return a ranked list of unwatched films tailored to your taste — all in a single conversation turn.


What you can do

"What should I watch tonight? Something intense, under 2 hours."

"Find me movies similar to Parasite that I haven't seen yet."

"Show me every A24 film in my library."

"What Coen Brothers movies do I still need to watch?"

"Find movies with both Cate Blanchett and Brad Pitt."

"What are my watch stats? How many hours have I logged?"

"Pause whatever's playing on the living room TV."

All of these work out of the box — no custom prompting required.


Architecture

Claude Code / Claude Desktop
        │  MCP (stdio)
        ▼
┌─────────────────────────────────────────────┐
│              jellyfin-mcp                   │
│                                             │
│  tools.py        ← @mcp.tool() definitions  │
│  client.py       ← async httpx Jellyfin API │
│  models.py       ← Pydantic v2 parsing      │
│  config.py       ← pydantic-settings env    │
│  db.py           ← asyncpg Postgres pool    │
└──────┬──────────────┬────────────────┬──────┘
       │              │                │
  Jellyfin API    TMDB API         Postgres DB
  (required)      (optional)    (optional — for
                                Letterboxd sync)

Three services, two optional. The server degrades gracefully — TMDB enhances similarity search; Postgres enables Letterboxd rating import. Both are opt-in.


Tools

Tool Description
search_media Full-text search across movies, shows, episodes, music
get_item_details Full metadata, cast, and director for a specific item
get_recently_added What's been added to the library recently
get_continue_watching In-progress items with resume position
find_unwatched_by_director Composite tool — filmography lookup + watch-state filter
find_watched_by_director Same, but shows what you've already seen
search_by_cast Movies starring any combination of actors, ranked by cast overlap
search_by_studio Browse by production studio or distributor (A24, Ghibli, Neon, etc.)
search_by_genre Genre browse with optional unwatched filter
find_similar Recommendation engine — see below
get_taste_profile Weighted genre/tag affinities derived from your watch history
recommend_for_tonight Personalized picks, optionally filtered by mood and runtime
get_watch_stats Movies watched vs. unwatched, top genres, hours logged
get_favorites Everything you've hearted in Jellyfin
mark_as_watched Mark one or more items watched/unwatched
mark_as_favorite Add or remove from favorites
sync_letterboxd_matches Link Letterboxd CSV imports to Jellyfin library items
get_active_sessions Who's currently watching and what
control_playback Pause, play, stop, or seek any active session
check_server_status Connectivity check and server info

How the recommendation engine works

find_similar and recommend_for_tonight use a multi-signal scoring pipeline:

1. IDF-weighted genre matching

Genres are weighted by how rare they are in your library:

weight(genre) = log(total_movies / movies_with_genre)

"Drama" appears in 60% of films → low weight. "Animation" appears in 5% → high weight. This means a niche genre overlap scores much more than a broad one, which is how you get genuinely similar results rather than everything being Drama.

2. Thematic keyword matching

Keywords extracted from overviews and Jellyfin tags are matched against the reference film's description. This surfaces films with shared themes (grief, isolation, corporate satire) even when genres don't overlap perfectly.

3. Exclusive genre penalties

Tags like superhero, gladiator, or medieval that signal a specific genre subtype are penalized when the reference film doesn't have them. This prevents a quiet character drama from surfacing action blockbusters just because both are tagged Drama.

4. TMDB cross-reference (optional)

When TMDB is configured, the engine fetches curated recommendations and similar films from TMDB and cross-references them against your library. Position-weighted bonus: the #1 TMDB recommendation gets +10 points, decaying linearly. Falls back to Jellyfin-only scoring if TMDB is unavailable.

5. Taste profile (for recommend_for_tonight)

Weighted genre and tag affinity scores are built from your full watch history:

Source Weight
Letterboxd ⭐⭐⭐⭐⭐ (5 stars) +10
Letterboxd ⭐⭐⭐⭐ (4 stars) +5
Letterboxd ⭐⭐ (2 stars) 0
Letterboxd ⭐ (1 star) −2
Jellyfin favorite +3
Jellyfin watched (unrated) +1

Negative weights from low-rated films actively push down genres and themes you don't enjoy. recommend_for_tonight combines this profile with optional mood filtering (10 moods: light, intense, scary, thoughtful, etc.) and filters for runtime cap and minimum community rating.


Setup

Prerequisites

  • Python 3.11+
  • uv (recommended) or pip
  • A running Jellyfin instance

Install

git clone https://github.com/yourname/jellyfin-mcp
cd jellyfin-mcp
uv sync

Configure

Copy the example env file and fill in your values:

cp .env.example .env
# Required
JELLYFIN_URL=http://192.168.1.100:8096
JELLYFIN_API_KEY=your_api_key_here
JELLYFIN_USER_ID=your_user_id_here

# Optional — enables TMDB-enhanced similarity search
TMDB_READ_ACCESS_TOKEN=your_tmdb_token

# Optional — enables Letterboxd rating import
DB_URL=postgresql://user:pass@localhost:5432/homelab

Get your API key: Jellyfin Dashboard → API Keys → + button.

Get your user ID: Dashboard → Users → click your user → read userId from the URL.

Get a TMDB token: themoviedb.org/settings/api — free account, no credit card.

Run tests (no Jellyfin instance required)

Tests use respx to mock httpx at the transport layer, so the full tool logic runs in CI without a real server.

uv run pytest

Add to Claude Code

Edit ~/.claude.json:

{
  "mcpServers": {
    "jellyfin": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/jellyfin-mcp", "jellyfin-mcp"],
      "env": {
        "JELLYFIN_URL": "http://192.168.1.100:8096",
        "JELLYFIN_API_KEY": "your_api_key_here",
        "JELLYFIN_USER_ID": "your_user_id_here"
      }
    }
  }
}

Restart Claude Code. The tools appear automatically in the MCP panel.

Add to Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows) — same JSON structure as above.


Letterboxd integration

If you want the recommendation engine to use your actual star ratings instead of just watch history:

  1. Export your data from letterboxd.com/settings/data
  2. Set up Postgres and run the import script:
uv run python scripts/import_letterboxd.py /path/to/letterboxd-export/watched.csv
  1. Link your Letterboxd films to Jellyfin items. Either ask Claude directly:
"Sync my Letterboxd matches"

Or run iteratively until all rows are matched:

# Dry run first — see what would be matched
uv run python -c "
import asyncio
from jellyfin_mcp.tools import sync_letterboxd_matches
print(asyncio.run(sync_letterboxd_matches(dry_run=True)))
"

The sync tool matches by normalized title + year (±1 tolerance), handling punctuation and article differences automatically. Unresolved titles (translated names, alternate spellings) are returned in the unresolved list for manual review.


Project structure

src/jellyfin_mcp/
├── server.py       # Entrypoint — mcp.run(transport="stdio")
├── tools.py        # All MCP tool definitions (@mcp.tool())
├── client.py       # Async httpx Jellyfin + TMDB clients
├── models.py       # Pydantic v2 response models
├── config.py       # pydantic-settings env config (3 settings classes)
└── db.py           # asyncpg connection pool + rating helpers

scripts/
├── import_letterboxd.py   # One-shot Letterboxd CSV → Postgres import
└── index_subtitles.py     # Subtitle embedding pipeline (see Extending)

tests/
└── test_tools.py   # respx-mocked async tests for every tool

Technical highlights

  • FastMCP + type hints → auto JSON schema@mcp.tool() reads Python type annotations and docstrings to generate the MCP tool schema. No separate schema files or manual registration.

  • Composable async toolsfind_unwatched_by_director makes three chained API calls (search by person, fetch watch state, filter) in a single tool invocation, returning a clean result. search_by_cast runs N parallel searches and deduplicates by overlap count.

  • Graceful degradation — TMDB and Postgres integrations are fully optional. The server detects whether they're configured at startup and silently omits those code paths. TMDB failures during similarity search are caught and ignored; the tool returns Jellyfin-only results.

  • No crashes exposed to Claude — every tool catches its error type and returns a descriptive string. Claude can explain the error to the user and suggest a fix rather than receiving an opaque exception.

  • Testable without infrastructurerespx intercepts httpx at the transport level. Tests exercise the full tool → client → model → response pipeline with realistic fixture data, no mocking of internal functions.

  • IDF genre scoring — avoids the naive "count matching genres" approach by weighting genres by their inverse document frequency in the library. This is the same concept as TF-IDF in information retrieval, applied to a local media collection.


Extending

Ideas for what to build next:

  • Scene search (in progress)index_subtitles.py chunks and embeds subtitle tracks into pgvector using all-MiniLM-L6-v2. The intent: ask Claude "pull up the scene where Jules recites the Bible verse" and get a deeplink to that exact timestamp. The pipeline works; the MCP tool is being hardened.
  • TV series support — episode-level watch state, "find the next unwatched episode" tool
  • Music tools — now-playing for audio sessions, playlist management
  • Multi-user — accept user_id as a tool parameter to support households with multiple Jellyfin accounts
  • SSE transport — run as a persistent HTTP service instead of a per-conversation subprocess
  • Webhook listener — emit MCP notifications when new content is added to the library

License

MIT

About

Jellyfin MCP server that gives Claude natural-language access to your media library — personalized recommendations, IDF-weighted similarity search, Letterboxd rating integration, and playback control.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors