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.
126 lines
3.4 KiB
Go
126 lines
3.4 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"git.i3omb.com/gronod/emby-mcp/internal/emby"
|
|
"git.i3omb.com/gronod/emby-mcp/internal/state"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
func mkItems(n int) []emby.MediaItem {
|
|
out := make([]emby.MediaItem, n)
|
|
for i := range out {
|
|
out[i] = emby.MediaItem{Title: "t", ItemID: string(rune('a' + i))}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestNextChunkJSON(t *testing.T) {
|
|
st := state.New(nil, "u1", 2)
|
|
st.SetSearch(&state.SearchChunk{
|
|
SearchID: "sid", TotalItems: 5, ChunkSize: 2, Items: mkItems(5),
|
|
})
|
|
|
|
var r chunkResult
|
|
if err := json.Unmarshal([]byte(nextChunkJSON(st)), &r); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.ChunkNumber != 1 || r.ChunkSize != 2 || !r.MoreChunksAvailable || len(r.Items) != 2 {
|
|
t.Fatalf("chunk1 = %+v", r)
|
|
}
|
|
json.Unmarshal([]byte(nextChunkJSON(st)), &r)
|
|
if r.ChunkNumber != 2 || !r.MoreChunksAvailable {
|
|
t.Fatalf("chunk2 = %+v", r)
|
|
}
|
|
json.Unmarshal([]byte(nextChunkJSON(st)), &r)
|
|
if r.ChunkNumber != 3 || r.ChunkSize != 1 || r.MoreChunksAvailable || len(r.Items) != 1 {
|
|
t.Fatalf("chunk3 = %+v", r)
|
|
}
|
|
// State cleared after last chunk.
|
|
if got := nextChunkJSON(st); got != "{}" {
|
|
t.Fatalf("after last chunk = %s", got)
|
|
}
|
|
}
|
|
|
|
func TestNextChunkJSONEmpty(t *testing.T) {
|
|
st := state.New(nil, "u1", 2)
|
|
if got := nextChunkJSON(st); got != "{}" {
|
|
t.Fatalf("empty = %s", got)
|
|
}
|
|
}
|
|
|
|
// end-to-end: MCP client calling tools over in-memory transport against a
|
|
// mocked Emby server.
|
|
func TestToolEndToEnd(t *testing.T) {
|
|
embySrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/Library/MediaFolders":
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"TotalRecordCount": 1,
|
|
"Items": []map[string]any{
|
|
{"Name": "Music", "Type": "CollectionFolder", "CollectionType": "music", "Id": "L1"},
|
|
},
|
|
})
|
|
default:
|
|
t.Errorf("unexpected call %s %s", r.Method, r.URL.Path)
|
|
}
|
|
}))
|
|
defer embySrv.Close()
|
|
|
|
st := state.New(emby.NewClient(embySrv.URL, emby.Options{VerifySSL: true}), "u1", 10)
|
|
srv := New(st)
|
|
|
|
ctx := context.Background()
|
|
t1, t2 := mcp.NewInMemoryTransports()
|
|
if _, err := srv.Connect(ctx, t1, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client := mcp.NewClient(&mcp.Implementation{Name: "test"}, nil)
|
|
cs, err := client.Connect(ctx, t2, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer cs.Close()
|
|
|
|
res, err := cs.CallTool(ctx, &mcp.CallToolParams{Name: "retrieve_library_list"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var libs []emby.Library
|
|
txt := res.Content[0].(*mcp.TextContent).Text
|
|
if err := json.Unmarshal([]byte(txt), &libs); err != nil {
|
|
t.Fatalf("result text = %q", txt)
|
|
}
|
|
if len(libs) != 1 || libs[0].Name != "Music" {
|
|
t.Fatalf("libs = %+v", libs)
|
|
}
|
|
|
|
// select_library by name (case-insensitive).
|
|
res, err = cs.CallTool(ctx, &mcp.CallToolParams{
|
|
Name: "select_library",
|
|
Arguments: map[string]any{"library_name": "music"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if txt := res.Content[0].(*mcp.TextContent).Text; txt != "Success" {
|
|
t.Fatalf("select = %q", txt)
|
|
}
|
|
|
|
// retrieve_current_library returns the selection as JSON.
|
|
res, err = cs.CallTool(ctx, &mcp.CallToolParams{Name: "retrieve_current_library"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cur emby.Library
|
|
json.Unmarshal([]byte(res.Content[0].(*mcp.TextContent).Text), &cur)
|
|
if cur.ID != "L1" {
|
|
t.Fatalf("current = %+v", cur)
|
|
}
|
|
}
|