internal/mcp: add iterator methods resources and prompts
This CL streamlines the client side pagination logic by adding a generic helper that handles pagination. This also simplifies the existing tools iterator method. Change-Id: If4304deda1ccd22d0c4580d76deaefc64e5dfeb9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/676975 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Robert Findley <rfindley@google.com> Auto-Submit: Sam Thanawalla <samthanawalla@google.com> Reviewed-by: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
parent
7f8b72203a
commit
bb2347385e
1 changed files with 347 additions and 64 deletions
201
mcp/client_test.go
Normal file
201
mcp/client_test.go
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
// 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/modelcontextprotocol/go-sdk/jsonschema"
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
type ListTestParams struct {
|
||||
Cursor string
|
||||
}
|
||||
|
||||
func (p *ListTestParams) cursorPtr() *string {
|
||||
return &p.Cursor
|
||||
}
|
||||
|
||||
type ListTestResult struct {
|
||||
Items []*Item
|
||||
NextCursor string
|
||||
}
|
||||
|
||||
func (r *ListTestResult) nextCursorPtr() *string {
|
||||
return &r.NextCursor
|
||||
}
|
||||
|
||||
var allItems = []*Item{
|
||||
{"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"},
|
||||
}
|
||||
|
||||
func toItemValueSlice(ptrSlice []*Item) []Item {
|
||||
var valSlice []Item
|
||||
for _, ptr := range ptrSlice {
|
||||
valSlice = append(valSlice, *ptr)
|
||||
}
|
||||
return valSlice
|
||||
}
|
||||
|
||||
// generatePaginatedResults is a helper to create a sequence of mock responses for pagination.
|
||||
// It simulates a server returning items in pages based on a given page size.
|
||||
func generatePaginatedResults(all []*Item, pageSize int) []*ListTestResult {
|
||||
if len(all) == 0 {
|
||||
return []*ListTestResult{{Items: []*Item{}, NextCursor: ""}}
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
panic("pageSize must be greater than 0")
|
||||
}
|
||||
numPages := (len(all) + pageSize - 1) / pageSize // Ceiling division
|
||||
var results []*ListTestResult
|
||||
for i := range numPages {
|
||||
startIndex := i * pageSize
|
||||
endIndex := min(startIndex+pageSize, len(all)) // Use min to prevent out of bounds
|
||||
nextCursor := ""
|
||||
if endIndex < len(all) { // If there are more items after this page
|
||||
nextCursor = fmt.Sprintf("cursor_%d", endIndex)
|
||||
}
|
||||
results = append(results, &ListTestResult{Items: all[startIndex:endIndex], NextCursor: nextCursor})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func TestClientPaginateBasic(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testCases := []struct {
|
||||
name string
|
||||
results []*ListTestResult
|
||||
mockError error
|
||||
initialParams *ListTestParams
|
||||
expected []Item
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "SinglePageAllItems",
|
||||
results: generatePaginatedResults(allItems, len(allItems)),
|
||||
expected: toItemValueSlice(allItems),
|
||||
},
|
||||
{
|
||||
name: "MultiplePages",
|
||||
results: generatePaginatedResults(allItems, 3),
|
||||
expected: toItemValueSlice(allItems),
|
||||
},
|
||||
{
|
||||
name: "EmptyResults",
|
||||
results: generatePaginatedResults([]*Item{}, 10),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "ListFuncReturnsErrorImmediately",
|
||||
results: []*ListTestResult{{}},
|
||||
mockError: fmt.Errorf("API error on first call"),
|
||||
expected: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "InitialCursorProvided",
|
||||
initialParams: &ListTestParams{Cursor: "cursor_2"},
|
||||
results: generatePaginatedResults(allItems[2:], 3),
|
||||
expected: toItemValueSlice(allItems[2:]),
|
||||
},
|
||||
{
|
||||
name: "CursorBeyondAllItems",
|
||||
initialParams: &ListTestParams{Cursor: "cursor_999"},
|
||||
results: []*ListTestResult{{Items: []*Item{}, NextCursor: ""}},
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
listFunc := func(ctx context.Context, params *ListTestParams) (*ListTestResult, error) {
|
||||
if len(tc.results) == 0 {
|
||||
t.Fatalf("listFunc called but no more results defined for test case %q", tc.name)
|
||||
}
|
||||
res := tc.results[0]
|
||||
tc.results = tc.results[1:]
|
||||
var err error
|
||||
if tc.mockError != nil {
|
||||
err = tc.mockError
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
params := tc.initialParams
|
||||
if tc.initialParams == nil {
|
||||
params = &ListTestParams{}
|
||||
}
|
||||
|
||||
var gotItems []Item
|
||||
var iterationErr error
|
||||
seq := paginate(ctx, params, listFunc, func(r *ListTestResult) []*Item { return r.Items })
|
||||
for item, err := range seq {
|
||||
if err != nil {
|
||||
iterationErr = err
|
||||
break
|
||||
}
|
||||
gotItems = append(gotItems, item)
|
||||
}
|
||||
if tc.expectError {
|
||||
if iterationErr == nil {
|
||||
t.Errorf("paginate() expected an error during iteration, but got none")
|
||||
}
|
||||
} else {
|
||||
if iterationErr != nil {
|
||||
t.Errorf("paginate() got: %v, want: nil", iterationErr)
|
||||
}
|
||||
}
|
||||
if diff := cmp.Diff(tc.expected, gotItems, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
|
||||
t.Fatalf("paginate() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientPaginateVariousPageSizes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
for i := 1; i < len(allItems)+1; i++ {
|
||||
testname := fmt.Sprintf("PageSize=%d", i)
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
results := generatePaginatedResults(allItems, i)
|
||||
listFunc := func(ctx context.Context, params *ListTestParams) (*ListTestResult, error) {
|
||||
res := results[0]
|
||||
results = results[1:]
|
||||
return res, nil
|
||||
}
|
||||
var gotItems []Item
|
||||
seq := paginate(ctx, &ListTestParams{}, listFunc, func(r *ListTestResult) []*Item { return r.Items })
|
||||
for item, err := range seq {
|
||||
if err != nil {
|
||||
t.Fatalf("paginate() unexpected error during iteration: %v", err)
|
||||
}
|
||||
gotItems = append(gotItems, item)
|
||||
}
|
||||
if diff := cmp.Diff(toItemValueSlice(allItems), gotItems, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
|
||||
t.Fatalf("paginate() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue