Skip to main content
Glama
Barrow1990

streamystats-mcp-server

by Barrow1990

streamystats-mcp-server

A minimal Model Context Protocol server that connects to Streamystats (a self-hosted watch-statistics dashboard for Jellyfin), packaged for Docker.

It runs as a standing network service (streamable-http transport, not stdio), so any MCP client on your internal network can connect to http://<host>:<port>/mcp — the container isn't spawned per-client, and container lifecycle/updates can be handed off to a tool like Dockhand.

Read this before trusting it for "watch stats"

Streamystats has no documented public REST API. Its dashboard, watch-time, most-watched, and per-user history pages are server-rendered React components that query its Postgres database directly — they are not exposed as JSON endpoints any external client (including this one) can call. This was confirmed by reading its main branch source directly (commit 1a154af6e59b, 2026-09-13), not by guessing from the README, which doesn't document an API at all.

Only six app/api/**/route.ts handlers exist in the whole codebase, and of those, only three groups accept external-client authentication (requireAuth/no-auth-at-all in lib/api-auth.ts) rather than requiring a logged-in browser session cookie (requireSession/requireAdmin, which this server cannot obtain and does not attempt to):

Endpoint

Auth

Usable from here?

GET /api/servers

none

yes — list_servers

GET /api/health

none

yes — system_status

GET /api/version

none

yes — system_status

GET /api/search

external-client OK

yes — search

GET /api/watchlists

external-client OK

yes — list_watchlists

POST /api/watchlists

external-client OK

yes — create_watchlist

everything else (dashboard stats, history, sessions, libraries, ...)

session cookie only

no

That means this server cannot answer "what's my most-watched show" or "how much has user X watched" — despite that being Streamystats' whole purpose — because there is no API for it to call. If Streamystats adds a documented stats API in the future, this server should be extended to use it; until then, use the Streamystats web dashboard directly for that data.

Treat everything here as best-effort against an undocumented, unofficial surface that a Streamystats upgrade could change or remove with no notice. tests/test_live_streamystats.py exists specifically to catch that kind of drift against a real instance — see Testing.

Related MCP server: Jellyseerr MCP Server

Tools

Tool

Description

list_servers

List the Jellyfin servers Streamystats is tracking, with sync status

search

Search Streamystats' indexed library items, users, watchlists, activity, sessions, and people

list_watchlists

List watchlists visible to the authenticated Jellyfin identity

create_watchlist

Create a new watchlist

system_status

Streamystats liveness and version/update-check info

create_watchlist is the only tool that changes state. Everything else is read-only.

Authentication to Streamystats

Streamystats has no API key of its own for external clients. Per its lib/api-auth.ts, it instead accepts a real Jellyfin API key or user access token, sent as Authorization: MediaBrowser Token="<token>", and validates that token directly against the Jellyfin server(s) it has registered. Set STREAMYSTATS_JELLYFIN_TOKEN to a Jellyfin API key (Jellyfin > Dashboard > API Keys) — not anything issued by Streamystats itself, because it issues nothing.

Health endpoints

Two plain HTTP endpoints, reachable without MCP_AUTH_TOKEN (so Docker's HEALTHCHECK, Dockhand, or any other monitor can poll them without the secret):

Endpoint

Checks

Healthy

Unhealthy

GET /health

The process is up and serving HTTP. Does not call Streamystats.

200 {"status": "ok"}

(doesn't respond)

GET /ready

STREAMYSTATS_URL is reachable (/api/health), and STREAMYSTATS_JELLYFIN_TOKEN is accepted by Streamystats' external-client auth (a 1-result /api/search probe).

200 {"status": "ok", "reachable": true, "authenticated": true, "streamystats": {...}}

503 {"status": "error", "reachable": ..., "authenticated": ..., "error": "..."}

Configuration

Environment variables (see .env.example):

Variable

Required

Default

Description

STREAMYSTATS_URL

yes

e.g. http://192.168.1.50:3000

STREAMYSTATS_JELLYFIN_TOKEN

yes

A Jellyfin API key/token — see Authentication

MCP_HOST

no

0.0.0.0

Interface the server binds to inside the container

MCP_PORT

no

8940

Port the server listens on

MCP_AUTH_TOKEN

no

Shared secret required as Authorization: Bearer <token>. Unset = no auth

Image

Built and pushed to ghcr.io/barrow1990/streamystats-mcp-server by .github/workflows/ci.yml on every push to main that passes tests, tagged :latest and :<commit-sha>. docker-compose.yml pulls :latest by default; swap in build: . there instead if you'd rather build locally from the Dockerfile.

Same three-stage scratch-flattened Alpine build as the sonarr/radarr MCP servers (~98MB) — see those repos' READMEs for why that design shrinks the image the way it does.

Running with Docker Compose

cp .env.example .env   # fill in STREAMYSTATS_URL / STREAMYSTATS_JELLYFIN_TOKEN
docker compose up -d --pull always

The server is then reachable at http://<docker-host>:8940/mcp from anything on your internal network.

Managing with Dockhand

Point Dockhand at ghcr.io/barrow1990/streamystats-mcp-server and let it track new tags. Make the GHCR package public, or every pull will need docker login ghcr.io with a PAT on each deploy host.

Connecting a client

Claude Code

claude mcp add streamystats -s user --transport http http://<docker-host>:8940/mcp \
  --header "Authorization: Bearer <MCP_AUTH_TOKEN>"

(Drop the --header flag if you're running with MCP_AUTH_TOKEN unset.)

Claude Desktop

{
  "mcpServers": {
    "streamystats": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote", "http://<docker-host>:8940/mcp",
        "--header", "Authorization: Bearer <MCP_AUTH_TOKEN>"
      ]
    }
  }
}

Running without Docker

pip install -r requirements.txt
STREAMYSTATS_URL=http://192.168.1.50:3000 STREAMYSTATS_JELLYFIN_TOKEN=your-jellyfin-key \
MCP_AUTH_TOKEN=your-shared-secret python server.py

Testing

pip install -r requirements-dev.txt
python -m pytest tests/ -v
  • tests/test_tools.py — each tool's logic against a mocked Streamystats (httpx.MockTransport, no extra mocking library needed).

  • tests/test_http.py/health, /ready, and the bearer-auth middleware, via server.build_app() (the exact app __main__ runs) through Starlette's TestClient.

  • tests/test_live_streamystats.pyopt-in contract tests against a real Streamystats instance, to catch this server's biggest real risk: that an upgrade removes or re-authenticates the undocumented endpoints this whole integration depends on. Skipped by default (no Streamystats in CI); run with:

    RUN_LIVE_STREAMYSTATS_TESTS=1 STREAMYSTATS_URL=https://streamystats.example.com \
    STREAMYSTATS_JELLYFIN_TOKEN=<real jellyfin api key> python -m pytest tests/test_live_streamystats.py -v

CI (.github/workflows/ci.yml) runs the mocked suite on every push/PR; the GHCR build only runs after it passes.

License

MIT

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    An MCP (Multi-Agent Conversation Protocol) Server that enables interaction with Jellyfin media server APIs, auto-generated using AG2's MCP builder based on the Jellyfin OpenAPI specification.
    1
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables interaction with Jellyseerr media request systems through natural language. Supports searching for media, creating requests, checking request status, and managing your media library workflow.
    8
    -
  • A
    license
    A
    quality
    D
    maintenance
    MCP server for managing a media server stack (Plex, Radarr, Overseerr, Bazarr, Prowlarr, Trakt.tv) using natural language to browse, request, and discover content.
    12
    MIT