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
251 lines
7.9 KiB
Markdown
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).
|