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.
138 lines
7.7 KiB
Go
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
|
|
})
|
|
}
|