Files
ha-gronod-addons/emby-mcp/internal/server/tools_items.go
gronod c4095d0be7 feat: restore premiere_date on item results (1.0.15)
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.
2026-09-23 14:04:30 +00:00

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)
}