Files
ha-gronod-addons/emby-mcp/internal/server/tools_playlists.go
gronod 2d5763d6c6 feat: slim tool responses, drop lyrics search, rename module (1.0.11)
Item-shaped results now omit empty fields and drop low-value metadata
(overview, lyrics, file_path, creation_date, premiere_date, bitrate);
absent numeric fields drop the key instead of emitting "". Player
sessions report hh:mm:ss times only; playlists drop overview/date_created.
The lyrics_or_description search parameter is removed, along with the
unused DTO fields, and upstream Fields requests are slimmed to match.
Module path renamed to git.i3omb.com/gronod/emby-mcp.
2026-09-21 23:54:07 +01:00

286 lines
14 KiB
Go

package server
import (
"context"
"encoding/json"
"fmt"
"strings"
"git.i3omb.com/gronod/emby-mcp/internal/state"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func registerPlaylistTools(s *mcp.Server, st *state.State) {
mcp.AddTool(s, &mcp.Tool{
Name: "create_playlist",
Description: `Create a new playlist on the Emby server with the supplied name, optional description and optional items to add.
Args:
playlist_name (str): The name of the playlist to create
media_type (str): The type of media the playlist will accept. One of 'Audio', 'Video'.
description (str, optional): A short description of the playlist, or an empty string.
item_ids (str, optional): The ID of one or more items obtained from tool search_for_item to add to the playlist as a comma separated list
Returns:
Dict: as JSON with keys:
playlist_id (str): the unique identifier of the playlist within this Emby server.
success (bool): True if the request was successful, False if an error occured.
error (str): An error message if the request failed, otherwise None.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
PlaylistName string `json:"playlist_name" jsonschema:"The name of the playlist to create"`
MediaType string `json:"media_type" jsonschema:"The type of media the playlist will accept. One of 'Audio', 'Video'."`
Description string `json:"description" jsonschema:"A short description of the playlist, or an empty string"`
ItemIDs string `json:"item_ids" jsonschema:"Comma separated item IDs to add to the playlist"`
}) (*mcp.CallToolResult, any, error) {
libs, err := ensureLibraries(ctx, st)
if err != nil {
return textResult(errf("ERROR: failed to retrieve library list because: %v", err)), nil, nil
}
if len(libs) == 0 {
return textResult(errf("ERROR: No available libraries found. Use tool retrieve_library_list to obtain a list of libraries.")), nil, nil
}
id, err := st.Client.NewPlaylist(ctx, st.UserID, libs, in.PlaylistName, in.MediaType, in.Description)
if err != nil {
return textResult(errf("ERROR: failed to create playlist because: %v", err)), nil, nil
}
result := map[string]any{"playlist_id": id, "success": true}
if in.ItemIDs != "" {
if _, err := st.Client.AddPlaylistItems(ctx, st.UserID, id, in.ItemIDs); err != nil {
msg := errf("ERROR: successfully created the playlist but failed to add items to it because: %v", err)
b, _ := json.Marshal(result)
return textResult(string(b) + "\n" + msg), nil, nil
}
}
return jsonResult(result), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "modify_playlist_name",
Description: `Modifies an existing playlist on the Emby server with the supplied new name and/or new description.
Args:
playlist_id (str): The ID of the playlist to modify, obtained from tool retrieve_playlist_list
new_name (str, optional): A new name for the playlist, or an empty string for no change.
new_description (str, optional): A new short description of the playlist, or an empty string for no change.
Returns:
Str: success messsage or error message.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
PlaylistID string `json:"playlist_id" jsonschema:"The ID of the playlist to modify, obtained from tool retrieve_playlist_list"`
NewName string `json:"new_name" jsonschema:"A new name for the playlist, or an empty string for no change"`
NewDescription string `json:"new_description" jsonschema:"A new short description of the playlist, or an empty string for no change"`
}) (*mcp.CallToolResult, any, error) {
libs, err := ensureLibraries(ctx, st)
if err != nil || len(libs) == 0 {
return textResult(errf("ERROR: No available libraries found. Use tool retrieve_library_list to obtain a list of libraries.")), nil, nil
}
if err := st.Client.SetPlaylistMeta(ctx, st.UserID, libs, in.PlaylistID, in.NewName, in.NewDescription); err != nil {
return textResult(errf("ERROR: failed to modify playlist because: %v", err)), nil, nil
}
return textResult("Playlist successfully modified"), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "retrieve_playlist_list",
Description: `Retrieve a list of playlists available to us on the Emby media server in JSON format.
If you supply an optional playlist_id then only information about this playlist will be returned.
Args:
playlist_id (str, optional): The ID of the playlist to list, obtained from tool retrieve_playlist_list, or an empty string to list all playlists.
Returns:
List of dicts as JSON with keys:
name (str): playlist name (may not be unique)
genres (list): list of the genres of all items on the playlist
run_time (str): the total play length as hh:mm:ss
can_share (bool): True if we can change the sharing of this list
user_access (list of dict): Per-user sharing (only available if can_share is True):
user_name (str): name of user
user_id (str): ID of user
access_level (str): access level this user has for this item
media_type (str): The type of media items the playlist will accept
playlist_id (str): The unique identifier for the list`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
PlaylistID string `json:"playlist_id" jsonschema:"The ID of the playlist to list, or an empty string to list all playlists"`
}) (*mcp.CallToolResult, any, error) {
libs, err := ensureLibraries(ctx, st)
if err != nil || len(libs) == 0 {
return textResult(errf("ERROR: No available libraries found. Use tool retrieve_library_list to obtain a list of libraries.")), nil, nil
}
pls, err := st.Client.GetPlaylists(ctx, st.UserID, libs, in.PlaylistID)
if err != nil {
return textResult(errf("ERROR: failed to retrive list of playlist because: %v", err)), nil, nil
}
// Substitute friendly name for Emby's 'ManageDelete'.
for i := range pls {
for j := range pls[i].UserAccess {
if pls[i].UserAccess[j].AccessLevel == "ManageDelete" {
pls[i].UserAccess[j].AccessLevel = "Full Control"
}
}
}
return jsonResult(pls), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "retrieve_playlist_items",
Description: `Retrieve the list of media items that are on a playlist from the Emby server in JSON format.
Args:
playlist_id (str): The ID of the playlist to list, obtained from tool retrieve_playlist_list.
Returns:
List of dicts as JSON with keys including title, artists, album, genres,
media_type, run_time, item_id, playlist_item_number, playlist_item_index.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
PlaylistID string `json:"playlist_id" jsonschema:"The ID of the playlist to list, obtained from tool retrieve_playlist_list"`
}) (*mcp.CallToolResult, any, error) {
items, err := st.Client.GetPlaylistItems(ctx, st.UserID, in.PlaylistID)
if err != nil {
return textResult(errf("ERROR: failed to retrieve list of items for playlist ID %s because: %v", in.PlaylistID, err)), nil, nil
}
return jsonResult(items), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "add_items_to_playlist",
Description: `Adds one or more items to the end of an existing playlist on the Emby server.
Args:
playlist_id (str): The ID of the playlist to list, obtained from tool retrieve_playlist_list.
item_ids (str): The ID of one or more items obtained from tool search_for_item as a comma separated list to add to the playlist.
Returns:
Str: success messsage or error message.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
PlaylistID string `json:"playlist_id" jsonschema:"The ID of the playlist"`
ItemIDs string `json:"item_ids" jsonschema:"Comma separated item IDs to add"`
}) (*mcp.CallToolResult, any, error) {
n, err := st.Client.AddPlaylistItems(ctx, st.UserID, in.PlaylistID, in.ItemIDs)
if err != nil {
return textResult(errf("ERROR: failed to add items to playlist ID %s because: %v", in.PlaylistID, err)), nil, nil
}
return textResult(fmt.Sprintf("Successfully added %d items to playlist.", n)), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "remove_items_from_playlist",
Description: `Removes one or more items from an existing playlist on the Emby server.
Args:
playlist_id (str): The ID of the playlist to list, obtained from tool retrieve_playlist_list.
playlist_item_numbers (str): The playlist_item_number of one or more items obtained from tool retrieve_playlist_items as a comma separated list to remove from the playlist.
Returns:
Str: success messsage or error message.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
PlaylistID string `json:"playlist_id" jsonschema:"The ID of the playlist"`
PlaylistItemNumbers string `json:"playlist_item_numbers" jsonschema:"Comma separated playlist_item_numbers to remove"`
}) (*mcp.CallToolResult, any, error) {
if err := st.Client.DeletePlaylistItems(ctx, in.PlaylistID, in.PlaylistItemNumbers); err != nil {
return textResult(errf("ERROR: failed to remove items from playlist ID %s because: %v", in.PlaylistID, err)), nil, nil
}
return textResult("Successfully removed items from playlist."), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "reorder_items_on_playlist",
Description: `Moves one items to a new position on an existing playlist on the Emby server.
Specify the playlist using the 'playlist_id' obtained from tool retrieve_playlist_list.
Specify the new position using the zero-based (top of list) 'playlist_item_index' obtained from tool retrieve_playlist_items.
Args:
playlist_id (str): The ID of the playlist to list, obtained from tool retrieve_playlist_list.
playlist_item_number (str): The playlist_item_number of one item obtained from tool retrieve_playlist_items to move within the playlist.
playlist_item_index (str): The new position using the zero-based (zero is top of list) 'playlist_item_index' obtained from tool retrieve_playlist_items.
Returns:
Str: success messsage or error message.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
PlaylistID string `json:"playlist_id" jsonschema:"The ID of the playlist"`
PlaylistItemNumber string `json:"playlist_item_number" jsonschema:"The playlist_item_number of the item to move"`
PlaylistItemIndex string `json:"playlist_item_index" jsonschema:"The new zero-based position"`
}) (*mcp.CallToolResult, any, error) {
if err := st.Client.MovePlaylistItem(ctx, in.PlaylistID, in.PlaylistItemNumber, in.PlaylistItemIndex); err != nil {
return textResult(errf("ERROR: failed to remove items from playlist ID %s because: %v", in.PlaylistID, err)), nil, nil
}
return textResult("Successfully reordered items on playlist."), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "share_playlist_public",
Description: `Shares an existing playlist with all other users of the Emby server as Read access.
Args:
playlist_id (str): The ID of the playlist to list, obtained from tool retrieve_playlist_list.
Returns:
Str: success messsage or error message.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
PlaylistID string `json:"playlist_id" jsonschema:"The ID of the playlist"`
}) (*mcp.CallToolResult, any, error) {
if err := st.Client.SetPlaylistSharing(ctx, in.PlaylistID, "Public", nil, ""); err != nil {
return textResult(errf("ERROR: failed to share playlist ID %s because: %v", in.PlaylistID, err)), nil, nil
}
return textResult("Successfully shared playlist with other users."), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "share_playlist_user_access",
Description: `Shares an existing playlist with specific users of the Emby server and specifi access rights.
Args:
playlist_id (str): The ID of the playlist to list, obtained from tool retrieve_playlist_list.
user_ids (str): the users using a comma separated list of 'user_id' obtained from tool retrieve_user_list.
access_level (str): the access level to grant the users as one of: 'None', 'Read', 'Write', 'Manage', 'Full Control'.
Returns:
Str: success messsage or error message.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
PlaylistID string `json:"playlist_id" jsonschema:"The ID of the playlist"`
UserIDs string `json:"user_ids" jsonschema:"Comma separated list of user_id"`
AccessLevel string `json:"access_level" jsonschema:"One of: 'None', 'Read', 'Write', 'Manage', 'Full Control'"`
}) (*mcp.CallToolResult, any, error) {
level := in.AccessLevel
switch level {
case "None", "Read", "Write", "Manage", "ManageDelete":
case "Full Control":
level = "ManageDelete"
default:
return textResult(fmt.Sprintf("ERROR: unknown access_level %s.", in.AccessLevel)), nil, nil
}
var ids []string
for _, id := range strings.Split(in.UserIDs, ",") {
if id = strings.TrimSpace(id); id != "" {
ids = append(ids, id)
}
}
if err := st.Client.SetPlaylistSharing(ctx, in.PlaylistID, "Shared", ids, level); err != nil {
return textResult(errf("ERROR: failed to share playlist ID %s because: %v", in.PlaylistID, err)), nil, nil
}
return textResult("Successfully shared playlist with other users."), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: "stop_sharing_playlist",
Description: `Stop the public sharing of an existing playlist with other users of the Emby server.
If a user was granted specific access then they will still retain that access after you stop public sharing - use tool
retrieve_playlist_list after you stop sharing to check all users have access_level 'None',
then use tool share_playlist_user_access if needed to grant access_level 'None' to users who still have access.
Args:
playlist_id (str): The ID of the playlist to list, obtained from tool retrieve_playlist_list.
Returns:
Str: success messsage or error message.`,
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
PlaylistID string `json:"playlist_id" jsonschema:"The ID of the playlist"`
}) (*mcp.CallToolResult, any, error) {
if err := st.Client.SetPlaylistSharing(ctx, in.PlaylistID, "Private", nil, ""); err != nil {
return textResult(errf("ERROR: failed to stop sharing playlist ID %s because: %v", in.PlaylistID, err)), nil, nil
}
return textResult("Successfully stopped sharing playlist with other users."), nil, nil
})
}