// Copyright 2025 The Go MCP SDK Authors. All rights reserved. // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package mcp import ( "log" "slices" "testing" "github.com/google/go-cmp/cmp" ) type testItem struct { Name string Value string } type testListParams struct { Cursor string } func (p *testListParams) cursorPtr() *string { return &p.Cursor } type testListResult struct { Items []*testItem NextCursor string } func (r *testListResult) nextCursorPtr() *string { return &r.NextCursor } var allTestItems = []*testItem{ {"alpha", "val-A"}, {"bravo", "val-B"}, {"charlie", "val-C"}, {"delta", "val-D"}, {"echo", "val-E"}, {"foxtrot", "val-F"}, {"golf", "val-G"}, {"hotel", "val-H"}, {"india", "val-I"}, {"juliet", "val-J"}, {"kilo", "val-K"}, } // getCursor encodes a string input into a URL-safe base64 cursor, // fatally logging any encoding errors. func getCursor(input string) string { cursor, err := encodeCursor(input) if err != nil { log.Fatalf("encodeCursor(%s) error = %v", input, err) } return cursor } func TestServerPaginateBasic(t *testing.T) { testCases := []struct { name string initialItems []*testItem inputCursor string inputPageSize int wantFeatures []*testItem wantNextCursor string wantErr bool }{ { name: "FirstPage_DefaultSize_Full", initialItems: allTestItems, inputCursor: "", inputPageSize: 5, wantFeatures: allTestItems[0:5], wantNextCursor: getCursor("echo"), // Based on last item of first page wantErr: false, }, { name: "SecondPage_DefaultSize_Full", initialItems: allTestItems, inputCursor: getCursor("echo"), inputPageSize: 5, wantFeatures: allTestItems[5:10], wantNextCursor: getCursor("juliet"), // Based on last item of second page wantErr: false, }, { name: "SecondPage_DefaultSize_Full_OutOfOrder", initialItems: append(allTestItems[5:], allTestItems[0:5]...), inputCursor: getCursor("echo"), inputPageSize: 5, wantFeatures: allTestItems[5:10], wantNextCursor: getCursor("juliet"), // Based on last item of second page wantErr: false, }, { name: "SecondPage_DefaultSize_Full_Duplicates", initialItems: append(allTestItems, allTestItems[0:5]...), inputCursor: getCursor("echo"), inputPageSize: 5, wantFeatures: allTestItems[5:10], wantNextCursor: getCursor("juliet"), // Based on last item of second page wantErr: false, }, { name: "LastPage_Remaining", initialItems: allTestItems, inputCursor: getCursor("juliet"), inputPageSize: 5, wantFeatures: allTestItems[10:11], // Only 1 item left wantNextCursor: "", // No more pages wantErr: false, }, { name: "PageSize_1", initialItems: allTestItems, inputCursor: "", inputPageSize: 1, wantFeatures: allTestItems[0:1], wantNextCursor: getCursor("alpha"), wantErr: false, }, { name: "PageSize_All", initialItems: allTestItems, inputCursor: "", inputPageSize: len(allTestItems), // Page size equals total wantFeatures: allTestItems, wantNextCursor: "", // No more pages wantErr: false, }, { name: "PageSize_LargerThanAll", initialItems: allTestItems, inputCursor: "", inputPageSize: len(allTestItems) + 5, // Page size larger than total wantFeatures: allTestItems, wantNextCursor: "", wantErr: false, }, { name: "EmptySet", initialItems: nil, inputCursor: "", inputPageSize: 5, wantFeatures: nil, wantNextCursor: "", wantErr: false, }, { name: "InvalidCursor", initialItems: allTestItems, inputCursor: "not-a-valid-gob-base64-cursor", inputPageSize: 5, wantFeatures: nil, // Should be nil for error cases wantNextCursor: "", wantErr: true, }, { name: "AboveNonExistentID", initialItems: allTestItems, inputCursor: getCursor("dne"), // A UID that doesn't exist inputPageSize: 5, wantFeatures: allTestItems[4:9], // Should return elements above UID. wantNextCursor: getCursor("india"), wantErr: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { fs := newFeatureSet(func(t *testItem) string { return t.Name }) fs.add(tc.initialItems...) params := &testListParams{Cursor: tc.inputCursor} gotResult, err := paginateList(fs, tc.inputPageSize, params, &testListResult{}, func(res *testListResult, items []*testItem) { res.Items = items }) if (err != nil) != tc.wantErr { t.Errorf("paginateList(%s) error, got %v, wantErr %v", tc.name, err, tc.wantErr) } if tc.wantErr { return } if diff := cmp.Diff(tc.wantFeatures, gotResult.Items); diff != "" { t.Errorf("paginateList(%s) mismatch (-want +got):\n%s", tc.name, diff) } if tc.wantNextCursor != gotResult.NextCursor { t.Errorf("paginateList(%s) nextCursor, got %v, want %v", tc.name, gotResult.NextCursor, tc.wantNextCursor) } }) } } func TestServerPaginateVariousPageSizes(t *testing.T) { fs := newFeatureSet(func(t *testItem) string { return t.Name }) fs.add(allTestItems...) // Try all possible page sizes, ensuring we get the correct list of items. for pageSize := 1; pageSize < len(allTestItems)+1; pageSize++ { var gotItems []*testItem var nextCursor string wantChunks := slices.Collect(slices.Chunk(allTestItems, pageSize)) index := 0 // Iterate through all pages, comparing sub-slices to the paginated list. for { params := &testListParams{Cursor: nextCursor} gotResult, err := paginateList(fs, pageSize, params, &testListResult{}, func(res *testListResult, items []*testItem) { res.Items = items }) if err != nil { t.Fatalf("paginateList() unexpected error for pageSize %d, cursor %q: %v", pageSize, nextCursor, err) } if diff := cmp.Diff(wantChunks[index], gotResult.Items); diff != "" { t.Errorf("paginateList mismatch (-want +got):\n%s", diff) } gotItems = append(gotItems, gotResult.Items...) nextCursor = gotResult.NextCursor if nextCursor == "" { break } index++ } if len(gotItems) != len(allTestItems) { t.Fatalf("paginateList() returned %d items, want %d", len(allTestItems), len(gotItems)) } } }