Files
gronod c660050d7e
Build and publish / Test and build (darwin) (push) Successful in 2m21s
Build and publish / Test and build (windows) (push) Successful in 2m53s
Build and publish / Test and build (linux) (push) Successful in 3m38s
Build and publish / Build and publish Docker image (push) Successful in 2m31s
docs: pin container image to 1.0.0
2026-09-21 16:51:52 +01:00

251 lines
7.9 KiB
Markdown

# emby-mcp (Go)
Go rewrite of the Python [Emby.MCP](https://github.com/angelltek/Emby.MCP) server — a Model Context
Protocol (MCP) server that connects an Emby media server to an AI client such
as Claude Desktop.
The original 20 tools match the Python version's parameter names and JSON
output shapes; additional Go-only tools add library browsing, next-episode
resolution, and subtitle/audio-track control.
## Features
* Log on to / log out of an Emby media server (username/password, or API key)
* List libraries, select a library, list genres
* Search items by title/album, artist, genre, release years, lyrics, and
item type (`item_types`, e.g. `Series`) — with chunked results for large
libraries
* Browse an item's children, a series' seasons, and a series' episodes
(`retrieve_item_children`, `retrieve_season_list`, `retrieve_episode_list`),
including per-user played state and resume positions
* Resolve the next episode to play (`retrieve_next_episode`, modes
`next_unplayed`/`latest`) without a library selection
* Create/modify playlists, add/remove/reorder items, share playlists
* List player sessions, retrieve play queues, control playback
(play/pause/seek/skip, PlayNow)
* Inspect the now-playing item's audio/subtitle tracks
(`retrieve_now_playing`) and switch tracks (`set_subtitle`,
`set_audio_track`) — mid-playback where the player supports it, else
restart-at-position
## Build
```
go build -o emby-mcp ./cmd/emby-mcp
```
Requires Go 1.27+.
## Configuration
Create a `.env` file (in the working directory or next to the binary):
```
EMBY_SERVER_URL = "http://localhost:8096"
# Either username/password:
EMBY_USERNAME = "user"
EMBY_PASSWORD = "pass"
# ...or an API key plus the user ID it should act as:
# EMBY_API_KEY = "..."
# EMBY_USER_ID = "..."
# Set to False to skip SSL certificate verification (e.g. self-signed).
EMBY_VERIFY_SSL = True
# Max items per search-result chunk; 0 = no limit.
LLM_MAX_ITEMS = 100
# Transport: "stdio" (default) or "http" (streamable HTTP at /mcp).
MCP_TRANSPORT = stdio
# HTTP mode: bind address and idle-session timeout.
MCP_LISTEN_ADDR = "127.0.0.1:8080"
MCP_SESSION_TIMEOUT = 30m
```
In `http` mode the `EMBY_USERNAME`/`EMBY_PASSWORD`/`EMBY_API_KEY` variables are
not needed — every request authenticates against Emby (see below).
## Usage
Startup checks only (login + list libraries, then exit):
```
./emby-mcp -check
```
Run the MCP server on stdio:
```
./emby-mcp
```
### HTTP mode
```
MCP_TRANSPORT=http MCP_LISTEN_ADDR=0.0.0.0:8080 ./emby-mcp
```
Serves the MCP [streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports)
transport (SSE streaming) at `POST http://<addr>/mcp`. `/healthz` is an
unauthenticated liveness endpoint.
Every request must carry Emby credentials:
* `Authorization: Basic base64(user:pass)` — exchanged for an Emby access
token via `AuthenticateByName` (cached ~15 min).
* `Authorization: Bearer <token>` — an Emby access token or API key used
directly. The token's owning user is resolved automatically via
`GET /Sessions`; server-wide API keys and other ambiguous tokens require a
disambiguation header:
* `X-Emby-User-Id: <id>` — the Emby user to act as (validated), or
* `X-Emby-Username: <name>` — looked up via the public user list.
Each MCP session is isolated: it gets its own library selection and
search-chunking state, and requests to a session are rejected if they
authenticate as a different user.
Plain HTTP only — put the server behind a reverse proxy for TLS.
### Docker
```
docker run -e EMBY_SERVER_URL="http://emby:8096" -p 8080:8080 \
git.i3omb.com/gronod/emby-mcp:1.0.0
```
The image defaults to `MCP_TRANSPORT=http` on `0.0.0.0:8080`.
### Docker Compose / TrueNAS SCALE
The YAML below is a Compose v2 file suitable for a TrueNAS SCALE Custom App
(Apps → Discover → Custom App / Install via YAML) as well as plain
`docker compose up -d`.
Replace the placeholder values. On SCALE, map the same keys in the app
environment UI if you prefer not to bake them into the file. Use the NAS
host/IP (or an internal container DNS name) for `EMBY_SERVER_URL` so the
app can reach Emby; `host` networking is an alternative if Emby is also
on the same TrueNAS box.
In HTTP mode (`MCP_TRANSPORT=http`, the image default) per-request Emby
auth is used and `EMBY_USERNAME` / `EMBY_PASSWORD` / `EMBY_API_KEY` are
optional. They are included so the same file also works if you switch
the container to stdio or want a default identity.
```yaml
# emby-mcp — TrueNAS SCALE Custom App / docker compose
# Placeholders: replace every CHANGE_ME_* value before starting.
services:
emby-mcp:
image: git.i3omb.com/gronod/emby-mcp:1.0.0
container_name: emby-mcp
restart: unless-stopped
ports:
- "8080:8080"
environment:
EMBY_SERVER_URL: "http://CHANGE_ME_EMBY_HOST:8096"
# Username/password login (leave empty if using an API key):
EMBY_USERNAME: "CHANGE_ME_EMBY_USERNAME"
EMBY_PASSWORD: "CHANGE_ME_EMBY_PASSWORD"
# API-key login (leave empty if using username/password).
# EMBY_USER_ID is required when EMBY_API_KEY is set:
EMBY_API_KEY: "CHANGE_ME_EMBY_API_KEY"
EMBY_USER_ID: "CHANGE_ME_EMBY_USER_ID"
EMBY_VERIFY_SSL: "true"
LLM_MAX_ITEMS: "100"
MCP_TRANSPORT: "http"
MCP_LISTEN_ADDR: "0.0.0.0:8080"
MCP_SESSION_TIMEOUT: "30m"
# Scratch image has no shell/curl — omit healthcheck, or front with a proxy.
# networks: [host] # uncomment instead of ports: if Emby is on the same NAS
```
MCP endpoint after start: `POST http://<truenas-host>:8080/mcp`
Liveness: `GET http://<truenas-host>:8080/healthz`
### Claude Desktop example (stdio)
```json
{
"mcpServers": {
"Emby": {
"command": "/path/to/emby-mcp",
"args": ["-env", "/path/to/.env"]
}
}
}
```
### Streamable HTTP example (`mcpServers`)
Point an MCP client at the container's `/mcp` endpoint. Every request must
carry Emby credentials in headers (see HTTP mode above). Replace the
placeholders.
API key (add `X-Emby-User-Id` or `X-Emby-Username` if the key is server-wide):
```json
{
"mcpServers": {
"Emby": {
"type": "http",
"url": "http://CHANGE_ME_MCP_HOST:8080/mcp",
"headers": {
"Authorization": "Bearer CHANGE_ME_EMBY_API_KEY",
"X-Emby-User-Id": "CHANGE_ME_EMBY_USER_ID"
}
}
}
}
```
Username and password (sent as HTTP Basic; the server exchanges them for an
Emby access token):
```json
{
"mcpServers": {
"Emby": {
"type": "http",
"url": "http://CHANGE_ME_MCP_HOST:8080/mcp",
"headers": {
"Authorization": "Basic CHANGE_ME_BASE64_USER_COLON_PASS"
}
}
}
}
```
`CHANGE_ME_MCP_HOST` is the host where emby-mcp is published (TrueNAS IP or
hostname), not the Emby server. `CHANGE_ME_BASE64_USER_COLON_PASS` is
`base64("<username>:<password>")`. Clients that do not understand `"type":
"http"` may accept the same block with only `"url"` and `"headers"`.
## Development
```
go build ./...
go vet ./...
go test ./...
```
Layout:
* `cmd/emby-mcp` — entry point, config loading, startup checks
* `internal/emby` — Emby REST client (auth, libraries, items, playlists, sessions)
* `internal/server` — MCP tool handlers (official go-sdk)
* `internal/mcphttp` — streamable-HTTP transport with per-request Emby auth
* `internal/state` — shared session state (library selection, search chunking)
* `internal/config` — `.env` parsing
* `internal/textutil` — Unicode→ASCII folding for lyrics search
* `docs/api/emby_openapi.json` — Emby REST API spec (reference)
Upstream endpoint conformance is checked by `internal/emby/spec_conformance_test.go`; see [`docs/api/endpoint_validation.md`](docs/api/endpoint_validation.md) for the audit report. Planned features live in [`docs/ROADMAP.md`](docs/ROADMAP.md).
## License
GPL v3 — see `LICENCE.md` (same license as the upstream
[angelltek/Emby.MCP](https://github.com/angelltek/Emby.MCP) project).