Skip to main content
Glama
streamystats-mcp-server

streamystats-mcp-server

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: jellyfin-api

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.py — opt-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

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    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
      -
    • A
      license
      A
      quality
      C
      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
    • A
      license
      A
      quality
      B
      maintenance
      Enables AI agents to interact with a Jellyfin media server, allowing them to search the library, fetch item details, list active playback sessions, and retrieve next-up episodes.
      5
      MIT