Expose session/last-used user and device IP on player rows. Filter retrieve_player_list by users, merge GET /Devices when include_offline is set, and resolve spoken names to HA media_player entities via player_links (device_id or IP). PlayNow still requires a live session.
528 lines
19 KiB
Markdown
528 lines
19 KiB
Markdown
# Emby MCP — documentation
|
|
|
|
Full configuration and usage documentation for the **Emby MCP** Home Assistant
|
|
add-on, plus standalone (non-add-on) usage of the underlying Go server.
|
|
|
|
## Installation
|
|
|
|
1. Add the repository to Home Assistant
|
|
(**Settings → Add-ons → Add-on Store → ⋮ → Repositories**):
|
|
`https://git.i3omb.com/gronod/ha-gronod-addons`
|
|
2. Install **Emby MCP**
|
|
3. Set `emby_server_url` plus username/password or an API key
|
|
4. Leave **Restrict to localhost** enabled and start the add-on
|
|
|
|
The add-on runs the Go Emby.MCP server in one container and exposes:
|
|
|
|
- Streamable MCP at `/mcp` (Claude, ChatGPT, and other MCP clients)
|
|
- REST tool calls at `/call/{tool}` (Home Assistant REST / conversation tools)
|
|
- Health checks at `/healthz` and `/health`
|
|
|
|
## Configuration
|
|
|
|
| Option | Default | Description |
|
|
|---|---|---|
|
|
| `emby_server_url` | `http://homeassistant.local:8096` | Emby base URL |
|
|
| `emby_username` | _(empty)_ | Stored Emby username |
|
|
| `emby_password` | _(empty)_ | Stored Emby password |
|
|
| `emby_api_key` | _(empty)_ | Stored Emby API key |
|
|
| `emby_user_id` | _(empty)_ | User id when using an API key |
|
|
| `restrict_to_localhost` | `true` | Allow only loopback + HA container network; apply stored creds |
|
|
| `emby_verify_ssl` | `true` | Verify TLS when talking to Emby |
|
|
| `llm_max_items` | `100` | Max items per search chunk |
|
|
| `mcp_transport` | `http` | Keep `http` |
|
|
| `mcp_listen_addr` | `0.0.0.0:8085` | Bind address |
|
|
| `mcp_session_timeout` | `30m` | MCP session lifetime |
|
|
| `log_level` | `INFO` | `DEBUG`, `INFO`, or `WARN` |
|
|
| `player_users` | `[]` | Default include-list of Emby usernames/ids applied when a tool call omits `users` |
|
|
| `player_links` | `[]` | Maps spoken names to HA `media_player` + Emby device. `names` is comma-separated. Set `device_ip` when the TV's LAN IP is known (Emby `device_ip_address` on a live session). WebOS often does **not** put that IP on the HA entity — copy it from the router or from an Emby session while the app is open. `emby_device_id` is the most stable key. |
|
|
| `debug.rest` | _(optional)_ | Shown under optional options. At DEBUG, log REST bodies |
|
|
| `debug.mcp` | _(optional)_ | Shown under optional options. At DEBUG, log `/mcp` bodies |
|
|
| `debug.emby` | _(optional)_ | Shown under optional options. At DEBUG, log Emby API bodies |
|
|
|
|
With **Restrict to localhost** on (default), only processes on the Home
|
|
Assistant machine can connect, and those local calls use the stored
|
|
credentials when no `Authorization` header is present.
|
|
|
|
Turn the switch off only if you need LAN/remote MCP clients. In that mode
|
|
stored credentials are **not** applied automatically; each client must send
|
|
its own `Authorization` header.
|
|
|
|
## Endpoints
|
|
|
|
| Path | Method | Purpose |
|
|
|---|---|---|
|
|
| `/mcp` | POST | Streamable MCP |
|
|
| `/call/{tool}` | POST | REST bridge → MCP tool |
|
|
| `/tools` | GET | List tools |
|
|
| `/healthz` | GET | Liveness |
|
|
| `/health` | GET | Bridge liveness |
|
|
|
|
## Auth
|
|
|
|
Send Emby credentials on every `/mcp` and `/call/...` request when
|
|
`restrict_to_localhost` is off (or when calling remotely):
|
|
|
|
- `Authorization: Basic <base64(user:pass)>`
|
|
- `Authorization: Bearer <emby-token-or-api-key>`
|
|
- Optional: `X-Emby-User-Id` or `X-Emby-Username`
|
|
|
|
Username/password can also be stored in the add-on options; they are applied
|
|
automatically to local calls while `restrict_to_localhost` is on.
|
|
|
|
## Tool names
|
|
|
|
Use the MCP tool names, for example `search_for_item`, `retrieve_player_list`,
|
|
`retrieve_now_playing`, `control_media_player`, `retrieve_next_episode`.
|
|
|
|
Arguments not declared in the tool's current input schema are dropped before
|
|
the call is forwarded; the JSON response then includes
|
|
`"dropped_arguments": ["name", ...]` listing what was ignored.
|
|
|
|
## Conversation agent
|
|
|
|
Home Assistant Supervisor names a custom-repo add-on `{repo-hash}-{slug}`.
|
|
The repo hash is derived from the repository URL, so it depends on which URL
|
|
was added:
|
|
|
|
| Repository URL | Add-on hostname |
|
|
|---|---|
|
|
| `https://git.i3omb.com/gronod/ha-gronod-addons` | `8e663231-emby-mcp` |
|
|
| `https://git.i3omb.com/gronod/ha-emby-mcp` (pre-rename) | `c5cb4244-emby-mcp` |
|
|
|
|
The examples below use `8e663231-emby-mcp`. If you added the repository before
|
|
the rename, replace it with `c5cb4244-emby-mcp` throughout (it cannot be
|
|
changed to a custom hostname).
|
|
|
|
These steps assume the add-on has Emby credentials configured and
|
|
**Restrict to localhost** is on. Home Assistant then calls
|
|
`http://8e663231-emby-mcp:8085`; no `Authorization` header is required.
|
|
|
|
### System prompt
|
|
|
|
Add this line to the conversation agent's instructions:
|
|
|
|
```text
|
|
- Media & Emby: Never use execute_services for Emby search or playback. Use `emby_search` to find films, TV series, episodes, or music tracks. Use `emby_episode_list` to list or count a series' episodes (seasons via `emby_season_list`). Each episode item includes `premiere_date` (YYYY-MM-DD first air date) when Emby has that metadata; use it for airdate questions. Use `emby_resolve_player` when the user names a room or TV ("play on the lounge"). If `wake_required` is true, use Home Assistant `media_player.turn_on` on `ha_entity` and `media_player.select_source` with `ha_source` (Emby), wait, then resolve again. Only then use `emby_playback_control` PlayNow with that `session_id`. Use `emby_list_players` with `users` set to the household include-list to hide other people's clients. Use `emby_next_episode` when asked what episode to watch next.
|
|
```
|
|
|
|
### Functions
|
|
|
|
Paste the following into the conversation agent's **Functions** list
|
|
(Settings → Voice assistants → your agent → Functions):
|
|
|
|
```yaml
|
|
- spec:
|
|
name: emby_search
|
|
description: Search for films, TV series, episodes, or music tracks in the Emby media library.
|
|
parameters:
|
|
type: object
|
|
properties:
|
|
title_or_album:
|
|
type: string
|
|
description: Title of the media item, film, track, or album.
|
|
artist_name:
|
|
type: string
|
|
description: Name of the artist or band (optional).
|
|
genre_name:
|
|
type: string
|
|
description: Genre of the media (optional).
|
|
item_types:
|
|
type: string
|
|
description: "Comma-separated filter, e.g. 'Movie', 'Series', 'Episode', or 'Audio'. Leave empty for all."
|
|
required:
|
|
- title_or_album
|
|
function:
|
|
type: rest
|
|
resource_template: "http://8e663231-emby-mcp:8085/call/search_for_item"
|
|
method: POST
|
|
payload_template: >-
|
|
{{ {
|
|
"title_or_album": title_or_album,
|
|
"artist_name": artist_name | default(""),
|
|
"genre_name": genre_name | default(""),
|
|
"broadcast_release_years": "",
|
|
"item_types": item_types | default("")
|
|
} | to_json }}
|
|
value_template: "{{ value_json.result }}"
|
|
|
|
- spec:
|
|
name: emby_list_players
|
|
description: List Emby media players. Each row includes user_name, user_id, device_ip_address, and online. Pass users as a comma-separated include-list of Emby usernames so other household accounts are omitted. Set include_offline true to include last-used devices with no live session.
|
|
parameters:
|
|
type: object
|
|
properties:
|
|
media_type:
|
|
type: string
|
|
description: Filter by player type ('Video', 'Audio', 'Photo'), or leave empty for all.
|
|
users:
|
|
type: string
|
|
description: Comma-separated Emby usernames or user ids to include (household include-list).
|
|
include_offline:
|
|
type: boolean
|
|
description: Also list known Emby devices that have no live session (last-used user).
|
|
function:
|
|
type: rest
|
|
resource_template: "http://8e663231-emby-mcp:8085/call/retrieve_player_list"
|
|
method: POST
|
|
payload_template: >-
|
|
{{ { "media_type": media_type | default(""), "users": users | default(""), "include_offline": include_offline | default(false) } | to_json }}
|
|
value_template: "{{ value_json.result }}"
|
|
|
|
- spec:
|
|
name: emby_resolve_player
|
|
description: Resolve a room name, person, HA entity, Emby device name, device id, or IP to ha_entity/ha_source plus a live session_id when the Emby client is running. If wake_required is true, turn on the HA media_player and launch Emby before PlayNow.
|
|
parameters:
|
|
type: object
|
|
properties:
|
|
query:
|
|
type: string
|
|
description: Spoken name (lounge), media_player entity_id, Emby device name, device id, or IP.
|
|
users:
|
|
type: string
|
|
description: Comma-separated Emby usernames or user ids to include.
|
|
required:
|
|
- query
|
|
function:
|
|
type: rest
|
|
resource_template: "http://8e663231-emby-mcp:8085/call/resolve_media_player"
|
|
method: POST
|
|
payload_template: >-
|
|
{{ { "query": query, "users": users | default("") } | to_json }}
|
|
value_template: "{{ value_json.result }}"
|
|
|
|
- spec:
|
|
name: emby_now_playing
|
|
description: Get currently playing media details, audio tracks, and subtitle options for an active player session.
|
|
parameters:
|
|
type: object
|
|
properties:
|
|
session_id:
|
|
type: string
|
|
description: The player session ID obtained from emby_list_players.
|
|
required:
|
|
- session_id
|
|
function:
|
|
type: rest
|
|
resource_template: "http://8e663231-emby-mcp:8085/call/retrieve_now_playing"
|
|
method: POST
|
|
payload_template: >-
|
|
{{ { "session_id": session_id } | to_json }}
|
|
value_template: "{{ value_json.result }}"
|
|
|
|
- spec:
|
|
name: emby_playback_control
|
|
description: Send playback commands to an Emby player session (e.g. PlayNow, Pause, Unpause, Stop, NextTrack, PreviousTrack).
|
|
parameters:
|
|
type: object
|
|
properties:
|
|
session_id:
|
|
type: string
|
|
description: The target player session ID.
|
|
command:
|
|
type: string
|
|
description: "Command: 'PlayNow', 'Stop', 'Pause', 'Unpause', 'NextTrack', 'PreviousTrack', 'Seek', 'Rewind', 'FastForward'."
|
|
item_ids:
|
|
type: string
|
|
description: Comma-separated item IDs to queue or play immediately (required for PlayNow).
|
|
time_milliseconds:
|
|
type: integer
|
|
description: Position in milliseconds for Seek/Rewind/FastForward, or start position for PlayNow (e.g. 246000 for 4:06). 0 when unused.
|
|
required:
|
|
- session_id
|
|
- command
|
|
function:
|
|
type: rest
|
|
resource_template: "http://8e663231-emby-mcp:8085/call/control_media_player"
|
|
method: POST
|
|
payload_template: >-
|
|
{{ {
|
|
"session_id": session_id,
|
|
"command": command,
|
|
"item_ids": item_ids | default(""),
|
|
"time_milliseconds": time_milliseconds | default(0)
|
|
} | to_json }}
|
|
value_template: "{{ value_json.result }}"
|
|
|
|
- spec:
|
|
name: emby_season_list
|
|
description: List the seasons of a TV series. Each result's item_id can be passed to emby_episode_list as season_id.
|
|
parameters:
|
|
type: object
|
|
properties:
|
|
series_id:
|
|
type: string
|
|
description: The series item_id obtained from emby_search with item_types 'Series'.
|
|
required:
|
|
- series_id
|
|
function:
|
|
type: rest
|
|
resource_template: "http://8e663231-emby-mcp:8085/call/retrieve_season_list"
|
|
method: POST
|
|
payload_template: >-
|
|
{{ { "series_id": series_id } | to_json }}
|
|
value_template: "{{ value_json.result }}"
|
|
|
|
- spec:
|
|
name: emby_episode_list
|
|
description: List or count the episodes of a TV series, optionally restricted to one season. total_number_of_items gives the episode count.
|
|
parameters:
|
|
type: object
|
|
properties:
|
|
series_id:
|
|
type: string
|
|
description: The series item_id obtained from emby_search with item_types 'Series'.
|
|
season_id:
|
|
type: string
|
|
description: Optional season item_id from emby_season_list to restrict to one season.
|
|
required:
|
|
- series_id
|
|
function:
|
|
type: rest
|
|
resource_template: "http://8e663231-emby-mcp:8085/call/retrieve_episode_list"
|
|
method: POST
|
|
payload_template: >-
|
|
{{ {
|
|
"series_id": series_id,
|
|
"season_id": season_id | default("")
|
|
} | to_json }}
|
|
value_template: "{{ value_json.result }}"
|
|
|
|
- spec:
|
|
name: emby_next_episode
|
|
description: Retrieve the next unplayed episode for a TV series.
|
|
parameters:
|
|
type: object
|
|
properties:
|
|
series_name:
|
|
type: string
|
|
description: The title of the television show.
|
|
required:
|
|
- series_name
|
|
function:
|
|
type: rest
|
|
resource_template: "http://8e663231-emby-mcp:8085/call/retrieve_next_episode"
|
|
method: POST
|
|
payload_template: >-
|
|
{{ {
|
|
"series_name": series_name,
|
|
"series_id": "",
|
|
"mode": "next_unplayed"
|
|
} | to_json }}
|
|
value_template: "{{ value_json.result }}"
|
|
```
|
|
|
|
If you changed the add-on port, replace `8085` in every `resource_template`.
|
|
If **Restrict to localhost** is off, send an `Authorization` header on each request.
|
|
|
|
Item-shaped tool results (search, episode list, next episode, queues) include
|
|
`premiere_date` as `YYYY-MM-DD` when Emby metadata has a first air / release
|
|
date. The Functions YAML above does not declare that field — it arrives inside
|
|
`value_json.result`. Update the system prompt so the model reads it.
|
|
|
|
## Standalone usage (outside Home Assistant)
|
|
|
|
The same Go binary can run outside the add-on, e.g. on a desktop or NAS.
|
|
Build it from this directory:
|
|
|
|
```sh
|
|
go build -o emby-mcp ./cmd/emby-mcp
|
|
```
|
|
|
|
Requires Go 1.27+.
|
|
|
|
### Environment 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 Auth above).
|
|
|
|
### Run modes
|
|
|
|
Startup checks only (login + list libraries, then exit):
|
|
|
|
```sh
|
|
./emby-mcp -check
|
|
```
|
|
|
|
Run the MCP server on stdio:
|
|
|
|
```sh
|
|
./emby-mcp
|
|
```
|
|
|
|
HTTP mode (streamable HTTP transport, SSE streaming at `POST /mcp`):
|
|
|
|
```sh
|
|
MCP_TRANSPORT=http MCP_LISTEN_ADDR=0.0.0.0:8080 ./emby-mcp
|
|
```
|
|
|
|
`/healthz` is an unauthenticated liveness endpoint. 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 (standalone image)
|
|
|
|
```sh
|
|
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 server's `/mcp` endpoint. Every request must
|
|
carry Emby credentials in headers (see Auth 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
|
|
|
|
```sh
|
|
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 text matching
|
|
* `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).
|