Build and publish / Test and build (darwin) (push) Failing after 1m42s
Build and publish / Test and build (linux) (push) Failing after 1m53s
Build and publish / Test and build (windows) (push) Failing after 2m50s
Build and publish / Build and publish Docker image (push) Skipped
Go rewrite of the Python Emby.MCP server with tool parity (20 tools), plus a streamable-HTTP transport with per-request Emby authentication (Basic / Bearer token passthrough), a multi-arch Docker build, and a Gitea CI workflow.
288 lines
14 KiB
Go
288 lines
14 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/gordon/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)
|
|
overview (str): short description
|
|
genres (list): list of the genres of all items on the playlist
|
|
date_created (str): date playplist was created
|
|
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, overview, lyrics,
|
|
media_type, run_time, bitrate, 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
|
|
})
|
|
}
|