Emby PremiereDate is the first air date for episodes/series and the release date for movies. Request it again in Fields, emit it as YYYY-MM-DD on search/browse/queue items, and document the conversation prompt so Assist can answer airdate questions. Potential breaking change: item JSON gains premiere_date versus the 1.0.11 slim shape. HA function parameter YAML is unchanged; re-paste the DOCS.md system prompt.
167 lines
7.5 KiB
Go
167 lines
7.5 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"git.i3omb.com/gronod/emby-mcp/internal/emby"
|
|
"git.i3omb.com/gronod/emby-mcp/internal/state"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// chunkResult is the JSON shape shared by search_for_item and
|
|
// retrieve_next_search_chunk.
|
|
type chunkResult struct {
|
|
SearchID string `json:"search_id"`
|
|
TotalNumberOfItems int `json:"total_number_of_items"`
|
|
ChunkSize int `json:"chunk_size"`
|
|
ChunkNumber int `json:"chunk_number"`
|
|
MoreChunksAvailable bool `json:"more_chunks_available"`
|
|
Items []emby.MediaItem `json:"items"`
|
|
}
|
|
|
|
const searchDescription = `Search for media items on the Emby server by item title or album name, artist name, genre name and release / broadcast years.
|
|
Parameters "and" together to narrow the results. Genre should be a name returned by tool retrieve_genre_list.
|
|
Returns search results as a JSON format, including control data 'total_number_of_items', 'chunk_size' and 'more_chunks_available'
|
|
which indicate whether further search results are available via tool retrieve_next_search_chunk.
|
|
A human may use any returned JSON field to identify an item. You must only supply the corresponding 'item_id' field when using other tools.
|
|
Item fields are omitted when empty.
|
|
|
|
Returns:
|
|
Dict: as JSON with keys:
|
|
search_id (str): The unique ID of the current search
|
|
total_number_of_items (int): Total number of items in the current search
|
|
chunk_size (int): the number of items in the current chunk
|
|
chunk_number (int): the current chunk number (one-based)
|
|
more_chunks_available (bool): False if this is the last chunk, otherwise True.
|
|
items (list of dict): the actual items with keys:
|
|
title (str): the title of the item.
|
|
item_id (str): the unique identifier of the item within this Emby server.
|
|
media_type (str): the item type, either 'Audio' or 'Video'.
|
|
item_type (str): the item kind, e.g. 'Audio', 'Movie', 'Series', 'Episode'.
|
|
artists (list): the artists of the item, as a list of strings.
|
|
album (str): the album of the item.
|
|
album_id (str): the unique identifier of the album within this Emby server.
|
|
album_artist (str): the designated album artist.
|
|
series_name (str): the series the item belongs to.
|
|
disk_number (int): the disk or series number of the item.
|
|
track_number (int): the track or episode number of the item.
|
|
production_year (int): the release / broadcast year of the item.
|
|
premiere_date (str): first air date for episodes/series, or release date for movies, as YYYY-MM-DD when metadata has it.
|
|
genres (list of str): the genres tagged to the item
|
|
run_time (str): the run time / play length of the item as hh:mm:ss.
|
|
played (bool): whether the item has been fully played.
|
|
played_percentage (float): the played percentage when partially played.
|
|
resume_position_milliseconds (int): playback position to resume from.`
|
|
|
|
func registerItemTools(s *mcp.Server, st *state.State) {
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "search_for_item",
|
|
Description: searchDescription,
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct {
|
|
TitleOrAlbum string `json:"title_or_album" jsonschema:"name of item, track, episode or album"`
|
|
ArtistName string `json:"artist_name" jsonschema:"name of artist"`
|
|
GenreName string `json:"genre_name" jsonschema:"genre that items are tagged with"`
|
|
BroadcastReleaseYears string `json:"broadcast_release_years" jsonschema:"The item release year(s). Allows multiple years, comma separated."`
|
|
LyricsOrDescription string `json:"lyrics_or_description,omitempty" jsonschema:"Deprecated: no longer used; accepted for backward compatibility"`
|
|
ItemTypes string `json:"item_types" jsonschema:"comma separated item types to match (e.g. 'Series' to find a TV show). Empty matches Audio and Video media items."`
|
|
}) (*mcp.CallToolResult, any, error) {
|
|
cur := st.CurrentLibrary()
|
|
if cur == nil && in.ItemTypes == "" {
|
|
b, _ := json.Marshal(map[string]string{"error": "ERROR: no library is currently selected. Select library using tool select_library"})
|
|
return textResult(string(b)), nil, nil
|
|
}
|
|
var libID string
|
|
if cur != nil {
|
|
libID = cur.ID
|
|
}
|
|
items, err := st.Client.GetItems(ctx, st.UserID, libID, emby.ItemQuery{
|
|
SearchTerm: in.TitleOrAlbum,
|
|
Artist: in.ArtistName,
|
|
Genre: in.GenreName,
|
|
Years: in.BroadcastReleaseYears,
|
|
IncludeItemTypes: in.ItemTypes,
|
|
EnableUserData: true,
|
|
})
|
|
if err != nil {
|
|
msg := errf("ERROR: failed to retrieve item list because: %v", err)
|
|
b, _ := json.Marshal(map[string]string{"error": msg})
|
|
return textResult(string(b)), nil, nil
|
|
}
|
|
|
|
return chunkedItems(st, items), nil, nil
|
|
})
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "retrieve_next_search_chunk",
|
|
Description: `Retrieve the next chunk of search results that were found by tool search_for_item. Use retrieve_next_search_chunk when you are
|
|
ready to process more media items, and repeat until 'more_chunks_available' is no longer true or no data is returned.
|
|
Returns search results as a JSON format, including control data 'total_number_of_items', 'chunk_size' and 'more_chunks_available'
|
|
which indicate whether further search results are available via tool retrieve_next_search_chunk.
|
|
A human may use any returned JSON field to identify an item. You must only supply the corresponding 'item_id' field when using other tools.
|
|
|
|
Returns:
|
|
Dict: as JSON with keys:
|
|
search_id (str): The unique ID of the current search
|
|
total_number_of_items (int): Total number of items in the current search
|
|
chunk_size (int): the number of items in the current chunk
|
|
chunk_number (int): the current chunk number (one-based)
|
|
more_chunks_available (bool): False if this is the last chunk, otherwise True.
|
|
items (list of dict): the actual items`,
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, in struct{}) (*mcp.CallToolResult, any, error) {
|
|
return textResult(nextChunkJSON(st)), nil, nil
|
|
})
|
|
}
|
|
|
|
// nextChunkJSON slices the next chunk out of the stored search state and
|
|
// returns it as a JSON string. It mirrors the Python chunking semantics,
|
|
// including the empty-object and zeroed-result edge cases.
|
|
func nextChunkJSON(st *state.State) string {
|
|
sc := st.Search()
|
|
if sc == nil {
|
|
return "{}"
|
|
}
|
|
// Defensive zeroed result for inconsistent state.
|
|
if sc.TotalItems <= 0 || len(sc.Items) == 0 || sc.ChunkSize <= 0 || sc.ChunkNum < 0 {
|
|
st.ClearSearch()
|
|
b, _ := json.Marshal(chunkResult{SearchID: sc.SearchID, Items: []emby.MediaItem{}})
|
|
return string(b)
|
|
}
|
|
|
|
start := sc.ChunkNum * sc.ChunkSize // zero-based slice index
|
|
remaining := sc.TotalItems - start
|
|
if remaining <= 0 {
|
|
st.ClearSearch()
|
|
b, _ := json.Marshal(chunkResult{
|
|
SearchID: sc.SearchID,
|
|
TotalNumberOfItems: sc.TotalItems,
|
|
ChunkNumber: sc.ChunkNum,
|
|
Items: []emby.MediaItem{},
|
|
})
|
|
return string(b)
|
|
}
|
|
|
|
size := sc.ChunkSize
|
|
more := true
|
|
end := start + size
|
|
if remaining <= size {
|
|
end = start + remaining
|
|
size = remaining
|
|
more = false
|
|
}
|
|
sc.ChunkNum++
|
|
chunkItems := sc.Items[start:end]
|
|
if !more {
|
|
st.ClearSearch()
|
|
}
|
|
b, _ := json.Marshal(chunkResult{
|
|
SearchID: sc.SearchID,
|
|
TotalNumberOfItems: sc.TotalItems,
|
|
ChunkSize: size,
|
|
ChunkNumber: sc.ChunkNum,
|
|
MoreChunksAvailable: more,
|
|
Items: chunkItems,
|
|
})
|
|
return string(b)
|
|
}
|