Files
gronod 7bcf5322e1 feat: emby-mcp 1.0.16 player owner filter and HA links
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.
2026-09-25 19:58:58 +00:00

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

  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:

- 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 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 for the audit report. Planned features live in docs/ROADMAP.md.