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.
19 KiB
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
- Add the repository to Home Assistant
(Settings → Add-ons → Add-on Store → ⋮ → Repositories):
https://git.i3omb.com/gronod/ha-gronod-addons - Install Emby MCP
- Set
emby_server_urlplus username/password or an API key - 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
/healthzand/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-IdorX-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:
- 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):
- 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:
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):
./emby-mcp -check
Run the MCP server on stdio:
./emby-mcp
HTTP mode (streamable HTTP transport, SSE streaming at POST /mcp):
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)
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.
# 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)
{
"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):
{
"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):
{
"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 checksinternal/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 authinternal/state— shared session state (library selection, search chunking)internal/config—.envparsinginternal/textutil— Unicode→ASCII folding for text matchingdocs/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 for the
audit report. Planned features live in docs/ROADMAP.md.