Skip to main content
Glama
README.md
# plex-mcp-server

A [Model Context Protocol](https://modelcontextprotocol.io) server for **Plex Media Server**.

Two layers in one server:

1. **Every documented Plex Media Server API operation as an MCP tool.** The server ships an OpenAPI 3.1 document of the Plex API, generates one tool per operation from it, and re-generates the document from Plex's published documentation on demand. Nothing is hand-waved: if Plex documents it, there is a tool for it.
2. **Curated tools** that do what a caller would otherwise have to reconstruct — resolve a title to a rating key, read Plex's filter vocabulary, compute per-user statistics, recommend something to watch, control a client, write a spoiler-free recap of a film in progress.

```
plex_library_list            plex_sessions_list           plex_media_search
plex_media_edit              plex_playlist_create         plex_collection_create
plex_analytics_overview      plex_recommendations         plex_clients_control
plex_subtitles_dialogue      plex_watchlist_add           plex_stream_url
plex_api_request             plex_endpoint_search         plex_library_section_get_all
…and one tool per documented operation
```

## Why this exists

There are several Plex MCP servers (see [Credits](#credits)). Each one is good at something different, and each one stops short of the full API. This project merges the useful parts, adds complete endpoint coverage generated from Plex's own OpenAPI document, and ships the specification so other tools can consume it.

## Quick start

You need a Plex server address and a token.

```bash
git clone https://github.com/aminamos/plex-mcp-server.git
cd plex-mcp-server
npm install
npm run build
```

### Get a Plex token

1. Sign in to Plex in a browser.
2. Open any library item, then **Get Info → View XML**.
3. Copy the `X-Plex-Token` query parameter from the URL that opens.

Or, if you only have an account token, leave `PLEX_URL` unset: the server discovers your owned servers through `plex.tv/api/v2/resources` and picks the best connection.

### Wire it into an MCP client

```json
{
  "mcpServers": {
    "plex": {
      "command": "node",
      "args": ["/absolute/path/to/plex-mcp-server/dist/index.js"],
      "env": {
        "PLEX_URL": "http://192.168.1.10:32400",
        "PLEX_TOKEN": "your-token"
      }
    }
  }
}
```

A `.env` file in the working directory (or `~/.config/plex-mcp-server/.env`) is also read, so you can keep credentials out of client configs:

```env
PLEX_URL=http://192.168.1.10:32400
PLEX_TOKEN=xxxxxxxxxxxx
```

### HTTP transport

```bash
node dist/index.js --transport http --host 127.0.0.1 --port 3000 --path /mcp
```

Streamable HTTP, with optional access control:

| Mode | Variables |
| --- | --- |
| None (default, and the default bind is `127.0.0.1`) | — |
| Static bearer token | `MCP_AUTH_TOKEN` |
| OAuth 2.1 with an external authorization server (RS256, verified against the issuer's JWKS) | `MCP_OAUTH_ISSUER`, `MCP_OAUTH_AUDIENCE` |

With OAuth configured, the server publishes `/.well-known/oauth-protected-resource` and proxied `/.well-known/oauth-authorization-server` metadata, and answers unauthenticated requests with a `WWW-Authenticate` challenge — the flow remote MCP clients expect.

## Configuration

Flags override environment variables, which override `.env`.

| Flag | Environment | Default | Meaning |
| --- | --- | --- | --- |
| `--plex-url` | `PLEX_URL` | discovered from plex.tv | Plex Media Server base URL |
| `--plex-token` | `PLEX_TOKEN` | — | `X-Plex-Token` for the server |
| `--cloud-token` | `PLEX_CLOUD_TOKEN` | falls back to `PLEX_TOKEN` | plex.tv account token (account, watchlist, Discover) |
| `--client-identifier` | `PLEX_CLIENT_IDENTIFIER` | generated once, then cached | Stable `X-Plex-Client-Identifier` |
| `--transport` | `MCP_TRANSPORT` | `stdio` | `stdio` or `http` |
| `--host` / `--port` / `--path` | `MCP_HTTP_HOST` / `MCP_HTTP_PORT` / `MCP_HTTP_PATH` | `127.0.0.1` / `3000` / `/mcp` | HTTP bind |
| `--tools` | `PLEX_TOOL_MODE` | `all` | `all` = one tool per documented operation; `curated` = only the hand-written tools |
| `--tags` | `PLEX_TOOL_TAGS` | — | Restrict tools to these OpenAPI tags, e.g. `Library,Status` |
| `--include` / `--exclude` | `PLEX_TOOL_INCLUDE` / `PLEX_TOOL_EXCLUDE` | — | Filter tools by name substring |
| `--read-only` | `PLEX_READ_ONLY` | off | Do not register or allow anything that is not `GET` |
| `--format` | `PLEX_OUTPUT_FORMAT` | `auto` | `json`, `compact` (tabular), or `auto` (shortest) |
| — | `PLEX_MAX_ITEMS` | `250` | Cap on items returned from one list endpoint |
| — | `PLEX_MAX_DESCRIPTION_LENGTH` | `1200` | Cap on generated tool descriptions |
| — | `PLEX_REQUEST_TIMEOUT_MS` | `30000` | HTTP timeout |
| — | `PLEX_CACHE_TTL_MS` | `0` | In-memory GET cache; `0` disables |
| — | `OPENSUBTITLES_API_KEY` | — | Enables the subtitle tools |
| — | `MCP_LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` (stderr only) |
| — | `PLEX_SERVERS` / `PLEX_SERVERS_TOKENS` | — | Extra servers for cross-server search |

### A note on tool count

Full coverage means a lot of tools: `--tools all` advertises one tool per documented operation, and a `tools/list` response of that size costs real context (order of tens of thousands of tokens, depending on the document version). Options:

- `--tools curated` — a few dozen hand-written tools, smallest footprint.
- `--tags Library,Status` — keep the generated coverage but only for the areas you care about.
- Keep `all` and let the client search: every generated tool is named after its operation, and `plex_endpoint_search` finds operations by keyword without loading the whole list.

`--read-only` is enforced at registration and again at call time: mutating tools — generated *and* curated — are not registered at all, and the escape hatch (`plex_api_request`) refuses non-`GET` methods. A read-only server therefore never advertises a tool that cannot run.

## What is in the box

**Generated tools.** One per operation of the bundled OpenAPI document, named `plex_` + the snake_cased `operationId`, e.g. `GET /library/sections/{sectionId}/all` → `plex_library_section_get_all`. Parameters (path and query — Plex documents nearly everything as query parameters) become the tool's JSON schema, and the tool performs exactly that request. See [`docs/ENDPOINTS.md`](docs/ENDPOINTS.md) for the full list.

Where a curated tool and a generated tool would share a name, the curated one wins and the generated one is not registered — that is deliberate for the three cases where the curated version does strictly more work (resolving a title to a Discover GUID for the watchlist, cross-checking Discover results against the local library). Use `plex_api_request` to reach the raw endpoint form of those operations.

**Curated tools.** See [`docs/TOOLS.md`](docs/TOOLS.md) for the generated catalogue. The areas they cover:

| Area | Examples |
| --- | --- |
| Library | list sections, per-section statistics, recently added, scan / refresh / analyze / empty trash, filter and sort vocabulary |
| Media | cross-library search, full detail, children (seasons, episodes), metadata edit with lock semantics, artwork, watch state, ratings |
| Playlists & collections | create from rating keys, add/remove items, edit, copy to a user |
| Sessions | live streams, watch history, terminate a stream, transcodes |
| Server | info, bandwidth, host and process resources, Butler tasks, preferences, clean bundles, database optimisation |
| Clients | inventory and timelines, playback control (play / pause / stop / seek / step / skip), stream selection |
| Users | shared and home users, per-user on-deck, watch history, activity statistics |
| Analytics | play/user/popular/library reporting over any window, computed in-process |
| Recommendations | scored suggestions from a user's actual watch history, with reasons |
| Watchlist & Discover | the account watchlist, and Discover search with "already in my library" cross-checks |
| Subtitles | find subtitles, and extract dialogue **up to a playback position** |
| Streaming | direct-play URLs, transcode decisions, subtitle streams |

**Discovery tools.**

- `plex_endpoint_search` — find an operation by keyword; returns the tool name, method, path and required arguments.
- `plex_api_request` — arbitrary request to the server, for endpoints, parameters or iterators that are not in the document.
- `plex_api_info` — what the bundled document contains and how tools are named.

**Resources.** `plex://openapi/index` (the whole endpoint catalogue in one read), `plex://openapi/tag/<tag>`, `plex://openapi/document` (the raw OpenAPI document), `plex://registry` (every tool with tag, access class and schema), and `plex://server/info` (the live server).

**Prompts.** `library_report`, `server_health`, and `what_have_i_missed` — the last one reads the active session, pulls the subtitles for what is playing, and asks for a recap that stops at the current playback position, so it cannot spoil the rest of the film.

## The OpenAPI document

`spec/plex-pms.openapi.json` (and `.yaml`) is generated, not hand-maintained:

```bash
npm run spec:build      # fetch developer.plex.tv/pms, extract, normalise, merge extensions
npm run spec:validate   # structural + policy checks
```

Plex publishes the Plex Media Server API as documentation rendered by ReDoc from an OpenAPI 3.1 document embedded in the page. `scripts/build-spec.mjs` extracts that document, normalises it (title, summary, server variables, provenance), and merges `spec/extensions.yaml`.

`spec/extensions.yaml` is hand-authored and holds endpoints in wide use that Plex's published document omits — `/library/sections`, `/library/recentlyAdded`, `/library/metadata/{ids}/children`, `/statistics/bandwidth`, `/clients`, `/diagnostics/*`, the Plex Cloud account endpoints and the Discover watchlist. Each one is marked `x-plex-extension: true`, and each carries the MCP-specific annotations this project defines:

| Extension | Meaning |
| --- | --- |
| `x-mcp-tool` | Explicit MCP tool name for the operation |
| `x-mcp-host` | `pms` (default), `cloud` (plex.tv) or `discover` (discover.provider.plex.tv) |
| `x-mcp-tag` | Override the tag used for `PLEX_TOOL_TAGS` filtering |
| `x-mcp-skip` | Do not generate a tool for this operation |
| `x-mcp-notes` | Extra guidance appended to the generated tool description |

Provenance — source URL, extraction timestamp, upstream version, operation counts — is recorded in `spec/plex-pms.openapi.json` under `x-plex-provenance`.

### Licensing of the document

The upstream document is published by Plex under Apache 2.0; that licence and attribution are preserved in `info.license` and `x-plex-provenance`. `spec/extensions.yaml` and everything under `src/` are this project's own work, under MIT.

## Development

```bash
npm run build      # tsc → dist/
npm test           # vitest
npm run smoke      # end-to-end: real server process, mock Plex, real MCP round trip
npm run spec:build # regenerate the OpenAPI document
npm run docs       # regenerate docs/TOOLS.md and docs/ENDPOINTS.md from the running server
npm run check      # build + validate the spec + tests
```

The smoke test is the honest one: it boots `dist/index.js` as a subprocess, points it at a mock Plex server, and drives `initialize` / `tools/list` / `tools/call` / `resources/read` / `prompts/get` over stdio, asserting both the MCP surface and the HTTP requests the server makes.

## Limitations

- Subtitle tools need an OpenSubtitles API key; without it they fail with a clear message and the rest of the server is unaffected.
- `plex_clients_*` tools talk to the client devices directly, so they need the client's host and port to be reachable from wherever this server runs.
- Plex's documented response format is XML by default; this server always asks for JSON, and reports the raw body as text when an endpoint answers with something that is not JSON (transcode playlists, image endpoints).
- Generated tools return Plex's payloads trimmed to `PLEX_MAX_ITEMS` with an explicit truncation marker. Curated tools return shaped results.
- The bundled document is a snapshot. Re-run `npm run spec:build` to pick up Plex's changes.

## Credits

This server merges the useful parts of four existing projects. No code was copied — the implementations here are new — but the scope, the tool taxonomy and several features come directly from them:

- **[vladimir-tutin/plex-mcp-server](https://github.com/vladimir-tutin/plex-mcp-server)** — the broad tool taxonomy (library, media, playlist, collection, user, sessions, server, client), the remote-access story with OAuth against an external issuer, and administrative tooling (logs, bandwidth, Butler, empty trash, optimise database, clean bundles).
- **[niavasha/plex-mcp-server](https://github.com/niavasha/plex-mcp-server)** — the architecture: one unified binary, tools separated from their schemas and implementation, tool annotations, write-operation gating, a compact/TOON-style tabular encoding that is only used when it is actually shorter, and viewing analytics built from Plex's own endpoints.
- **[eddmann/plex-mcp](https://github.com/eddmann/plex-mcp)** — the idea worth stealing: ground answers in subtitles up to the current playback position, so a recap of a film in progress cannot spoil it. That is `plex_subtitles_dialogue` and the `what_have_i_missed` prompt.
- **[BenjaminOddou/alfred-plex](https://github.com/BenjaminOddou/alfred-plex)** — search and filter ergonomics (reading Plex's filter/sort vocabulary and exposing it to a caller), Discover search, server actions (settings, statistics, history), the watchlist toggle, deep links into Plex Web, and streaming URLs handed to a player.

Endpoint coverage comes from Plex's own documentation at <https://developer.plex.tv/pms/>.

## License

MIT — see [LICENSE](LICENSE). The generated OpenAPI document retains Plex's Apache 2.0 attribution; the details are in [NOTICE](NOTICE) and inside the document's `info.license` and `x-plex-provenance` fields.