Files
ha-gronod-addons/emby-mcp/internal/server/tools_players.go
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

138 lines
7.7 KiB
Go

package server
import (
"context"
"strings"
"git.i3omb.com/gronod/emby-mcp/internal/emby"
"git.i3omb.com/gronod/emby-mcp/internal/state"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func registerPlayerTools(s *mcp.Server, st *state.State) {
mcp.AddTool(s, &mcp.Tool{
Name: "retrieve_player_list",
Description: `Retrieve a list of media players that we can use with the supplied media type in JSON format.
A human may use any JSON field to identify a player, but do not display the 'device_id' or 'session_id'
You must only supply the 'session_id' field to identify the player when using the control_media_player or retrieve_player_queue tools.
Each row includes user_name/user_id of the signed-in or last-used Emby user, device_ip_address when Emby reports it, and online=false for known devices with no live session.
When the user names a room or person, prefer resolve_media_player. Filter with users so other household accounts drop out.
Args:
media_type (str, optional): List only players of this media type (one of: 'Audio', 'Video', 'Photo') or an empty string to list all players
users (str, optional): Comma-separated Emby usernames or user ids to include. Combined with the add-on player_users include-list when empty.
include_offline (bool, optional): When true, also list GET /Devices entries that have no live session (last-used user).
Returns:
List of dicts as JSON with keys including client_name, session_id, device_id, device_name,
device_ip_address, user_id, user_name, online, local_to_media_server, media_types, and now_playing_* fields (times as hh:mm:ss).`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
MediaType string `json:"media_type" jsonschema:"List only players of this media type (one of: 'Audio', 'Video', 'Photo') or empty for all"`
Users string `json:"users" jsonschema:"Comma-separated Emby usernames or user ids to include"`
IncludeOffline bool `json:"include_offline" jsonschema:"Also list known devices with no live session"`
}) (*mcp.CallToolResult, any, error) {
sessions, err := st.Client.GetPlayerSessions(ctx, st.UserID, in.MediaType)
if err != nil {
return textResult(errf("ERROR: failed to retrieve player list because: %v", err)), nil, nil
}
if in.IncludeOffline {
devs, derr := st.Client.GetDevices(ctx)
if derr != nil {
return textResult(errf("ERROR: failed to retrieve device list because: %v", derr)), nil, nil
}
sessions = emby.MergeOfflineDevices(sessions, devs)
}
sessions = emby.FilterPlayersByUsers(sessions, mergeUserFilters(in.Users, st.PlayerUsers))
return jsonResult(sessions), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "resolve_media_player",
Description: `Resolve a spoken room, person, HA entity, Emby device name, device id, or IP to a Home Assistant media_player and an Emby session when one is live.
Use this before PlayNow when the user says "play on the lounge" or similar. If wake_required is true, use Home Assistant to turn on ha_entity and select ha_source (Emby), wait, then resolve again. Do not call PlayNow until session_id is present.
Args:
query (str): Room alias, person, media_player entity_id, Emby device name, device id, or IP address
users (str, optional): Comma-separated include-list of Emby usernames or user ids
Returns:
JSON object with ha_entity, ha_source, emby_device_id, device_ip, session_id, online, user_name, wake_required.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
Query string `json:"query" jsonschema:"Room alias, HA entity, Emby device name, device id, or IP"`
Users string `json:"users" jsonschema:"Comma-separated Emby usernames or user ids to include"`
}) (*mcp.CallToolResult, any, error) {
sessions, err := st.Client.GetPlayerSessions(ctx, st.UserID, "")
if err != nil {
return textResult(errf("ERROR: failed to retrieve player list because: %v", err)), nil, nil
}
devs, derr := st.Client.GetDevices(ctx)
if derr == nil {
sessions = emby.MergeOfflineDevices(sessions, devs)
}
sessions = emby.FilterPlayersByUsers(sessions, mergeUserFilters(in.Users, st.PlayerUsers))
resolved, ok := resolvePlayer(in.Query, st.PlayerLinks, sessions)
if !ok {
return textResult(resolved.Note), nil, nil
}
return jsonResult(resolved), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "retrieve_player_queue",
Description: `Retrieve a list of items in the play queue of a media player in JSON format.
Args:
session_id (str): The ID of the player session to query, obtained from tool retrieve_player_list
Returns:
List of dicts as JSON with keys including title, artists, album, genres,
media_type, run_time, item_id, playlist_item_id.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
SessionID string `json:"session_id" jsonschema:"The ID of the player session to query, obtained from tool retrieve_player_list"`
}) (*mcp.CallToolResult, any, error) {
items, err := st.Client.GetPlayQueueItems(ctx, in.SessionID)
if err != nil {
return textResult(errf("ERROR: failed to retrieve player queue items because: %v", err)), nil, nil
}
return jsonResult(items), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "control_media_player",
Description: `Control the media player identified as 'session_id' by sending it a 'command'.
Valid commands are: 'PlayNow', 'Stop', 'Pause', 'Unpause', 'NextTrack', 'PreviousTrack', 'Seek', 'Rewind', 'FastForward'.
The PlayNow command requires 'item_ids' contain one or more comma separated 'item_id' obtained from the retrieve_item_list_by_genre tool.
Seek, Rewind, FastForward can specify a time in milliseconds. The 'session_id' is obtained from the retrieve_player_list tool.
For PlayNow, 'time_milliseconds' sets the start position (e.g. "play from 4:06" → 246000).
Args:
session_id (str): The ID of the player session to control, obtained from tool retrieve_player_list
command (str): One of 'PlayNow', 'Stop', 'Pause', 'Unpause', 'NextTrack', 'PreviousTrack', 'Seek', 'Rewind', 'FastForward'.
item_ids (str, optional): The ID of one or more items obtained from tool search_for_item to add to the play queue as a comma separated list. Required for command 'PlayNow'.
time_milliseconds (int, optional): The time in milliseconds for commands 'Seek', 'Rewind', 'FastForward', or the start position for 'PlayNow'. If 0 or None then defaults will be used.
Returns:
Str: success messsage or error message.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
SessionID string `json:"session_id" jsonschema:"The ID of the player session to control"`
Command string `json:"command" jsonschema:"One of 'PlayNow', 'Stop', 'Pause', 'Unpause', 'NextTrack', 'PreviousTrack', 'Seek', 'Rewind', 'FastForward'"`
ItemIDs string `json:"item_ids" jsonschema:"Comma separated item IDs; required for 'PlayNow'"`
TimeMilliseconds int64 `json:"time_milliseconds" jsonschema:"Time in ms for 'Seek', 'Rewind', 'FastForward'; start position for 'PlayNow'"`
}) (*mcp.CallToolResult, any, error) {
if in.SessionID == "" {
return textResult("ERROR: no session_id was supplied. Obtain session_id from tool retrieve_player_list"), nil, nil
}
if in.Command == "" {
return textResult("ERROR: no command was supplied. Valid commands are: 'PlayNow', 'Stop', 'Pause', 'Unpause', 'NextTrack', 'PreviousTrack', 'Seek', 'Rewind', 'FastForward'."), nil, nil
}
command := in.Command
if strings.EqualFold(command, "play") {
command = "PlayNow"
}
if err := st.Client.SendPlayerCommand(ctx, in.SessionID, command, in.ItemIDs, st.UserID, in.TimeMilliseconds); err != nil {
return textResult(errf("ERROR: failed to control the player because: %v", err)), nil, nil
}
return textResult("Success"), nil, nil
})
}